@pixel-point/toolcraft 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +6 -0
- package/templates/runtime/contracts/component-contracts.test.ts +59 -2
- package/templates/runtime/contracts/component-contracts.ts +23 -2
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +269 -0
- package/templates/runtime/react/controls-panel.tsx +133 -24
- package/templates/runtime/react/settings-transfer.test.ts +3 -3
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/define-toolcraft.test.ts +77 -1
- package/templates/runtime/schema/define-toolcraft.ts +138 -5
- package/templates/runtime/schema/runtime-targets.ts +1 -0
- package/templates/runtime/schema/types.ts +23 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +6 -6
- package/templates/runtime/state/reducer.test.ts +86 -8
- package/templates/runtime/state/reducer.ts +41 -22
- package/templates/runtime/state/types.ts +1 -0
- package/templates/starter/AGENTS.md +2 -2
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +1 -1
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +13 -1
- package/templates/starter/docs/toolcraft/performance.md +5 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +10 -4
- package/templates/starter/gitignore +36 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +55 -0
- package/templates/starter/src/app/starter-acceptance.ts +67 -1
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
|
@@ -146,7 +146,9 @@ Async product actions such as Export, Download, Copy, Generate, or Apply must re
|
|
|
146
146
|
|
|
147
147
|
For complex apps, use schema `settingsTransfer: "auto"` or `true` for settings import/export. Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Do not put Import Settings or Export Settings in sticky footer `panelActions`; runtime inserts the technical `Setup` settings-transfer section first without a visible section heading.
|
|
148
148
|
|
|
149
|
-
If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`,
|
|
149
|
+
If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
|
|
150
|
+
|
|
151
|
+
For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` to the same first technical section. The slider changes backing resolution from `1x` to `2x` without changing visible canvas size; DOM/SVG/vector-native previews should not use it.
|
|
150
152
|
|
|
151
153
|
If a controls panel shows only `Export Settings` and `Import Settings` in the first runtime section, check the canvas sizing decision. Product-output apps usually need `editable-output`; intrinsic media and explicitly fixed output are the cases where visible canvas size inputs are absent.
|
|
152
154
|
|
|
@@ -197,7 +199,7 @@ Do not rerun `pnpm install` after every edit. Run it after fresh export, depende
|
|
|
197
199
|
|
|
198
200
|
Use `pnpm verify:ui` when a tier calls for the browser acceptance suite without the performance suite. Use a focused named Playwright test instead when only one entity changed and the relevant test is already known.
|
|
199
201
|
|
|
200
|
-
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of the app exists, when renderer/canvas/animation/export/timeline/layers change, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
202
|
+
Run a full performance checkpoint with `pnpm verify:perf` when the first working version of the app exists, when renderer/canvas/animation/export/timeline/layers change, when `canvas.renderScale` or the `Resolution scale` retina slider is added/enabled, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
201
203
|
|
|
202
204
|
Fast feature loops may defer full performance only when none of the checkpoint triggers apply. Record the deferred check and reason in the worklog.
|
|
203
205
|
|
|
@@ -50,6 +50,10 @@ Visual discrete sliders must declare `step`; the runtime derives one marker per
|
|
|
50
50
|
|
|
51
51
|
Schema sliders always render stacked at full width. Do not put `slider` or `rangeSlider` controls in two-column inline rows. The only built-in exception is `fontPicker`, whose letter-spacing and line-height footer sliders stay paired inside that component.
|
|
52
52
|
|
|
53
|
+
Use slider `unit` only for measurement or scale suffixes: `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, `cols`, or a similarly useful domain unit. Do not use `unit` to repeat the entity already named by the section or label. Avoid `Letters` + `letters`, `Shape Density / Count` + `shapes`, `Words` + `words`, `Symbols` + `symbols`, `Items` + `items`, `Particles` + `particles`, and `Layers` + `layers`. If the numeric value needs an entity noun to make sense, rename the label or section instead of appending the noun to the value. Compact units render tight (`70%`, `24px`, `1.2x`, `8s`); word or acronym units render with a space (`5 cols`, `17 fps`) only when they are truly needed.
|
|
54
|
+
|
|
55
|
+
Slider value labels are editable only when they contain a numeric value. Textual state labels such as `Normal` are display-only and must not expose hover or click editing affordances.
|
|
56
|
+
|
|
53
57
|
Range sliders are always full-width two-thumb controls. Do not put a `rangeSlider` in an inline row. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the control does not collapse into a single-value slider.
|
|
54
58
|
|
|
55
59
|
Range slider value editing accepts common range separators such as `20/80`, `20-80`, `20 - 80`, `20 80`, and en-dash ranges. Use the built-in parser instead of adding custom label parsing.
|
|
@@ -134,6 +138,8 @@ Use `fileDrop` for source material uploads in the controls panel. Do not place u
|
|
|
134
138
|
|
|
135
139
|
In single-layer apps, the runtime shows uploaded image preview and clear button in the file control. Clearing removes source material from the renderer and canvas.
|
|
136
140
|
|
|
141
|
+
Use `multiple: true` when the app needs several uploaded images as one source set. The runtime appends media, switches to a four-column thumbnail grid when more than one image is present, puts the add-more tile last, and keeps per-image removal inside the file control.
|
|
142
|
+
|
|
137
143
|
In multi-layer apps, deletion and visibility belong to the Layers panel; `fileDrop` stays an upload target.
|
|
138
144
|
|
|
139
145
|
## Image Picker
|
|
@@ -211,6 +217,8 @@ Short labels must still be semantically sufficient with nearby context. `Animati
|
|
|
211
217
|
|
|
212
218
|
Visible control labels can get a runtime-owned filled Phosphor question tooltip icon. Put a concise product-specific explanation in `description` only when it adds meaning beyond the label. Do not write recaps like `Adjusts Opacity`, and do not build custom help icons beside built-in labels.
|
|
213
219
|
|
|
220
|
+
Do not add `description` to obvious color clusters. If a section title already names the palette/color context, sequential labels such as `Color 1`, `Color 2`, or simple palette controls such as `Spread` do not need help icons. Keep the whole obvious group clean unless the tooltip explains a non-obvious product behavior.
|
|
221
|
+
|
|
214
222
|
For compound controls such as `fontPicker`, `description` must not enumerate owned fields like font, weight, size, case, color, opacity, letter spacing, or line height. The component already labels those fields.
|
|
215
223
|
|
|
216
224
|
If a source label is unavoidably long, keep the visible label concise and rely on native `title` for the full text.
|
|
@@ -267,10 +275,14 @@ Use schema `settingsTransfer` for settings import/export. Do not add Import Sett
|
|
|
267
275
|
|
|
268
276
|
Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. If the threshold is reached, use `settingsTransfer: "auto"` / `true` or document a product-specific opt-out through `runtime.settingsTransfer` acceptance evidence.
|
|
269
277
|
|
|
270
|
-
When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`,
|
|
278
|
+
When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
|
|
271
279
|
|
|
272
280
|
If only `Export Settings` and `Import Settings` appear in that section, the schema is not using `editable-output` canvas sizing or already owns `canvas.size.width` / `canvas.size.height` controls. For product-output apps, prefer fixing the canvas sizing decision over adding hand-built size fields.
|
|
273
281
|
|
|
282
|
+
Manual `Canvas width` or `Canvas height` edits are exact output-size edits. They keep the other dimension unchanged, switch `Aspect ratio` to `Custom`, and update the custom ratio inputs to the reduced current ratio. Do not recreate the old behavior where typing one size field stays locked to the previous aspect preset.
|
|
283
|
+
|
|
284
|
+
Enable `canvas.renderScale: true` for non-vector raster previews such as Canvas 2D, WebGL, or WebGPU output. Runtime adds a `Resolution scale` slider after canvas sizing; it defaults to `2x` and lets users trade preview quality/performance without changing output size. Adding or enabling this slider requires a full `pnpm verify:perf` checkpoint. Performance fixes must preserve the selected scale and keep canvas preview responsive while dragging sliders or other high-frequency controls. Diagnose the actual bottleneck before lowering quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews.
|
|
285
|
+
|
|
274
286
|
Reset belongs to the controls panel header reset button. Do not add a footer action with `label`, `value`, or `command` containing reset; acceptance treats that as a duplicate Reset.
|
|
275
287
|
|
|
276
288
|
Still-output product apps include one primary `Export PNG` action.
|
|
@@ -55,6 +55,8 @@ Ordinary controls still need lightweight responsiveness checks. They should not
|
|
|
55
55
|
- panel scroll affecting canvas zoom;
|
|
56
56
|
- timeline or layer interactions destabilizing the viewport.
|
|
57
57
|
|
|
58
|
+
When `canvas.renderScale` / `Resolution scale` is enabled, responsiveness coverage must include slider or other high-frequency control drags at the selected scale. If the canvas lags, diagnose the source before changing quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling.
|
|
59
|
+
|
|
58
60
|
## Renderer Performance
|
|
59
61
|
|
|
60
62
|
Custom renderers should:
|
|
@@ -105,8 +107,11 @@ Run a full performance checkpoint with `pnpm verify:perf` when:
|
|
|
105
107
|
|
|
106
108
|
- the first working version of the app exists;
|
|
107
109
|
- renderer, canvas, animation, export, timeline, or layers change;
|
|
110
|
+
- `canvas.renderScale` or the `Resolution scale` retina slider is added/enabled;
|
|
108
111
|
- a bug that previously broke functionality is fixed;
|
|
109
112
|
- a performance optimization lands;
|
|
110
113
|
- the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
|
|
111
114
|
|
|
115
|
+
Performance fixes must preserve selected output and preview quality. Do not pass budgets by lowering image quality, selected `canvas.renderScale`, export resolution, source media fidelity, or canvas backing pixels unless the user explicitly chooses that lower-quality value through a visible control. Prefer coalescing slider updates, caching expensive inputs, moving work off the React render path, reusing GPU resources, or changing renderer strategy over reducing visual fidelity.
|
|
116
|
+
|
|
112
117
|
Do not use the full performance suite as the default loop for Tier 0-2 edits. Those edits still need the targeted checks named by the verification tier, but they should not pay for renderer and viewport stress tests unless a checkpoint trigger applies. If a fast feature loop defers full performance, record the deferred check and reason in the worklog.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Choose render technology per product layer. Do not choose a renderer because it is convenient; choose it from product output semantics, reference behavior, fidelity, and workload.
|
|
4
4
|
|
|
5
|
-
The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery.
|
|
5
|
+
The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery. Do not make a renderer look fast by silently reducing the selected preview scale, backing pixels, source media quality, or export fidelity.
|
|
6
6
|
|
|
7
7
|
## Strategy Guide
|
|
8
8
|
|
|
@@ -9,7 +9,7 @@ Edit `src/app/app-schema.ts` as the public product surface.
|
|
|
9
9
|
- Use `settingsTransfer: "auto"` for complex apps that should let users import/export control settings.
|
|
10
10
|
- Bind every control to a schema `target`.
|
|
11
11
|
- Use `defaultValue` for reset behavior.
|
|
12
|
-
- Use `description` for product-specific help beside a visible label. Keep `label` short. Omit `description` instead of writing label recaps like `Adjusts Opacity`;
|
|
12
|
+
- Use `description` for product-specific help beside a visible label. Keep `label` short. Omit `description` instead of writing label recaps like `Adjusts Opacity`; also omit it for obvious color clusters such as `Color 1` / `Color 2` inside a color section. Compound controls such as `fontPicker` must not use `description` to list their own fields.
|
|
13
13
|
- Use `disabled: true` only when the control is intentionally unavailable; the runtime renders the disabled visual and interaction state.
|
|
14
14
|
- Use `visibleWhen` when a control or section exists only for a specific template, type, mode, variant, or count. Hidden values are preserved. A section with no visible controls is hidden automatically.
|
|
15
15
|
- Use `disabledWhen` when a control belongs to the current entity but is temporarily unavailable in the selected state. The value is preserved while disabled.
|
|
@@ -134,10 +134,14 @@ Choose sizing from product context:
|
|
|
134
134
|
|
|
135
135
|
For product output, export, copy, download, shader rendering, procedural rendering, or no single intrinsic source image, use `editable-output` unless the product explicitly needs `fixed-output`.
|
|
136
136
|
|
|
137
|
-
A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
|
|
137
|
+
A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). When no explicit product size is provided, runtime defaults to `16:9` / `1920x1080`; choose another preset only when the product meaning calls for it. Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
|
|
138
138
|
|
|
139
139
|
Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory only for `editable-output` sizing. They do not depend on `settingsTransfer`: when settings transfer is off, the runtime prepends a technical `Setup` canvas size section without a visible heading; when settings transfer is on, the controls merge into the first technical `Setup` runtime settings section without a visible heading. Do not hand-build a duplicate size selector.
|
|
140
140
|
|
|
141
|
+
When the user manually edits `Canvas width` or `Canvas height`, the runtime treats that as an exact custom output size. It keeps the typed dimension, keeps the other dimension unchanged, switches `Aspect ratio` to `Custom`, and shows the reduced current ratio in the custom ratio inputs. Only selecting an aspect preset may resize both dimensions from a canonical preset.
|
|
142
|
+
|
|
143
|
+
For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` after canvas sizing in the first technical `Setup` section. The slider ranges from `1x` to `2x`, defaults to `2x`, and changes the renderer backing pixels without changing the visible CSS size or product output dimensions. Adding or enabling this slider requires a full `pnpm verify:perf` checkpoint. Performance fixes must preserve the selected scale and keep the canvas responsive while dragging sliders or other high-frequency controls at that scale. Diagnose whether lag comes from renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling before reducing quality. Do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews; use native vector rendering for those.
|
|
144
|
+
|
|
141
145
|
## Panels
|
|
142
146
|
|
|
143
147
|
- Use `panels: {}` for the neutral starter or for products that have no user-facing panels yet.
|
|
@@ -161,7 +165,7 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
|
|
|
161
165
|
| `color` | Hex color picker | `defaultValue: { hex }`, `target`, `label` |
|
|
162
166
|
| `colorOpacity` | Hex color picker plus opacity percent input | `defaultValue: { hex, opacity }`, `target`, `label` |
|
|
163
167
|
| `curves` | RGB or single curve editor | `defaultValue`, `target`, `variant: "single"`, `interpolation: "smooth" \| "monotone"` |
|
|
164
|
-
| `fileDrop` | Upload/drop input | `accept`, `target` |
|
|
168
|
+
| `fileDrop` | Upload/drop input | `accept`, `multiple`, `target` |
|
|
165
169
|
| `fontPicker` | Font preview select with popup, category search, weight, size, text case, text color/opacity, letter spacing, and line height; product text must consume `fontId`, `fontWeight`, `fontSize`, `letterSpacing`, `lineHeight`, `textCase`, `color`, and `opacity` | `defaultValue: { fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, opacity }`, `target` |
|
|
166
170
|
| `gradient` | Gradient editor | `defaultValue: { angle, gradientType, stops }`, `target` |
|
|
167
171
|
| `imagePicker` | Image choice grid | `items`, `defaultValue`, `target` |
|
|
@@ -178,6 +182,8 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
|
|
|
178
182
|
|
|
179
183
|
`text` defaults to `commitMode: "content"` and applies while typing for real content such as prompts, names, titles, tokens, and short text. Use `commitMode: "setting"` for text inputs that edit settings such as font size, numeric-like style values, dimensions, ids, or configuration fields; setting text commits on blur or Enter. Canvas width and Canvas height always commit on blur or Enter. `code` / `CodeTextarea` is a content editor, applies while typing, and is capped at 12 visible lines. Long content scrolls inside the textarea instead of making the controls panel taller.
|
|
180
184
|
|
|
185
|
+
For `slider` and `rangeSlider`, `unit` is a measurement or scale suffix, not the entity being counted. Use units such as `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, or `cols` only when they clarify the number. Do not add repeated nouns such as `letters`, `shapes`, `words`, `symbols`, `items`, `particles`, or `layers` when the label or section already names that entity. If the value needs a noun, rename the label or section. Word or acronym units, when truly needed, render with a space (`5 cols`, `17 fps`); compact symbol/CSS units stay tight (`70%`, `24px`).
|
|
186
|
+
|
|
181
187
|
## Control Selection Inventory
|
|
182
188
|
|
|
183
189
|
Before writing schema controls, map product needs to built-ins by value model, not visual similarity.
|
|
@@ -296,7 +302,7 @@ When enabled, the runtime inserts a technical `Setup` settings-transfer section
|
|
|
296
302
|
|
|
297
303
|
After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. If the threshold is reached, use `"auto"` / `true` or add an explicit `runtime.settingsTransfer` opt-out acceptance row with product evidence.
|
|
298
304
|
|
|
299
|
-
When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`,
|
|
305
|
+
When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate sections or recreate them manually.
|
|
300
306
|
|
|
301
307
|
A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output` or the app already declares its own `canvas.size.width` / `canvas.size.height` controls. For product-output apps, treat that as a schema decision to review.
|
|
302
308
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist
|
|
6
|
+
dist-ssr
|
|
7
|
+
.vite
|
|
8
|
+
.turbo
|
|
9
|
+
.output
|
|
10
|
+
.next
|
|
11
|
+
*.tsbuildinfo
|
|
12
|
+
|
|
13
|
+
# Test output
|
|
14
|
+
coverage
|
|
15
|
+
test-results
|
|
16
|
+
playwright-report
|
|
17
|
+
|
|
18
|
+
# Local environment
|
|
19
|
+
.env
|
|
20
|
+
.env.*
|
|
21
|
+
!.env.example
|
|
22
|
+
|
|
23
|
+
# Logs
|
|
24
|
+
logs
|
|
25
|
+
*.log
|
|
26
|
+
npm-debug.log*
|
|
27
|
+
pnpm-debug.log*
|
|
28
|
+
yarn-debug.log*
|
|
29
|
+
|
|
30
|
+
# OS and editor files
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
.idea
|
|
34
|
+
.vscode/*
|
|
35
|
+
!.vscode/extensions.json
|
|
36
|
+
!.vscode/settings.json
|
|
@@ -5997,6 +5997,61 @@ describe("Toolcraft template app acceptance coverage", () => {
|
|
|
5997
5997
|
);
|
|
5998
5998
|
});
|
|
5999
5999
|
|
|
6000
|
+
it("rejects redundant descriptions in obvious color sections", () => {
|
|
6001
|
+
const schemaWithObviousColorHelp = defineToolcraft({
|
|
6002
|
+
canvas: { enabled: true },
|
|
6003
|
+
panels: {
|
|
6004
|
+
controls: {
|
|
6005
|
+
sections: [
|
|
6006
|
+
{
|
|
6007
|
+
controls: {
|
|
6008
|
+
color1: {
|
|
6009
|
+
defaultValue: { hex: "#DFFF1A" },
|
|
6010
|
+
description: "Sets the first bead color.",
|
|
6011
|
+
label: "Color 1",
|
|
6012
|
+
target: "beads.color1",
|
|
6013
|
+
type: "color",
|
|
6014
|
+
},
|
|
6015
|
+
color2: {
|
|
6016
|
+
defaultValue: { hex: "#8CFF3A" },
|
|
6017
|
+
description: "Sets the second bead color.",
|
|
6018
|
+
label: "Color 2",
|
|
6019
|
+
target: "beads.color2",
|
|
6020
|
+
type: "color",
|
|
6021
|
+
},
|
|
6022
|
+
colorSpread: {
|
|
6023
|
+
defaultValue: 34,
|
|
6024
|
+
description:
|
|
6025
|
+
"Controls how often beads use colors 2-5 instead of Color 1.",
|
|
6026
|
+
label: "Spread",
|
|
6027
|
+
max: 100,
|
|
6028
|
+
min: 0,
|
|
6029
|
+
target: "beads.colorSpread",
|
|
6030
|
+
type: "slider",
|
|
6031
|
+
unit: "%",
|
|
6032
|
+
},
|
|
6033
|
+
},
|
|
6034
|
+
title: "Bead Colors",
|
|
6035
|
+
},
|
|
6036
|
+
],
|
|
6037
|
+
title: "Controls",
|
|
6038
|
+
},
|
|
6039
|
+
},
|
|
6040
|
+
});
|
|
6041
|
+
|
|
6042
|
+
expect(validateToolcraftAcceptanceCoverage(schemaWithObviousColorHelp, [
|
|
6043
|
+
makeControlAcceptance("beads.color1", "color"),
|
|
6044
|
+
makeControlAcceptance("beads.color2", "color"),
|
|
6045
|
+
makeControlAcceptance("beads.colorSpread", "slider"),
|
|
6046
|
+
])).toEqual(
|
|
6047
|
+
expect.arrayContaining([
|
|
6048
|
+
"Bead Colors / color1 description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
|
|
6049
|
+
"Bead Colors / color2 description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
|
|
6050
|
+
"Bead Colors / colorSpread description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
|
|
6051
|
+
]),
|
|
6052
|
+
);
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6000
6055
|
it("requires mode selectors to appear before dependent controls", () => {
|
|
6001
6056
|
const schemaWithLateModeSelector = {
|
|
6002
6057
|
...starterSchema,
|
|
@@ -1561,14 +1561,33 @@ function getToolcraftControlDescriptionError({
|
|
|
1561
1561
|
control,
|
|
1562
1562
|
controlId,
|
|
1563
1563
|
sectionLabel,
|
|
1564
|
+
sectionTitle,
|
|
1564
1565
|
}: {
|
|
1565
1566
|
control: ToolcraftControlSchema;
|
|
1566
1567
|
controlId: string;
|
|
1567
1568
|
sectionLabel: string;
|
|
1569
|
+
sectionTitle: string | undefined;
|
|
1568
1570
|
}): string | undefined {
|
|
1569
1571
|
const description = control.description?.trim();
|
|
1570
1572
|
|
|
1571
|
-
if (!description
|
|
1573
|
+
if (!description) {
|
|
1574
|
+
return undefined;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const label = getControlLabelText(control).trim();
|
|
1578
|
+
|
|
1579
|
+
if (
|
|
1580
|
+
isToolcraftObviousColorSectionControlDescription({
|
|
1581
|
+
control,
|
|
1582
|
+
description,
|
|
1583
|
+
label,
|
|
1584
|
+
sectionTitle,
|
|
1585
|
+
})
|
|
1586
|
+
) {
|
|
1587
|
+
return `${sectionLabel} / ${controlId} description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (control.type !== "fontPicker") {
|
|
1572
1591
|
return undefined;
|
|
1573
1592
|
}
|
|
1574
1593
|
|
|
@@ -1583,6 +1602,52 @@ function getToolcraftControlDescriptionError({
|
|
|
1583
1602
|
return `${sectionLabel} / ${controlId} description repeats FontPicker-owned fields (${repeatedParts.join(", ")}). FontPicker help must explain only non-obvious product behavior; use section titles and visible field labels for font family, weight, size, case, color, opacity, letter spacing, and line height, or omit description.`;
|
|
1584
1603
|
}
|
|
1585
1604
|
|
|
1605
|
+
function isToolcraftColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1606
|
+
return /\b(colou?rs?|palette|palettes)\b/i.test(sectionTitle ?? "");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function isToolcraftSequentialColorLabel(label: string): boolean {
|
|
1610
|
+
return /^colou?r\s+\d+$/i.test(label.trim());
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function isToolcraftSimplePaletteDistributionLabel(label: string): boolean {
|
|
1614
|
+
return /^(spread|mix|distribution)$/i.test(label.trim());
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function isToolcraftGenericControlHelpDescription(description: string): boolean {
|
|
1618
|
+
return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
|
|
1619
|
+
description.trim(),
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function isToolcraftObviousColorSectionControlDescription({
|
|
1624
|
+
control,
|
|
1625
|
+
description,
|
|
1626
|
+
label,
|
|
1627
|
+
sectionTitle,
|
|
1628
|
+
}: {
|
|
1629
|
+
control: ToolcraftControlSchema;
|
|
1630
|
+
description: string;
|
|
1631
|
+
label: string;
|
|
1632
|
+
sectionTitle: string | undefined;
|
|
1633
|
+
}): boolean {
|
|
1634
|
+
if (!isToolcraftColorSectionTitle(sectionTitle)) {
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (
|
|
1639
|
+
(control.type === "color" || control.type === "colorOpacity") &&
|
|
1640
|
+
isToolcraftSequentialColorLabel(label)
|
|
1641
|
+
) {
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return (
|
|
1646
|
+
isToolcraftSimplePaletteDistributionLabel(label) &&
|
|
1647
|
+
isToolcraftGenericControlHelpDescription(description)
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1586
1651
|
function getToolcraftControlSectionGroupingErrors(
|
|
1587
1652
|
schema: ResolvedToolcraftAppSchema,
|
|
1588
1653
|
): string[] {
|
|
@@ -1691,6 +1756,7 @@ function getToolcraftControlSectionGroupingErrors(
|
|
|
1691
1756
|
control,
|
|
1692
1757
|
controlId,
|
|
1693
1758
|
sectionLabel,
|
|
1759
|
+
sectionTitle,
|
|
1694
1760
|
});
|
|
1695
1761
|
|
|
1696
1762
|
if (descriptionError) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { CloudArrowUpIcon, XIcon } from "@phosphor-icons/react";
|
|
4
|
+
import { CloudArrowUpIcon, PlusIcon, XIcon } from "@phosphor-icons/react";
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../../lib/utils";
|
|
7
7
|
import { Button, Field } from "../../primitives";
|
|
8
8
|
|
|
9
9
|
export type FileDropPreview = {
|
|
10
10
|
alt?: string;
|
|
11
|
+
id?: string;
|
|
11
12
|
size?: {
|
|
12
13
|
height: number;
|
|
13
14
|
width: number;
|
|
@@ -17,9 +18,13 @@ export type FileDropPreview = {
|
|
|
17
18
|
|
|
18
19
|
export type FileDropControlProps = {
|
|
19
20
|
accept: string;
|
|
21
|
+
multiple?: boolean;
|
|
20
22
|
onClear?: () => void;
|
|
21
23
|
onFileSelect?: (file: File) => void;
|
|
24
|
+
onFilesSelect?: (files: File[]) => void;
|
|
25
|
+
onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
|
|
22
26
|
preview?: FileDropPreview;
|
|
27
|
+
previews?: readonly FileDropPreview[];
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
function isDragLeavingCurrentTarget(event: React.DragEvent<HTMLElement>): boolean {
|
|
@@ -73,20 +78,38 @@ function getPreviewImageStyle(size: FileDropPreview["size"]): React.CSSPropertie
|
|
|
73
78
|
|
|
74
79
|
export function FileDropControl({
|
|
75
80
|
accept,
|
|
81
|
+
multiple = false,
|
|
76
82
|
onClear,
|
|
77
83
|
onFileSelect,
|
|
84
|
+
onFilesSelect,
|
|
85
|
+
onPreviewRemove,
|
|
78
86
|
preview,
|
|
87
|
+
previews,
|
|
79
88
|
}: FileDropControlProps): React.JSX.Element {
|
|
80
89
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
81
90
|
const [dragOver, setDragOver] = React.useState(false);
|
|
82
|
-
const
|
|
91
|
+
const previewItems = previews ?? (preview ? [preview] : []);
|
|
92
|
+
const hasPreview = previewItems.some((item) => Boolean(item.src));
|
|
93
|
+
const shouldRenderPreviewGrid = multiple && previewItems.length > 1;
|
|
83
94
|
|
|
84
|
-
function
|
|
85
|
-
|
|
95
|
+
function handleFiles(fileList: FileList | readonly File[] | undefined): void {
|
|
96
|
+
const files = Array.from(fileList ?? []);
|
|
97
|
+
|
|
98
|
+
if (files.length === 0) {
|
|
86
99
|
return;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
if (multiple) {
|
|
103
|
+
if (onFilesSelect) {
|
|
104
|
+
onFilesSelect(files);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
files.forEach((file) => onFileSelect?.(file));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onFileSelect?.(files[0]);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
function openFileDialog(): void {
|
|
@@ -109,15 +132,24 @@ export function FileDropControl({
|
|
|
109
132
|
aria-hidden="true"
|
|
110
133
|
className="hidden"
|
|
111
134
|
onChange={(event) => {
|
|
112
|
-
|
|
135
|
+
handleFiles(event.currentTarget.files ?? undefined);
|
|
113
136
|
event.currentTarget.value = "";
|
|
114
137
|
}}
|
|
138
|
+
multiple={multiple}
|
|
115
139
|
ref={inputRef}
|
|
116
140
|
tabIndex={-1}
|
|
117
141
|
type="file"
|
|
118
142
|
/>
|
|
119
143
|
<div
|
|
120
|
-
aria-label={
|
|
144
|
+
aria-label={
|
|
145
|
+
hasPreview
|
|
146
|
+
? multiple
|
|
147
|
+
? "Drop image files"
|
|
148
|
+
: "Replace image file"
|
|
149
|
+
: multiple
|
|
150
|
+
? "Browse image files"
|
|
151
|
+
: "Browse image file"
|
|
152
|
+
}
|
|
121
153
|
className={cn(
|
|
122
154
|
"group/file-upload relative flex min-h-16 w-full cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed border-[color:color-mix(in_oklab,var(--border)_18%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_3%,transparent)] text-center shadow-none transition-[background-color,border-color,box-shadow] duration-150 ease-out hover:border-[color:color-mix(in_oklab,var(--border)_35%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)] data-[drag-over=true]:border-[color:color-mix(in_oklab,var(--link)_28%,transparent)] data-[drag-over=true]:bg-[color:color-mix(in_oklab,var(--link)_13%,transparent)] data-[drag-over=true]:shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--background)]",
|
|
123
155
|
hasPreview ? "overflow-hidden p-2" : "px-3 py-3",
|
|
@@ -140,23 +172,74 @@ export function FileDropControl({
|
|
|
140
172
|
onDrop={(event) => {
|
|
141
173
|
event.preventDefault();
|
|
142
174
|
setDragOver(false);
|
|
143
|
-
|
|
175
|
+
handleFiles(event.dataTransfer?.files ?? undefined);
|
|
144
176
|
}}
|
|
145
177
|
onKeyDown={handleDropTargetKeyDown}
|
|
146
178
|
role="button"
|
|
147
179
|
tabIndex={0}
|
|
148
180
|
>
|
|
149
|
-
{
|
|
181
|
+
{shouldRenderPreviewGrid ? (
|
|
182
|
+
<div
|
|
183
|
+
className="grid w-full grid-cols-4 gap-2"
|
|
184
|
+
data-slot="file-upload-preview-grid"
|
|
185
|
+
>
|
|
186
|
+
{previewItems.map((item, index) => (
|
|
187
|
+
<div
|
|
188
|
+
className="relative aspect-square min-w-0 overflow-hidden rounded-[calc(var(--radius-lg)-4px)] bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)]"
|
|
189
|
+
data-slot="file-upload-preview-item"
|
|
190
|
+
key={item.id ?? `${item.src}-${index}`}
|
|
191
|
+
>
|
|
192
|
+
<img
|
|
193
|
+
alt={item.alt ?? ""}
|
|
194
|
+
className="size-full object-cover"
|
|
195
|
+
draggable={false}
|
|
196
|
+
height={item.size?.height}
|
|
197
|
+
src={item.src}
|
|
198
|
+
width={item.size?.width}
|
|
199
|
+
/>
|
|
200
|
+
{onPreviewRemove ? (
|
|
201
|
+
<Button
|
|
202
|
+
aria-label={`Remove ${item.alt ?? "image"}`}
|
|
203
|
+
className="absolute top-1 right-1"
|
|
204
|
+
onClick={(event) => {
|
|
205
|
+
event.stopPropagation();
|
|
206
|
+
onPreviewRemove(item, index);
|
|
207
|
+
}}
|
|
208
|
+
size="icon-sm"
|
|
209
|
+
type="button"
|
|
210
|
+
variant="ghost"
|
|
211
|
+
>
|
|
212
|
+
<XIcon className="drop-shadow-[0_2px_1px_color-mix(in_oklab,var(--background)_80%,transparent)]" />
|
|
213
|
+
</Button>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
<button
|
|
218
|
+
aria-label="Add image files"
|
|
219
|
+
className="flex aspect-square min-w-0 items-center justify-center rounded-[calc(var(--radius-lg)-4px)] border border-[color:color-mix(in_oklab,var(--border)_5%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_4%,transparent)] text-[color:color-mix(in_oklab,var(--foreground)_65%,transparent)] transition-[background-color,border-color,color] duration-150 ease-out hover:border-[color:color-mix(in_oklab,var(--border)_35%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_7%,transparent)] hover:text-[color:var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)]"
|
|
220
|
+
data-slot="file-upload-add-preview"
|
|
221
|
+
onClick={(event) => {
|
|
222
|
+
event.stopPropagation();
|
|
223
|
+
openFileDialog();
|
|
224
|
+
}}
|
|
225
|
+
type="button"
|
|
226
|
+
>
|
|
227
|
+
<PlusIcon className="size-4" weight="regular" />
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
) : hasPreview ? (
|
|
150
231
|
<>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
232
|
+
{previewItems[0] ? (
|
|
233
|
+
<img
|
|
234
|
+
alt={previewItems[0].alt ?? ""}
|
|
235
|
+
className="block h-auto max-h-[196px] max-w-full rounded-[calc(var(--radius-lg)-4px)] object-contain"
|
|
236
|
+
draggable={false}
|
|
237
|
+
height={previewItems[0].size?.height}
|
|
238
|
+
src={previewItems[0].src}
|
|
239
|
+
style={getPreviewImageStyle(previewItems[0].size)}
|
|
240
|
+
width={previewItems[0].size?.width}
|
|
241
|
+
/>
|
|
242
|
+
) : null}
|
|
160
243
|
{onClear ? (
|
|
161
244
|
<Button
|
|
162
245
|
aria-label="Remove image"
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { applySliderValueLabelUnit } from "../slider/slider-value";
|
|
2
|
+
|
|
1
3
|
export function formatRangeSliderValue(
|
|
2
4
|
value: readonly number[],
|
|
3
5
|
unit?: string,
|
|
4
6
|
): string {
|
|
5
|
-
const formatValue = (item: number): string =>
|
|
7
|
+
const formatValue = (item: number): string =>
|
|
8
|
+
applySliderValueLabelUnit(String(Math.round(item)), unit);
|
|
6
9
|
|
|
7
10
|
if (value.length >= 2 && value[0] === value[1]) {
|
|
8
11
|
return formatValue(value[0] ?? 0);
|
|
@@ -17,19 +17,62 @@ export function formatSliderValue(value: number, step: number): string {
|
|
|
17
17
|
return String(rounded);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const compactSliderValueUnits = new Set([
|
|
21
|
+
"%",
|
|
22
|
+
"°",
|
|
23
|
+
"px",
|
|
24
|
+
"em",
|
|
25
|
+
"rem",
|
|
26
|
+
"vw",
|
|
27
|
+
"vh",
|
|
28
|
+
"vmin",
|
|
29
|
+
"vmax",
|
|
30
|
+
"x",
|
|
31
|
+
"s",
|
|
32
|
+
"ms",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function escapeRegExp(value: string): string {
|
|
36
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getSliderValueUnitSeparator(unit: string): "" | " " {
|
|
40
|
+
const normalizedUnit = unit.trim();
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
normalizedUnit === "" ||
|
|
44
|
+
compactSliderValueUnits.has(normalizedUnit) ||
|
|
45
|
+
/^[^\p{Letter}\p{Number}]+$/u.test(normalizedUnit)
|
|
46
|
+
) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return " ";
|
|
51
|
+
}
|
|
52
|
+
|
|
20
53
|
export function applySliderValueLabelUnit(
|
|
21
54
|
valueLabel: string,
|
|
22
55
|
unit?: string,
|
|
23
56
|
): string {
|
|
24
|
-
|
|
57
|
+
const normalizedUnit = unit?.trim();
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!normalizedUnit ||
|
|
61
|
+
typeof parseSliderValueLabel(valueLabel) !== "number"
|
|
62
|
+
) {
|
|
25
63
|
return valueLabel;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
|
-
|
|
29
|
-
|
|
66
|
+
const separator = getSliderValueUnitSeparator(normalizedUnit);
|
|
67
|
+
const unitPattern = new RegExp(
|
|
68
|
+
`(-?\\d+(?:\\.\\d+)?)(?:\\s*${escapeRegExp(normalizedUnit)})?`,
|
|
69
|
+
"g",
|
|
70
|
+
);
|
|
30
71
|
|
|
31
|
-
|
|
32
|
-
|
|
72
|
+
return valueLabel.replaceAll(
|
|
73
|
+
unitPattern,
|
|
74
|
+
(_match, value: string) => `${value}${separator}${normalizedUnit}`,
|
|
75
|
+
);
|
|
33
76
|
}
|
|
34
77
|
|
|
35
78
|
export function formatSliderValueWithUnit(
|
|
@@ -40,6 +40,7 @@ export function EditableSliderValueLabel({
|
|
|
40
40
|
const [editing, setEditing] = useState(false);
|
|
41
41
|
const editorRef = useRef<HTMLSpanElement>(null);
|
|
42
42
|
const valueLabelRef = useRef(valueLabel);
|
|
43
|
+
const isEditableValueLabel = hasEditableNumericValueLabel(valueLabel);
|
|
43
44
|
const valueTextClassName = getEditableValueTextClassName({ layout, textAlign });
|
|
44
45
|
const widestValueLabel = getWidestValueLabel(valueLabel, maxValueLabel);
|
|
45
46
|
|
|
@@ -61,7 +62,7 @@ export function EditableSliderValueLabel({
|
|
|
61
62
|
}
|
|
62
63
|
}, [editing]);
|
|
63
64
|
|
|
64
|
-
if (disabled || !onCommit) {
|
|
65
|
+
if (disabled || !onCommit || !isEditableValueLabel) {
|
|
65
66
|
const valueTextToneClassName = disabled
|
|
66
67
|
? "text-[color:color-mix(in_oklab,var(--foreground)_60%,transparent)] opacity-60"
|
|
67
68
|
: "text-[color:var(--muted-foreground)]";
|
|
@@ -323,6 +324,10 @@ function getWidestValueLabel(valueLabel: string, maxValueLabel?: string): string
|
|
|
323
324
|
return maxValueLabel;
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
function hasEditableNumericValueLabel(valueLabel: string): boolean {
|
|
328
|
+
return /-?\d+(?:\.\d+)?/.test(valueLabel);
|
|
329
|
+
}
|
|
330
|
+
|
|
326
331
|
function selectEditableText(node: HTMLElement): void {
|
|
327
332
|
const selection = window.getSelection();
|
|
328
333
|
|