style / tools optis & new tools calculator
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
This commit is contained in:
parent
6b84607b25
commit
0db35a0d2c
23 changed files with 1106 additions and 24 deletions
|
|
@ -67,6 +67,16 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.summary",
|
||||
"toolboxes.storageHelp.title",
|
||||
"toolboxes.storageHelp.text",
|
||||
"toolboxes.modules.notepad.placeholder",
|
||||
"toolboxes.modules.checklist.itemPlaceholder",
|
||||
"toolboxes.modules.checklist.importPlaceholder",
|
||||
"toolboxes.modules.links.titlePlaceholder",
|
||||
"toolboxes.modules.links.urlPlaceholder",
|
||||
"toolboxes.modules.links.importPlaceholder",
|
||||
"toolboxes.modules.counters.labelPlaceholder",
|
||||
"toolboxes.modules.calculator.expressionPlaceholder",
|
||||
"toolboxes.modules.calculator.labelPlaceholder",
|
||||
"toolboxes.modules.calculator.scrollableTitle",
|
||||
"toolboxes.emptyTitle",
|
||||
"toolboxes.emptyText"
|
||||
].forEach((path) => assertNonEmptyString(site, path));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
const iconComponent = await readFile("website/src/components/Icon.jsx", "utf8");
|
||||
const gamesPage = await readFile("website/src/features/games/GamesPage.jsx", "utf8");
|
||||
const gameRoute = await readFile("website/src/features/games/GameRoute.jsx", "utf8");
|
||||
const checklistCopyButton = await readFile("website/src/features/games/CopyChecklistItemsButton.jsx", "utf8");
|
||||
const gameLoaders = await readFile("website/src/features/games/loaders.js", "utf8");
|
||||
const gameFiltersPanel = await readFile("website/src/features/games/GameFiltersPanel.jsx", "utf8");
|
||||
const diablo4Page = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8");
|
||||
|
|
@ -42,10 +43,12 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
const damageTable = await readFile("website/src/features/games/mhwilds/cards/DamageTable.jsx", "utf8");
|
||||
const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
|
||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||
const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8");
|
||||
const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8");
|
||||
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
||||
|
||||
assert.match(source, /sokkog:toolboxes/);
|
||||
|
|
@ -64,11 +67,18 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(styleHome, /\.dialogue-line i/);
|
||||
assert.match(styleHome, /font-style: italic/);
|
||||
assert.match(styleToolboxes, /\.module-icon-abacus/);
|
||||
assert.match(styleToolboxes, /\.module-icon-calculator/);
|
||||
assert.match(styleToolboxes, /\.calculator-module/);
|
||||
assert.match(styleToolboxes, /\.toolbox-icon-picker/);
|
||||
assert.match(styleToolboxes, /\.toolbox-game-icon/);
|
||||
assert.match(styleToolboxes, /\.toolbox-hero-link/);
|
||||
assert.match(styleToolboxes, /\.toolbox-actions-row/);
|
||||
assert.match(styleToolboxes, /\.text-import-form/);
|
||||
assert.match(styleIcons, /ui-icon-drag/);
|
||||
assert.match(styleIcons, /ui-icon-dropdown/);
|
||||
assert.match(styleIcons, /ui-icon-enter/);
|
||||
assert.match(styleIcons, /ui-icon-calculator/);
|
||||
assert.match(styleIcons, /ui-icon-scrollable/);
|
||||
assert.match(styleIcons, /ui-icon-flip/);
|
||||
assert.match(styleIcons, /ui-icon-add/);
|
||||
assert.match(styleMhwilds, /\.mhwilds-card/);
|
||||
|
|
@ -91,6 +101,10 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(gameRoute, /export function GameRoute/);
|
||||
assert.match(gameRoute, /MhwildsPage/);
|
||||
assert.match(gameRoute, /Diablo4Page/);
|
||||
assert.match(checklistCopyButton, /formatChecklistImportItems/);
|
||||
assert.match(checklistCopyButton, /\$\{label\}:1/);
|
||||
assert.match(checklistCopyButton, /navigator\.clipboard\.writeText/);
|
||||
assert.match(checklistCopyButton, /results-copy-button/);
|
||||
assert.match(gameLoaders, /export async function loadMhwildsData/);
|
||||
assert.match(gameLoaders, /export async function loadDiablo4Data/);
|
||||
assert.match(gameLoaders, /\/data\/diablo4\/affixes_types\.json/);
|
||||
|
|
@ -100,6 +114,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(diablo4Page, /export function Diablo4Page/);
|
||||
assert.match(diablo4Overview, /export function Diablo4Overview/);
|
||||
assert.match(diablo4Listing, /export function Diablo4Listing/);
|
||||
assert.match(diablo4Listing, /CopyChecklistItemsButton/);
|
||||
assert.match(diablo4Filters, /export function Diablo4Filters/);
|
||||
assert.match(diablo4Filters, /GameFiltersPanel/);
|
||||
assert.match(diablo4AffixCard, /export function Diablo4AffixCard/);
|
||||
|
|
@ -108,6 +123,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(diablo4Utils, /getCategoryIconStyle/);
|
||||
assert.match(mhwildsPage, /export function MhwildsPage/);
|
||||
assert.match(mhwildsListing, /export function MhwildsListing/);
|
||||
assert.match(mhwildsListing, /CopyChecklistItemsButton/);
|
||||
assert.match(mhwildsListing, /endemic_life/);
|
||||
assert.match(mhwildsFilters, /export function MhwildsFilters/);
|
||||
assert.match(mhwildsFilters, /GameFiltersPanel/);
|
||||
|
|
@ -124,13 +140,32 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(moduleRegistry, /screenshots:/);
|
||||
assert.match(moduleRegistry, /links:/);
|
||||
assert.match(moduleRegistry, /counters:/);
|
||||
assert.match(moduleRegistry, /calculator:/);
|
||||
assert.match(moduleRegistry, /editable: false/);
|
||||
assert.match(moduleRegistry, /module-edit-button/);
|
||||
assert.match(notepadModule, /export function NotepadModule/);
|
||||
assert.match(calculatorModule, /export function CalculatorModule/);
|
||||
assert.match(calculatorModule, /calculateExpression/);
|
||||
assert.match(calculatorModule, /activeParentId/);
|
||||
assert.match(calculatorModule, /parentId/);
|
||||
assert.match(calculatorModule, /scrollResults/);
|
||||
assert.match(calculatorModule, /calculator-scroll-toggle/);
|
||||
assert.match(calculatorModule, /EditableCalculatorLabel/);
|
||||
assert.match(calculatorModule, /calculator-entry-label/);
|
||||
assert.match(calculatorModule, /copyChecklistImport/);
|
||||
assert.match(calculatorModule, /resetCalculator/);
|
||||
assert.match(calculatorModule, /Quantité non modifiable/);
|
||||
assert.doesNotMatch(styleToolboxes, /lecture seule/);
|
||||
assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/);
|
||||
assert.match(calculatorModule, /ResizeObserver/);
|
||||
assert.match(calculatorModule, /--calculator-scroll-height/);
|
||||
assert.match(calculatorModule, /CalculatorEntries/);
|
||||
assert.match(checklistModule, /export function ChecklistModule/);
|
||||
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
||||
assert.match(linksModule, /export function LinksModule/);
|
||||
assert.match(countersModule, /export function CountersModule/);
|
||||
assert.match(textImport, /export function parseColonImportLines/);
|
||||
assert.match(textImport, /line\.indexOf\(":\"\)/);
|
||||
assert.match(checklistModule, /editing &&/);
|
||||
assert.match(screenshotsModule, /editing &&/);
|
||||
assert.match(linksModule, /editing &&/);
|
||||
|
|
@ -163,6 +198,11 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /value\.split\("\\n"\)/);
|
||||
assert.match(source, /<br key=/);
|
||||
assert.match(source, /getToolboxGame\(toolbox\)/);
|
||||
assert.match(source, /Vers la page de jeu/);
|
||||
assert.match(source, /Vers la toolbox complète/);
|
||||
assert.match(source, /#\/games\/\$\{toolboxGame\.id\}/);
|
||||
assert.match(source, /#\/toolbox\/\$\{toolbox\.id\}/);
|
||||
assert.match(source, /setDrawerGameId\(""\)/);
|
||||
assert.match(source, /toolbox-card-cover-link/);
|
||||
assert.match(source, /toolbox-icon-cover/);
|
||||
assert.match(source, /moduleColumns/);
|
||||
|
|
@ -184,7 +224,14 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /qtyCurrent/);
|
||||
assert.match(source, /normalizeLinksData/);
|
||||
assert.match(source, /normalizeCountersData/);
|
||||
assert.match(source, /normalizeCalculatorData/);
|
||||
assert.match(source, /scrollResults/);
|
||||
assert.match(checklistModule, /ChecklistItem/);
|
||||
assert.match(checklistModule, /parseColonImportLines/);
|
||||
assert.match(checklistModule, /Number\.parseInt/);
|
||||
assert.match(checklistModule, /checklist-qty-current/);
|
||||
assert.match(linksModule, /parseColonImportLines/);
|
||||
assert.match(linksModule, /context\.normalizeUrl/);
|
||||
assert.match(screenshotsModule, /clipboardData/);
|
||||
assert.match(source, /async function addScreenshotFiles/);
|
||||
assert.match(source, /function ScreenshotViewer/);
|
||||
|
|
@ -217,7 +264,10 @@ test("legacy svg icons are available for reuse", async () => {
|
|||
await readFile("website/public/static/icons/columns.svg", "utf8");
|
||||
await readFile("website/public/static/icons/copy.svg", "utf8");
|
||||
await readFile("website/public/static/icons/abacus.svg", "utf8");
|
||||
await readFile("website/public/static/icons/calculator.svg", "utf8");
|
||||
await readFile("website/public/static/icons/enter.svg", "utf8");
|
||||
await readFile("website/public/static/icons/export.svg", "utf8");
|
||||
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
||||
await readFile("website/public/static/icons/notepad.svg", "utf8");
|
||||
await readFile("website/public/static/icons/picture.svg", "utf8");
|
||||
await readFile("website/public/static/icons/rows.svg", "utf8");
|
||||
|
|
|
|||
16
tests/toolbox-modules.test.mjs
Normal file
16
tests/toolbox-modules.test.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
|
||||
test("colon text import keeps urls intact after the first separator", () => {
|
||||
assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [
|
||||
{ label: "Build", value: "https://example.com/build?class=rogue" },
|
||||
{ label: "Potion", value: "10" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("colon text import accepts labels without value", () => {
|
||||
assert.deepEqual(parseColonImportLines("Simple item"), [
|
||||
{ label: "Simple item", value: "" }
|
||||
]);
|
||||
});
|
||||
|
|
@ -98,6 +98,28 @@
|
|||
"Faites un export global régulier pour garder une sauvegarde."
|
||||
]
|
||||
},
|
||||
"modules": {
|
||||
"notepad": {
|
||||
"placeholder": "Notes rapides..."
|
||||
},
|
||||
"checklist": {
|
||||
"itemPlaceholder": "Nouvel item",
|
||||
"importPlaceholder": "Importer plusieurs items...\nPotion:10\nMéga potion:5"
|
||||
},
|
||||
"links": {
|
||||
"titlePlaceholder": "Nom du lien",
|
||||
"urlPlaceholder": "https://...",
|
||||
"importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
||||
},
|
||||
"counters": {
|
||||
"labelPlaceholder": "Nom du compteur"
|
||||
},
|
||||
"calculator": {
|
||||
"expressionPlaceholder": "10*10",
|
||||
"labelPlaceholder": "ex: Lingots de fer",
|
||||
"scrollableTitle": "Liste scrollable"
|
||||
}
|
||||
},
|
||||
"emptyTitle": "Aucune toolbox",
|
||||
"emptyText": "Créez une première toolbox pour stocker vos outils dans ce navigateur."
|
||||
}
|
||||
|
|
|
|||
4
website/public/static/icons/calculator.svg
Normal file
4
website/public/static/icons/calculator.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 9H19M15 18V15M9 18H9.01M12 18H12.01M12 15H12.01M9 15H9.01M15 12H15.01M12 12H12.01M9 12H9.01M8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V6.2C19 5.0799 19 4.51984 18.782 4.09202C18.5903 3.71569 18.2843 3.40973 17.908 3.21799C17.4802 3 16.9201 3 15.8 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.07989 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.07989 21 8.2 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 886 B |
8
website/public/static/icons/enter.svg
Normal file
8
website/public/static/icons/enter.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 512 512" id="_04_In_alt_" data-name="04 In (alt)" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group_12" data-name="Group 12">
|
||||
<path id="Path_6" data-name="Path 6" d="M480,0H256a32,32,0,0,0,0,64H448V448H256a32,32,0,0,0,0,64H480a31.991,31.991,0,0,0,32-32V32A31.991,31.991,0,0,0,480,0Z" fill-rule="evenodd"/>
|
||||
<path id="Path_7" data-name="Path 7" d="M224,384,352,256,224,128v96H28.591C12.788,224,0,238.328,0,256s12.788,32,28.591,32H224Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 666 B |
16
website/public/static/icons/scrollable.svg
Normal file
16
website/public/static/icons/scrollable.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 32 32" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<path class="st0" d="M28,30H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h24c1.1,0,2,0.9,2,2v24C30,29.1,29.1,30,28,30z"/>
|
||||
<line class="st0" x1="20" y1="2" x2="20" y2="30"/>
|
||||
<polyline class="st0" points="27,24 25,26 23,24 "/>
|
||||
<polyline class="st0" points="23,8 25,6 27,8 "/>
|
||||
<line class="st0" x1="6" y1="9" x2="16" y2="9"/>
|
||||
<line class="st0" x1="6" y1="13" x2="12" y2="13"/>
|
||||
<line class="st0" x1="6" y1="17" x2="12" y2="17"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 855 B |
|
|
@ -1,2 +1,3 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<ns0:svg xmlns:ns0="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 512 512"><ns0:path fill="none" d="M487 37.1C396.4 53.23 292 95.28 207.5 140 163 163.6 124 187.8 95.39 209.2 81.08 220 69.36 230 60.93 238.6c-8.43 8.7-13.38 16.3-14.65 20.3-9.04 28.7-3.42 57.7 1.73 84.7 9.55 50.4-3.23 88.9-22.98 126.3 25.24-5.7 45.36-19.8 57-47 8.47-19.8 9.13-37 11.43-57.6 2.3-20.6 6.45-44.2 22.44-73.2l.2-.4.2-.4c8.8-12.6 26.2-22.2 50-33.4 23.7-11.2 53.6-23 86-35.1 63.8-23.8 137.2-48.7 190.1-71.3 20-30.1 34-74.24 44.6-114.4zm-55 138.2c-51.7 21-116.6 43.1-173.5 64.3-32.2 12-61.8 23.7-84.6 34.5-22.6 10.7-38.5 21.6-42.6 27.2-6.8 12.3-11.1 23.2-14 33.3 83.4-6.5 195.3-31.8 271.3-66.6 27.4-29.7 36.9-59.7 43.4-92.7zm-58 118.8c-79 32.2-182 53.3-260.8 58.6-.9 5-1.5 9.8-2 14.6-.4 3.5-.7 7.1-1.1 10.6 72.4 7.5 136.3 4 206.2-6.5 32.6-22.5 49.8-49.6 57.7-77.3zm-78.4 98.2c-62.3 8.1-121.6 10.2-187.6 3.4-.7 4.5-1.6 9-2.7 13.6 35.9 19.2 98.1 25.8 140.7 24.6 30.2-12.4 41.5-24.8 49.6-41.6zM99.78 426.7c-1.15 2.1-3.14 6.7-4.21 8.9 14.03 20.2 48.73 32.2 88.43 39.3 21.2-8 28.3-15.5 36.5-23-39.7-1.1-86.7-7.7-120.7-25.2z" stroke="#000000" stroke-width="20" stroke-linejoin="round" stroke-linecap="round" /></ns0:svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 273" width="216" height="273">
|
||||
<path d="M 186 25 L 184 24 L 182 24 L 181 23 L 180 24 L 177 24 L 174 27 L 172 31 L 172 33 L 171 35 L 169 37 L 168 40 L 163 46 L 163 47 L 153 57 L 152 57 L 142 66 L 141 66 L 139 68 L 135 70 L 132 73 L 129 74 L 121 80 L 116 82 L 114 84 L 104 89 L 102 91 L 72 106 L 70 108 L 66 110 L 63 113 L 62 113 L 58 117 L 57 117 L 45 129 L 45 130 L 42 133 L 42 134 L 36 142 L 33 148 L 33 150 L 32 151 L 32 153 L 30 157 L 30 161 L 29 162 L 29 180 L 30 181 L 31 188 L 33 191 L 33 193 L 36 199 L 42 206 L 42 207 L 53 218 L 54 218 L 58 222 L 59 222 L 64 226 L 67 227 L 69 229 L 83 236 L 85 236 L 88 238 L 90 238 L 93 240 L 95 240 L 96 241 L 98 241 L 99 242 L 101 242 L 102 243 L 104 243 L 108 245 L 111 245 L 112 246 L 116 246 L 117 247 L 135 247 L 136 246 L 139 246 L 140 245 L 142 245 L 146 243 L 149 240 L 150 240 L 155 235 L 155 234 L 157 232 L 160 226 L 160 224 L 162 220 L 162 217 L 163 216 L 163 209 L 164 208 L 164 198 L 163 197 L 163 192 L 167 190 L 169 188 L 170 188 L 178 181 L 178 180 L 181 177 L 185 169 L 185 166 L 186 165 L 186 154 L 185 153 L 185 150 L 184 149 L 184 147 L 183 145 L 188 140 L 188 139 L 190 137 L 191 135 L 191 133 L 192 132 L 192 130 L 193 129 L 193 125 L 194 124 L 194 117 L 193 116 L 193 111 L 192 110 L 191 105 L 189 101 L 187 99 L 187 96 L 191 89 L 191 87 L 193 83 L 193 80 L 194 79 L 194 75 L 195 74 L 195 53 L 194 52 L 194 47 L 193 46 L 193 42 L 191 38 L 191 35 Z M 178 55 L 180 59 L 180 68 L 179 69 L 179 75 L 178 76 L 177 81 L 173 89 L 171 91 L 171 92 L 167 96 L 167 97 L 159 104 L 158 104 L 150 110 L 140 115 L 138 115 L 136 116 L 133 119 L 132 121 L 132 125 L 134 129 L 135 129 L 137 131 L 142 131 L 143 130 L 145 130 L 148 128 L 150 128 L 156 125 L 161 121 L 167 118 L 175 111 L 177 112 L 177 114 L 178 115 L 178 126 L 176 130 L 167 139 L 151 147 L 149 147 L 148 148 L 146 148 L 145 149 L 143 149 L 139 151 L 136 151 L 134 152 L 131 155 L 130 157 L 130 161 L 131 163 L 134 166 L 136 167 L 140 167 L 141 166 L 144 166 L 145 165 L 147 165 L 148 164 L 150 164 L 151 163 L 156 162 L 168 156 L 170 157 L 170 164 L 168 168 L 161 175 L 160 175 L 158 177 L 148 182 L 146 182 L 143 184 L 141 184 L 137 186 L 134 186 L 133 187 L 130 187 L 129 188 L 125 188 L 124 189 L 121 189 L 119 190 L 116 194 L 116 199 L 117 201 L 120 204 L 129 204 L 130 203 L 133 203 L 134 202 L 137 202 L 138 201 L 141 201 L 142 200 L 145 200 L 146 199 L 148 200 L 148 209 L 147 210 L 147 215 L 146 216 L 146 218 L 145 219 L 145 221 L 144 223 L 137 230 L 135 230 L 134 231 L 131 231 L 130 232 L 125 232 L 124 231 L 118 231 L 117 230 L 114 230 L 113 229 L 110 229 L 109 228 L 107 228 L 106 227 L 101 226 L 98 224 L 96 224 L 88 220 L 86 220 L 82 218 L 80 216 L 75 214 L 73 212 L 72 212 L 70 210 L 66 208 L 62 204 L 61 204 L 52 194 L 52 193 L 50 191 L 46 183 L 46 180 L 45 179 L 45 173 L 44 172 L 44 170 L 45 169 L 45 163 L 46 162 L 46 159 L 47 158 L 47 156 L 51 148 L 53 146 L 53 145 L 56 142 L 56 141 L 70 127 L 71 127 L 74 124 L 75 124 L 80 120 L 83 119 L 85 117 L 98 111 L 100 109 L 113 103 L 115 101 L 120 99 L 122 97 L 132 92 L 134 90 L 135 90 L 137 88 L 138 88 L 140 86 L 141 86 L 143 84 L 144 84 L 146 82 L 150 80 L 158 73 L 159 73 L 166 66 L 167 66 L 167 65 L 169 64 L 170 62 L 177 55 Z" fill="#000000" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.3 KiB |
43
website/src/features/games/CopyChecklistItemsButton.jsx
Normal file
43
website/src/features/games/CopyChecklistItemsButton.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../components/Icon.jsx";
|
||||
|
||||
async function copyText(value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatChecklistImportItems(labels) {
|
||||
return labels
|
||||
.map((label) => String(label || "").trim())
|
||||
.filter(Boolean)
|
||||
.map((label) => `${label}:1`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function CopyChecklistItemsButton({ labels, title = "Copier pour checklist" }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const text = formatChecklistImportItems(labels);
|
||||
|
||||
async function copyItems() {
|
||||
if (!text || !await copyText(text)) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="filter-reset-button results-copy-button"
|
||||
type="button"
|
||||
onClick={copyItems}
|
||||
disabled={!text}
|
||||
aria-label={title}
|
||||
title={copied ? "Copié" : title}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
|
||||
import { Diablo4AffixCard } from "./Diablo4AffixCard.jsx";
|
||||
import { Diablo4Filters } from "./Diablo4Filters.jsx";
|
||||
import { getCategoryLabel, getFilteredDiablo4Affixes } from "./utils.js";
|
||||
|
|
@ -19,6 +20,7 @@ export function Diablo4Listing({ diablo4, filters, setFilters }) {
|
|||
<div className="game-title-row">
|
||||
<h1>Affixes</h1>
|
||||
<span className="results-count">{visible.length} / {diablo4.affixes.length}</span>
|
||||
<CopyChecklistItemsButton labels={visible.map((affix) => affix.label)} title="Copier les affixes visibles pour checklist" />
|
||||
</div>
|
||||
<p>Filtrez les affixes par nom et catégories pour retrouver rapidement les statistiques utiles à votre build.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo } from "react";
|
||||
import { EndemicCard } from "./cards/EndemicCard.jsx";
|
||||
import { MonsterCard } from "./cards/MonsterCard.jsx";
|
||||
import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
|
||||
import { MhwildsFilters } from "./MhwildsFilters.jsx";
|
||||
import { getFilteredMhwildsItems, getMhwildsFilterKey, getUniqueConditionValues } from "./utils.js";
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
|
|||
<div className="game-title-row">
|
||||
<h1>{label}</h1>
|
||||
<span className="results-count">{visible.length} / {items.length}</span>
|
||||
<CopyChecklistItemsButton labels={visible.map((item) => t(item.name, { capitalize: true }))} title={`Copier les ${label.toLowerCase()} visibles pour checklist`} />
|
||||
</div>
|
||||
<p>{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}</p>
|
||||
</div>
|
||||
|
|
|
|||
243
website/src/features/toolboxes/modules/CalculatorModule.jsx
Normal file
243
website/src/features/toolboxes/modules/CalculatorModule.jsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
function calculateExpression(expression) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!normalized || !/^[\d\s+\-*/().]+$/.test(normalized)) return null;
|
||||
|
||||
try {
|
||||
const value = Function(`"use strict"; return (${normalized})`)();
|
||||
return Number.isFinite(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
}
|
||||
|
||||
function getChildren(entries, parentId) {
|
||||
return entries.filter((entry) => (entry.parentId || "") === parentId);
|
||||
}
|
||||
|
||||
function getDescendantIds(entries, parentId) {
|
||||
const children = getChildren(entries, parentId);
|
||||
return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]);
|
||||
}
|
||||
|
||||
function getEntriesInTreeOrder(entries, parentId = "") {
|
||||
return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]);
|
||||
}
|
||||
|
||||
export function CalculatorModule({ toolboxId, moduleId, context }) {
|
||||
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
||||
const textContent = context.moduleText?.calculator || {};
|
||||
const [expression, setExpression] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
const [activeParentId, setActiveParentId] = useState("");
|
||||
const [calculatorHeight, setCalculatorHeight] = useState(0);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const calculatorCardRef = useRef(null);
|
||||
const result = useMemo(() => calculateExpression(expression), [expression]);
|
||||
const activeParent = data.entries.find((entry) => entry.id === activeParentId);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = calculatorCardRef.current;
|
||||
if (!element) return undefined;
|
||||
|
||||
function updateHeight() {
|
||||
setCalculatorHeight(Math.ceil(element.getBoundingClientRect().height));
|
||||
}
|
||||
|
||||
updateHeight();
|
||||
const observer = new ResizeObserver(updateHeight);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
function save(entries) {
|
||||
context.setModuleData(toolboxId, moduleId, { ...data, entries });
|
||||
}
|
||||
|
||||
function setScrollResults(scrollResults) {
|
||||
context.setModuleData(toolboxId, moduleId, { ...data, scrollResults });
|
||||
}
|
||||
|
||||
function saveResult(event) {
|
||||
event.preventDefault();
|
||||
if (result == null) return;
|
||||
const cleanLabel = label.trim();
|
||||
|
||||
save([
|
||||
...data.entries,
|
||||
{
|
||||
id: context.uid("calc"),
|
||||
parentId: activeParentId,
|
||||
label: cleanLabel,
|
||||
value: result
|
||||
}
|
||||
]);
|
||||
setExpression("");
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
function useEntry(entry) {
|
||||
setExpression(formatResult(entry.value));
|
||||
setActiveParentId(entry.id);
|
||||
}
|
||||
|
||||
function deleteEntry(entryId) {
|
||||
const deletedIds = new Set([entryId, ...getDescendantIds(data.entries, entryId)]);
|
||||
if (deletedIds.has(activeParentId)) setActiveParentId("");
|
||||
save(data.entries.filter((entry) => !deletedIds.has(entry.id)));
|
||||
}
|
||||
|
||||
function renameEntry(entryId, label) {
|
||||
const cleanLabel = label.trim();
|
||||
save(data.entries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry));
|
||||
}
|
||||
|
||||
function resetCalculator() {
|
||||
setExpression("");
|
||||
setLabel("");
|
||||
setActiveParentId("");
|
||||
}
|
||||
|
||||
async function copyChecklistImport() {
|
||||
const text = getEntriesInTreeOrder(data.entries)
|
||||
.map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`)
|
||||
.join("\n");
|
||||
if (!text || !await context.copyText(text)) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calculator-module">
|
||||
<form className="calculator-form" onSubmit={saveResult}>
|
||||
<div className="calculator-card" ref={calculatorCardRef}>
|
||||
<label>
|
||||
<span>Calcul</span>
|
||||
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
|
||||
</label>
|
||||
<div className="calculator-result" aria-live="polite">
|
||||
<span>Résultat</span>
|
||||
<strong>{result == null ? "-" : formatResult(result)}</strong>
|
||||
</div>
|
||||
<label>
|
||||
<span>Libellé</span>
|
||||
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||
</label>
|
||||
<button className="primary" disabled={result == null}>Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
className={`calculator-saved ${data.scrollResults ? "is-scrollable" : ""}`}
|
||||
style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined}
|
||||
>
|
||||
<div className="calculator-result-actions">
|
||||
{activeParent && (
|
||||
<button className="calculator-root-button" type="button" onClick={() => setActiveParentId("")}>
|
||||
Revenir à la racine
|
||||
</button>
|
||||
)}
|
||||
<button className="calculator-action-button danger" type="button" onClick={resetCalculator} aria-label="Réinitialiser le calculateur" title="Réinitialiser">
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
<button className="calculator-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label="Copier pour checklist" title={copied ? "Copié" : "Copier pour checklist"}>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button
|
||||
className={`calculator-scroll-toggle ${data.scrollResults ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setScrollResults(!data.scrollResults)}
|
||||
aria-pressed={data.scrollResults}
|
||||
title={textContent.scrollableTitle || "Liste scrollable"}
|
||||
aria-label={textContent.scrollableTitle || "Liste scrollable"}
|
||||
>
|
||||
<Icon name="scrollable" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={`calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
{data.entries.length ? (
|
||||
<CalculatorEntries entries={data.entries} parentId="" activeParentId={activeParentId} onUse={useEntry} onRename={renameEntry} onDelete={deleteEntry} />
|
||||
) : (
|
||||
<p className="muted">Aucun résultat enregistré.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalculatorEntries({ entries, parentId, activeParentId, onUse, onRename, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
const [editingId, setEditingId] = useState("");
|
||||
if (!children.length) return null;
|
||||
|
||||
return (
|
||||
<ul className="calculator-entry-list">
|
||||
{children.map((entry) => (
|
||||
<li className={`${entry.id === activeParentId ? "active" : ""} ${entry.id === editingId ? "is-editing" : ""}`} key={entry.id}>
|
||||
<div className="calculator-entry">
|
||||
{entry.id === editingId ? (
|
||||
<>
|
||||
<span className="calculator-entry-value is-readonly" title="Quantité non modifiable">
|
||||
<strong>{formatResult(entry.value)}</strong>
|
||||
</span>
|
||||
<EditableCalculatorLabel entry={entry} onRename={onRename} onDone={() => setEditingId("")} />
|
||||
</>
|
||||
) : (
|
||||
<button className="calculator-entry-summary" type="button" onClick={() => onUse(entry)} title="Utiliser comme base">
|
||||
<span><strong>{formatResult(entry.value)}</strong> {entry.label}</span>
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`Renommer ${entry.label}`} title="Renommer">
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`Supprimer ${entry.label}`} title="Supprimer">
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} activeParentId={activeParentId} onUse={onUse} onRename={onRename} onDelete={onDelete} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
||||
const [label, setLabel] = useState(entry.label);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [entry.id]);
|
||||
|
||||
function saveLabel() {
|
||||
const cleanLabel = label.trim();
|
||||
if (cleanLabel !== entry.label) onRename(entry.id, cleanLabel);
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="calculator-entry-label"
|
||||
value={label}
|
||||
onChange={(event) => setLabel(event.target.value)}
|
||||
onBlur={saveLabel}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") event.currentTarget.blur();
|
||||
if (event.key === "Escape") {
|
||||
setLabel(entry.label);
|
||||
onDone();
|
||||
}
|
||||
}}
|
||||
aria-label={`Renommer ${entry.label}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { parseColonImportLines } from "./textImport.js";
|
||||
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||
const textContent = context.moduleText?.checklist || {};
|
||||
const [label, setLabel] = useState("");
|
||||
const [qty, setQty] = useState(1);
|
||||
const [textImport, setTextImport] = useState("");
|
||||
|
||||
function save(items) {
|
||||
context.setModuleData(toolboxId, moduleId, { items });
|
||||
|
|
@ -18,14 +21,38 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
setQty(1);
|
||||
}
|
||||
|
||||
function importItems(event) {
|
||||
event.preventDefault();
|
||||
const imported = parseColonImportLines(textImport).map((entry) => ({
|
||||
id: context.uid("item"),
|
||||
label: entry.label,
|
||||
qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1),
|
||||
qtyCurrent: 0
|
||||
}));
|
||||
if (!imported.length) return;
|
||||
save([...data.items, ...imported]);
|
||||
setTextImport("");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form checklist-add-form module-add-panel" onSubmit={addItem}>
|
||||
<input name="label" placeholder="Nouvel item" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<div className="module-add-panel">
|
||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
||||
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importItems}>
|
||||
<textarea
|
||||
value={textImport}
|
||||
onChange={(event) => setTextImport(event.target.value)}
|
||||
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ul className="checklist">
|
||||
{data.items.map((item) => (
|
||||
|
|
@ -56,7 +83,17 @@ function ChecklistItem({ item, context, items, save }) {
|
|||
) : (
|
||||
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label="Retirer une quantité">-</button>
|
||||
<small>{context.clampQty(item.qtyCurrent, item.qtyTarget)}/{item.qtyTarget}</small>
|
||||
<label className="checklist-qty-current">
|
||||
<input
|
||||
type="number"
|
||||
max={item.qtyTarget}
|
||||
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
|
||||
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
|
||||
onChange={(event) => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(event.target.value, entry.qtyTarget) }))}
|
||||
aria-label={`Quantité actuelle pour ${item.label}`}
|
||||
/>
|
||||
<span>/ {item.qtyTarget}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label="Ajouter une quantité">+</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
|||
|
||||
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||
const textContent = context.moduleText?.counters || {};
|
||||
const [label, setLabel] = useState("");
|
||||
|
||||
function save(counters) {
|
||||
|
|
@ -26,7 +27,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
<>
|
||||
{editing && (
|
||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
||||
<input name="label" placeholder="Nom du compteur" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input name="label" placeholder={textContent.labelPlaceholder || "Nom du compteur"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { parseColonImportLines } from "./textImport.js";
|
||||
|
||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||
const textContent = context.moduleText?.links || {};
|
||||
const [title, setTitle] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [textImport, setTextImport] = useState("");
|
||||
const [copiedId, setCopiedId] = useState("");
|
||||
|
||||
function save(links) {
|
||||
|
|
@ -28,6 +31,20 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
setUrl("");
|
||||
}
|
||||
|
||||
function importLinks(event) {
|
||||
event.preventDefault();
|
||||
const imported = parseColonImportLines(textImport)
|
||||
.map((entry) => ({
|
||||
id: context.uid("link"),
|
||||
title: entry.label,
|
||||
url: context.normalizeUrl(entry.value)
|
||||
}))
|
||||
.filter((link) => link.url);
|
||||
if (!imported.length) return;
|
||||
save([...data.links, ...imported]);
|
||||
setTextImport("");
|
||||
}
|
||||
|
||||
async function copyUrl(link) {
|
||||
const copied = await context.copyText(link.url);
|
||||
if (!copied) return;
|
||||
|
|
@ -38,11 +55,22 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form links-add-form module-add-panel" onSubmit={addLink}>
|
||||
<input name="title" placeholder="Nom du lien" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder="https://..." value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<div className="module-add-panel">
|
||||
<form className="inline-form links-add-form" onSubmit={addLink}>
|
||||
<input name="title" placeholder={textContent.titlePlaceholder || "Nom du lien"} value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importLinks}>
|
||||
<textarea
|
||||
value={textImport}
|
||||
onChange={(event) => setTextImport(event.target.value)}
|
||||
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ul className="links-list">
|
||||
{data.links.map((link) => (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useState } from "react";
|
|||
export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||
const data = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||
const [text, setText] = useState(data.text || "");
|
||||
const textContent = context.moduleText?.notepad || {};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
|
|
@ -12,7 +13,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
|||
setText(event.target.value);
|
||||
context.setModuleData(toolboxId, moduleId, { text: event.target.value });
|
||||
}}
|
||||
placeholder="Notes rapides..."
|
||||
placeholder={textContent.placeholder || "Notes rapides..."}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
|
|
@ -13,7 +14,8 @@ const MODULE_COMPONENTS = {
|
|||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
||||
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule, editable: true },
|
||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true }
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true },
|
||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false }
|
||||
};
|
||||
|
||||
const MODULE_COLUMN_GAP_PX = 16;
|
||||
|
|
|
|||
15
website/src/features/toolboxes/modules/textImport.js
Normal file
15
website/src/features/toolboxes/modules/textImport.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export function parseColonImportLines(text) {
|
||||
return String(text || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const separatorIndex = line.indexOf(":");
|
||||
if (separatorIndex === -1) return { label: line, value: "" };
|
||||
return {
|
||||
label: line.slice(0, separatorIndex).trim(),
|
||||
value: line.slice(separatorIndex + 1).trim()
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.label);
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ const STORAGE_KEYS = {
|
|||
const APP_STORAGE_PREFIX = "sokkog:";
|
||||
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c", calc: "r" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
|
|
@ -95,6 +95,24 @@ const DEFAULT_SITE_CONTENT = {
|
|||
text: "",
|
||||
items: []
|
||||
},
|
||||
modules: {
|
||||
notepad: { placeholder: "Notes rapides..." },
|
||||
checklist: {
|
||||
itemPlaceholder: "Nouvel item",
|
||||
importPlaceholder: "Importer plusieurs items...\nPotion:10\nMéga potion:5"
|
||||
},
|
||||
links: {
|
||||
titlePlaceholder: "Nom du lien",
|
||||
urlPlaceholder: "https://...",
|
||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
||||
},
|
||||
counters: { labelPlaceholder: "Nom du compteur" },
|
||||
calculator: {
|
||||
expressionPlaceholder: "10*10",
|
||||
labelPlaceholder: "Lingots de fer",
|
||||
scrollableTitle: "Liste scrollable"
|
||||
}
|
||||
},
|
||||
emptyTitle: "Aucune toolbox",
|
||||
emptyText: ""
|
||||
}
|
||||
|
|
@ -253,7 +271,7 @@ function parsePositiveInt(value, fallback = 1) {
|
|||
function clampQty(value, target) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
const safeValue = Number.isFinite(parsed) ? parsed : 0;
|
||||
return Math.min(Math.max(safeValue, 0), Math.max(1, target));
|
||||
return Math.min(safeValue, Math.max(1, target));
|
||||
}
|
||||
|
||||
function normalizeChecklistItem(item) {
|
||||
|
|
@ -327,6 +345,26 @@ function normalizeCountersData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeCalculatorValue(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function normalizeCalculatorData(data) {
|
||||
const entries = (data?.entries || [])
|
||||
.map((entry) => ({
|
||||
id: entry?.id || uid("calc"),
|
||||
parentId: String(entry?.parentId || ""),
|
||||
label: String(entry?.label || "").trim(),
|
||||
value: normalizeCalculatorValue(entry?.value)
|
||||
}));
|
||||
const entryIds = new Set(entries.map((entry) => entry.id));
|
||||
return {
|
||||
scrollResults: data?.scrollResults === true,
|
||||
entries: entries.map((entry) => entryIds.has(entry.parentId) ? entry : { ...entry, parentId: "" })
|
||||
};
|
||||
}
|
||||
|
||||
function compactChecklistItemForStorage(item) {
|
||||
const normalized = normalizeChecklistItem(item);
|
||||
const compact = { id: normalized.id, label: normalized.label };
|
||||
|
|
@ -369,6 +407,20 @@ function compactModuleDataForStorage(type, value) {
|
|||
}));
|
||||
return counters.length ? { counters } : null;
|
||||
}
|
||||
if (type === "calculator") {
|
||||
const normalized = normalizeCalculatorData(value);
|
||||
const entries = normalized.entries.map((entry) => {
|
||||
const compact = {
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
value: entry.value
|
||||
};
|
||||
if (entry.parentId) compact.parentId = entry.parentId;
|
||||
return compact;
|
||||
});
|
||||
if (!entries.length && !normalized.scrollResults) return null;
|
||||
return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +461,20 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
|
||||
}
|
||||
|
||||
if (type === "calculator") {
|
||||
const entryIdMap = new Map();
|
||||
compact.entries.forEach((entry) => entryIdMap.set(entry.id, nextId("calc")));
|
||||
const remapped = {
|
||||
entries: compact.entries.map((entry) => {
|
||||
const remapped = { ...entry, id: entryIdMap.get(entry.id) };
|
||||
if (entry.parentId) remapped.parentId = entryIdMap.get(entry.parentId) || "";
|
||||
return remapped;
|
||||
})
|
||||
};
|
||||
if (compact.scrollResults) remapped.scrollResults = true;
|
||||
return remapped;
|
||||
}
|
||||
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
@ -842,6 +908,8 @@ function App() {
|
|||
{drawerGameId && (
|
||||
<ToolboxDrawer
|
||||
gameId={drawerGameId}
|
||||
game={getGame(drawerGameId)}
|
||||
siteContent={siteContent}
|
||||
toolboxes={store.toolboxes}
|
||||
links={store.links}
|
||||
actions={actions}
|
||||
|
|
@ -1207,14 +1275,17 @@ function ToolboxGameIcon({ game }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||
const moduleText = siteContent?.toolboxes?.modules || DEFAULT_SITE_CONTENT.toolboxes.modules;
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
moduleText,
|
||||
normalizeChecklistData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
|
|
@ -1255,6 +1326,16 @@ function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, u
|
|||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
</div>
|
||||
{toolboxGame && (
|
||||
<a
|
||||
className="button toolbox-hero-link"
|
||||
href={embedded ? `#/toolbox/${toolbox.id}` : `#/games/${toolboxGame.id}`}
|
||||
onClick={embedded ? () => actions.setDrawerGameId("") : undefined}
|
||||
>
|
||||
<Icon name="enter" />
|
||||
<span>{embedded ? "Vers la toolbox complète" : "Vers la page de jeu"}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<AddToolControls onAdd={addModule} />
|
||||
|
|
@ -1291,7 +1372,7 @@ function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, u
|
|||
);
|
||||
}
|
||||
|
||||
function ToolboxDrawer({ gameId, toolboxes, links, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const selectedId = links[gameId] || "";
|
||||
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
||||
const [width, setWidth] = useState(() => {
|
||||
|
|
@ -1343,7 +1424,7 @@ function ToolboxDrawer({ gameId, toolboxes, links, actions, updateToolbox, updat
|
|||
</div>
|
||||
<button className="drawer-close-button" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||
</header>
|
||||
{toolbox ? <ToolboxView toolbox={toolbox} embedded actions={actions} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addScreenshotFiles={addScreenshotFiles} /> : (
|
||||
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addScreenshotFiles={addScreenshotFiles} /> : (
|
||||
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
|
||||
)}
|
||||
<StorageQuota />
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@
|
|||
font-weight: 800;
|
||||
}
|
||||
|
||||
.results-copy-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.game-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
-webkit-mask-image: url("/static/icons/open.svg");
|
||||
}
|
||||
|
||||
.ui-icon-enter {
|
||||
mask-image: url("/static/icons/enter.svg");
|
||||
-webkit-mask-image: url("/static/icons/enter.svg");
|
||||
}
|
||||
|
||||
.ui-icon-zoom {
|
||||
mask-image: url("/static/icons/zoom.svg");
|
||||
-webkit-mask-image: url("/static/icons/zoom.svg");
|
||||
|
|
@ -56,6 +61,16 @@
|
|||
-webkit-mask-image: url("/static/icons/copy.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calculator {
|
||||
mask-image: url("/static/icons/calculator.svg");
|
||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.ui-icon-scrollable {
|
||||
mask-image: url("/static/icons/scrollable.svg");
|
||||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||
}
|
||||
|
||||
.ui-icon-drag {
|
||||
mask-image: url("/static/icons/drag.svg");
|
||||
-webkit-mask-image: url("/static/icons/drag.svg");
|
||||
|
|
|
|||
|
|
@ -160,6 +160,18 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-module {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.toolbox-actions-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,19 @@
|
|||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.toolbox-hero-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
width: fit-content;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.toolbox-hero-link .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.toolbox-icon-picker {
|
||||
position: relative;
|
||||
z-index: 70;
|
||||
|
|
@ -526,6 +539,11 @@
|
|||
-webkit-mask-image: url("/static/icons/abacus.svg");
|
||||
}
|
||||
|
||||
.module-icon-calculator {
|
||||
mask-image: url("/static/icons/calculator.svg");
|
||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.notepad {
|
||||
display: block;
|
||||
width: calc(100% - 32px);
|
||||
|
|
@ -552,6 +570,32 @@
|
|||
grid-template-columns: minmax(0, 1fr) 76px auto;
|
||||
}
|
||||
|
||||
.module-add-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.module-add-panel .inline-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-import-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-import-form textarea {
|
||||
min-height: 86px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.text-import-form button {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.checklist-qty-input {
|
||||
padding-inline: 10px;
|
||||
text-align: center;
|
||||
|
|
@ -572,6 +616,7 @@ textarea {
|
|||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
|
|
@ -631,10 +676,40 @@ textarea:focus {
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
.checklist-qty-controls small {
|
||||
min-width: 42px;
|
||||
.checklist-qty-current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checklist-qty-current input {
|
||||
width: max(18px, calc((var(--qty-current-digits, 1) * 0.72rem) + 8px));
|
||||
min-height: 28px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.checklist-qty-current input::-webkit-outer-spin-button,
|
||||
.checklist-qty-current input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.checklist-qty-current input:hover,
|
||||
.checklist-qty-current input:focus {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background: rgba(21, 26, 48, 0.42);
|
||||
}
|
||||
|
||||
.checklist-qty-current span {
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.links-add-form {
|
||||
|
|
@ -712,6 +787,398 @@ textarea:focus {
|
|||
font-weight: 900;
|
||||
}
|
||||
|
||||
.calculator-module {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 35fr) minmax(0, 65fr);
|
||||
column-gap: var(--space-6);
|
||||
row-gap: var(--space-4);
|
||||
align-items: start;
|
||||
overflow: hidden;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-self: start;
|
||||
align-content: start;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.calculator-card,
|
||||
.calculator-saved {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.08), transparent 62%),
|
||||
rgba(7, 10, 24, 0.3);
|
||||
}
|
||||
|
||||
.calculator-card {
|
||||
gap: 10px;
|
||||
align-self: start;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.calculator-form label,
|
||||
.calculator-result {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.calculator-card > *,
|
||||
.calculator-form input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.calculator-form label span,
|
||||
.calculator-result span,
|
||||
.calculator-root-button {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calculator-result {
|
||||
min-height: 72px;
|
||||
justify-content: stretch;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 36%),
|
||||
rgba(5, 7, 17, 0.56);
|
||||
}
|
||||
|
||||
.calculator-result strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: clamp(1.55rem, 3vw, 2.15rem);
|
||||
font-weight: 900;
|
||||
line-height: 0.95;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calculator-saved {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
overflow: visible;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.045), transparent 62%),
|
||||
rgba(7, 10, 24, 0.22);
|
||||
}
|
||||
|
||||
.calculator-saved.is-scrollable {
|
||||
height: var(--calculator-scroll-height, auto);
|
||||
max-height: var(--calculator-scroll-height, none);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculator-result-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calculator-root-button {
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.calculator-action-button {
|
||||
display: inline-flex;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.calculator-action-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.1), rgba(7, 10, 24, 0.32));
|
||||
color: #fecdd3;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.54);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.2), rgba(7, 10, 24, 0.44));
|
||||
color: #fff1f2;
|
||||
box-shadow: 0 0 14px rgba(251, 113, 133, 0.16);
|
||||
}
|
||||
|
||||
.calculator-action-button .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 18px 34px;
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 6px 0 10px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle i {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.18);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(7, 10, 24, 0.72);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle i::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-muted);
|
||||
transform: translateY(-50%);
|
||||
transition:
|
||||
left var(--duration-fast) var(--ease-standard),
|
||||
background-color var(--duration-fast) var(--ease-standard),
|
||||
box-shadow var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.calculator-root-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.calculator-root-button:hover,
|
||||
.calculator-action-button:hover:not(:disabled),
|
||||
.calculator-scroll-toggle:hover,
|
||||
.calculator-scroll-toggle.active {
|
||||
border-color: rgba(196, 181, 253, 0.32);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active {
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active i {
|
||||
border-color: rgba(246, 196, 83, 0.36);
|
||||
background: rgba(246, 196, 83, 0.12);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active i::before {
|
||||
left: 17px;
|
||||
background: var(--color-accent-gold);
|
||||
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
||||
}
|
||||
|
||||
.calculator-tree {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.calculator-tree.is-scrollable {
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.calculator-entry-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list {
|
||||
position: relative;
|
||||
margin-top: 6px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
bottom: 14px;
|
||||
left: 7px;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, rgba(246, 196, 83, 0.52), rgba(139, 92, 246, 0.2));
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list > li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list > li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: -15px;
|
||||
width: 11px;
|
||||
height: 1px;
|
||||
background: rgba(246, 196, 83, 0.52);
|
||||
}
|
||||
|
||||
.calculator-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 34px 34px;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.calculator-entry-list li.is-editing > .calculator-entry {
|
||||
grid-template-columns: auto minmax(0, 1fr) 34px 34px;
|
||||
}
|
||||
|
||||
.calculator-entry > button {
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calculator-entry-summary,
|
||||
.calculator-entry-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 1px solid rgba(165, 180, 252, 0.09);
|
||||
border-radius: var(--radius-md);
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
background: rgba(21, 26, 48, 0.58);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.calculator-entry-summary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calculator-entry-value {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.calculator-entry-value.is-readonly {
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
background: rgba(21, 26, 48, 0.38);
|
||||
cursor: default;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calculator-entry-summary:hover,
|
||||
.calculator-entry-value:not(.is-readonly):hover {
|
||||
border-color: rgba(196, 181, 253, 0.2);
|
||||
background: rgba(21, 26, 48, 0.72);
|
||||
}
|
||||
|
||||
.calculator-entry-summary span {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calculator-entry-summary strong,
|
||||
.calculator-entry-value strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-entry-label {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
background: rgba(21, 26, 48, 0.38);
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calculator-entry-label:hover,
|
||||
.calculator-entry-label:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background: rgba(21, 26, 48, 0.58);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-entry .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calculator-entry > button.danger {
|
||||
min-width: 34px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value, .danger) {
|
||||
min-width: 34px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-summary,
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-value,
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-label {
|
||||
border-color: var(--color-accent-gold);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.09), rgba(139, 92, 246, 0.1)),
|
||||
rgba(7, 10, 24, 0.48);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: 0 0 0 1px rgba(246, 196, 83, 0.06);
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue