@weasel-js/ui 1.7.2 → 1.7.3
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/dist/chunks/{LayerList-Bxj1N6bj.js → LayerList-BqeWFuGc.js} +3 -3
- package/dist/chunks/{LayerList-Bxj1N6bj.js.map → LayerList-BqeWFuGc.js.map} +1 -1
- package/dist/chunks/{Prefs-C4MZ28SX.js → Prefs-BnVEyfWO.js} +2 -2
- package/dist/chunks/{Prefs-C4MZ28SX.js.map → Prefs-BnVEyfWO.js.map} +1 -1
- package/dist/chunks/{Properties-DseN56WI.js → Properties-BoG2MMHR.js} +2 -2
- package/dist/chunks/{Properties-DseN56WI.js.map → Properties-BoG2MMHR.js.map} +1 -1
- package/dist/chunks/{PropertyField-BbNqK8xr.js → PropertyField-PMrqBA0n.js} +61 -61
- package/dist/chunks/{PropertyField-BbNqK8xr.js.map → PropertyField-PMrqBA0n.js.map} +1 -1
- package/dist/chunks/{SelectionPanel-C3WzyaZV.js → SelectionPanel-COZ6z9_P.js} +2 -2
- package/dist/chunks/{SelectionPanel-C3WzyaZV.js.map → SelectionPanel-COZ6z9_P.js.map} +1 -1
- package/dist/chunks/{ToolOptionsBar-CZTYPoC2.js → ToolOptionsBar-BJKwnemU.js} +2 -2
- package/dist/chunks/{ToolOptionsBar-CZTYPoC2.js.map → ToolOptionsBar-BJKwnemU.js.map} +1 -1
- package/dist/chunks/{model-qsAFK7_0.js → model-B6zuQg6b.js} +2 -2
- package/dist/chunks/{model-qsAFK7_0.js.map → model-B6zuQg6b.js.map} +1 -1
- package/dist/components/LayerList/index.js +1 -1
- package/dist/components/Prefs/index.js +1 -1
- package/dist/components/Properties/index.js +2 -2
- package/dist/components/SelectionPanel/index.js +2 -2
- package/dist/components/ToolOptionsBar/index.js +1 -1
- package/dist/index.js +7 -7
- package/dist/style.css +1 -1
- package/package.json +9 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as e, f as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./model-
|
|
1
|
+
import { d as e, f as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./model-B6zuQg6b.js";
|
|
2
2
|
import { asNodeId as u } from "@weasel-js/core";
|
|
3
3
|
import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
4
4
|
import { Fragment as m, useSyncExternalStore as h } from "react";
|
|
@@ -104,4 +104,4 @@ function v(e, t, n, r, a, o) {
|
|
|
104
104
|
//#endregion
|
|
105
105
|
export { _ as t };
|
|
106
106
|
|
|
107
|
-
//# sourceMappingURL=SelectionPanel-
|
|
107
|
+
//# sourceMappingURL=SelectionPanel-COZ6z9_P.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectionPanel-C3WzyaZV.js","names":["s","_Fragment","Fragment","renderCells","SelectionPanel_module_default","effectiveSections","setAtPath","aggregateValue","kindBreakdown","classifyKind","MIXED","renderBuiltin","Fragment$1","defaultKindLabel","kind","SelectionPanel","props","scene","nodes","selection","asNodeId","id","n","kinds","routing","selectionKey","sections","properties","className","emptyState","commit","head","rest","leaf","ids","node","value","next","nodeValueAt","kindLabel","Fragment","rows","section","row","leafCell","panelLeaf","renderers","controls","content","key","aggregated","path","mixed","ctx","v","fn","at","p","custom","ariaLabel"],"sources":["../../src/components/SelectionPanel/SelectionPanel.tsx"],"sourcesContent":["import { Fragment, useSyncExternalStore, type ReactNode } from 'react';\nimport {\n asNodeId,\n type NodePropertiesEntry,\n type NodeRoutingEntry,\n type Scene,\n type SelectionApi,\n} from '@weasel-js/core';\nimport {\n renderBuiltin,\n renderCells,\n type LeafCell,\n type PropertyRenderContext,\n type PropertyRenderer,\n} from './renderLeaf';\nimport {\n MIXED,\n aggregateValue,\n classifyKind,\n effectiveSections,\n kindBreakdown,\n nodeValueAt,\n setAtPath,\n type AnyNode,\n type PanelLeaf,\n} from './model';\nimport s from './SelectionPanel.module.css';\n\nexport type { PropertyRenderContext, PropertyRenderer } from './renderLeaf';\n\n/** Props for {@link SelectionPanel}. */\nexport interface SelectionPanelProps<TData, TLayer extends string, TPose> {\n /** The scene handle (`useScene`). The panel subscribes itself, so it\n * re-renders on scene mutations regardless of parent renders. */\n scene: Scene<TData, TLayer, TPose>;\n /** Selection handle (`useSelection`). Only `current` is read. */\n selection: Pick<SelectionApi, 'current'>;\n /** Properties-trait entries, e.g. core's `defaultNodeProperties` /\n * `inferredNodeProperties` (+ consumer extras). Memoize or hoist. */\n properties: readonly NodePropertiesEntry[];\n /** Routing-trait classifiers used to derive each node's kind — pass\n * the same list the canvas uses. Memoize or hoist. */\n routing: readonly NodeRoutingEntry[];\n /** Control overrides / app-defined kinds (PrefsForm-style). Keys are\n * leaf paths (`data.fill`, checked first) or leaf kinds (`color`).\n * A renderer returning `null` collapses its leaf (and the row, when\n * every leaf in it collapses). */\n renderers?: Record<string, PropertyRenderer>;\n /** Kind → header label. Default: capitalized kind name. */\n kindLabel?: (kind: string) => string;\n /** Rendered when the selection is empty. */\n emptyState?: ReactNode;\n className?: string;\n}\n\nconst defaultKindLabel = (kind: string): string =>\n kind.length === 0 ? kind : kind[0].toUpperCase() + kind.slice(1);\n\n/**\n * Pre-baked selection properties panel. Shows the selected nodes' kind\n * and the properties-trait schema for that kind; multi-selections show\n * the intersection of the kinds' schemas with per-field Mixed state.\n * Edits commit as one labeled `scene.batch` fan-out per gesture.\n */\nexport function SelectionPanel<TData, TLayer extends string, TPose>(\n props: SelectionPanelProps<TData, TLayer, TPose>,\n) {\n const {\n scene,\n selection,\n properties,\n routing,\n renderers,\n kindLabel = defaultKindLabel,\n emptyState = null,\n className,\n } = props;\n\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const nodes = selection.current\n .map((id) => scene.get(asNodeId(id)))\n .filter((n): n is NonNullable<typeof n> => n != null) as readonly AnyNode[];\n\n const kinds = nodes.map((n) => classifyKind(n, routing));\n const selectionKey = nodes.map((n) => n.id).join(',');\n const sections = effectiveSections(kinds, properties);\n\n if (nodes.length === 0) {\n return <div className={[s.root, className].filter(Boolean).join(' ')}>{emptyState}</div>;\n }\n\n /** Writes, into every selected node, what `next` derives from the value\n * that node holds at the leaf's path. */\n const commit = (leaf: PanelLeaf, next: (prev: unknown) => unknown): void => {\n const [head, ...rest] = leaf.path.split('.');\n if (rest.length === 0) return;\n const ids = selection.current.map(asNodeId);\n scene.batch(`Edit ${leaf.leaf.name}`, () => {\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const value = next(nodeValueAt(node as AnyNode, leaf.path));\n // `setAtPath` spreads plain objects/arrays down the schema path;\n // consumer `pose`/`data` are assumed plain-object-shaped along\n // that path (a class instance's prototype would be dropped by\n // the spread) — the `as TPose`/`as TData` casts below rely on it.\n if (head === 'pose') {\n scene.setPose(id, setAtPath(node.pose as object, rest, value) as TPose);\n } else if (head === 'data') {\n scene.update(id, { data: setAtPath(node.data as object, rest, value) as TData });\n }\n }\n });\n };\n\n return (\n <div className={[s.root, className].filter(Boolean).join(' ')}>\n <header className={s.header}>\n {nodes.length === 1 ? (\n <span className={s.kind}>{kindLabel(kinds[0])}</span>\n ) : (\n <>\n <span className={s.kind}>{nodes.length} selected</span>\n <span className={s.breakdown}>{kindBreakdown(kinds)}</span>\n </>\n )}\n </header>\n {sections.map((section) => {\n // Render controls before emitting row chrome so a null-rendering\n // leaf (custom renderer opting out) collapses its cell — and the\n // whole row / section when nothing survives. PrefsForm precedent.\n const rows = section.rows\n .map((row) => {\n const controls = renderCells(\n row.leaves.map((panelLeaf) =>\n leafCell(\n panelLeaf,\n row.leaves.length > 1\n ? `${row.label} ${panelLeaf.leaf.name}`\n : panelLeaf.leaf.name,\n nodes,\n renderers,\n commit,\n selectionKey,\n ),\n ),\n renderers,\n );\n return { row, controls };\n })\n .filter(({ controls }) => controls.length > 0);\n if (rows.length === 0) return null;\n return (\n <section key={section.key} className={s.section}>\n {section.name !== '' && <h3 className={s.sectionTitle}>{section.name}</h3>}\n {rows.map(({ row, controls }) => (\n // A `block` leaf brings its own chrome — it spans the section\n // instead of sitting in a labeled row's control cell. Under a\n // headless section nothing else names it, so it heads itself;\n // an object leaf already renders its own heading.\n row.leaves.length === 1 && row.leaves[0].leaf.block ? (\n <div key={row.leaves[0].path}>\n {section.name === '' && row.leaves[0].leaf.kind !== 'object' && (\n <h4 className={s.sectionTitle} title={row.leaves[0].leaf.description}>\n {row.leaves[0].leaf.name}\n </h4>\n )}\n {controls[0].content}\n </div>\n ) : (\n <div key={row.leaves[0].path} className={s.row}>\n <span className={s.rowLabel} title={row.leaves[0].leaf.description}>\n {row.label}\n </span>\n <span className={s.rowControls}>\n {controls.map(({ key, content }) => (\n <Fragment key={key}>{content}</Fragment>\n ))}\n </span>\n </div>\n )\n ))}\n </section>\n );\n })}\n </div>\n );\n}\n\n\nfunction leafCell(\n panelLeaf: PanelLeaf,\n ariaLabel: string,\n nodes: readonly AnyNode[],\n renderers: Record<string, PropertyRenderer> | undefined,\n commit: (leaf: PanelLeaf, next: (prev: unknown) => unknown) => void,\n selectionKey: string,\n): LeafCell {\n const { path, leaf } = panelLeaf;\n const aggregated = aggregateValue(nodes, path);\n const mixed = aggregated === MIXED;\n const value = mixed ? undefined : aggregated;\n\n const ctx: PropertyRenderContext = {\n path,\n pref: leaf,\n value,\n mixed,\n unset: !mixed && aggregated === undefined,\n each: nodes.map((n) => nodeValueAt(n, path)),\n setValue: (v) => commit(panelLeaf, () => v),\n update: (fn) => commit(panelLeaf, fn),\n valueAt: (p) => {\n const at = aggregateValue(nodes, p);\n return at === MIXED ? { value: undefined, mixed: true } : { value: at, mixed: false };\n },\n selectionKey,\n };\n\n return {\n key: path,\n leaf,\n ctx,\n ariaLabel,\n render: () => {\n const custom = renderers?.[path] ?? renderers?.[leaf.kind];\n if (custom) return custom(ctx);\n return renderBuiltin(ctx, ariaLabel, renderers, selectionKey);\n },\n };\n}\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SelectionPanel-COZ6z9_P.js","names":["s","_Fragment","Fragment","renderCells","SelectionPanel_module_default","effectiveSections","setAtPath","aggregateValue","kindBreakdown","classifyKind","MIXED","renderBuiltin","Fragment$1","defaultKindLabel","kind","SelectionPanel","props","scene","nodes","selection","asNodeId","id","n","kinds","routing","selectionKey","sections","properties","className","emptyState","commit","head","rest","leaf","ids","node","value","next","nodeValueAt","kindLabel","Fragment","rows","section","row","leafCell","panelLeaf","renderers","controls","content","key","aggregated","path","mixed","ctx","v","fn","at","p","custom","ariaLabel"],"sources":["../../src/components/SelectionPanel/SelectionPanel.tsx"],"sourcesContent":["import { Fragment, useSyncExternalStore, type ReactNode } from 'react';\nimport {\n asNodeId,\n type NodePropertiesEntry,\n type NodeRoutingEntry,\n type Scene,\n type SelectionApi,\n} from '@weasel-js/core';\nimport {\n renderBuiltin,\n renderCells,\n type LeafCell,\n type PropertyRenderContext,\n type PropertyRenderer,\n} from './renderLeaf';\nimport {\n MIXED,\n aggregateValue,\n classifyKind,\n effectiveSections,\n kindBreakdown,\n nodeValueAt,\n setAtPath,\n type AnyNode,\n type PanelLeaf,\n} from './model';\nimport s from './SelectionPanel.module.css';\n\nexport type { PropertyRenderContext, PropertyRenderer } from './renderLeaf';\n\n/** Props for {@link SelectionPanel}. */\nexport interface SelectionPanelProps<TData, TLayer extends string, TPose> {\n /** The scene handle (`useScene`). The panel subscribes itself, so it\n * re-renders on scene mutations regardless of parent renders. */\n scene: Scene<TData, TLayer, TPose>;\n /** Selection handle (`useSelection`). Only `current` is read. */\n selection: Pick<SelectionApi, 'current'>;\n /** Properties-trait entries, e.g. core's `defaultNodeProperties` /\n * `inferredNodeProperties` (+ consumer extras). Memoize or hoist. */\n properties: readonly NodePropertiesEntry[];\n /** Routing-trait classifiers used to derive each node's kind — pass\n * the same list the canvas uses. Memoize or hoist. */\n routing: readonly NodeRoutingEntry[];\n /** Control overrides / app-defined kinds (PrefsForm-style). Keys are\n * leaf paths (`data.fill`, checked first) or leaf kinds (`color`).\n * A renderer returning `null` collapses its leaf (and the row, when\n * every leaf in it collapses). */\n renderers?: Record<string, PropertyRenderer>;\n /** Kind → header label. Default: capitalized kind name. */\n kindLabel?: (kind: string) => string;\n /** Rendered when the selection is empty. */\n emptyState?: ReactNode;\n className?: string;\n}\n\nconst defaultKindLabel = (kind: string): string =>\n kind.length === 0 ? kind : kind[0].toUpperCase() + kind.slice(1);\n\n/**\n * Pre-baked selection properties panel. Shows the selected nodes' kind\n * and the properties-trait schema for that kind; multi-selections show\n * the intersection of the kinds' schemas with per-field Mixed state.\n * Edits commit as one labeled `scene.batch` fan-out per gesture.\n */\nexport function SelectionPanel<TData, TLayer extends string, TPose>(\n props: SelectionPanelProps<TData, TLayer, TPose>,\n) {\n const {\n scene,\n selection,\n properties,\n routing,\n renderers,\n kindLabel = defaultKindLabel,\n emptyState = null,\n className,\n } = props;\n\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const nodes = selection.current\n .map((id) => scene.get(asNodeId(id)))\n .filter((n): n is NonNullable<typeof n> => n != null) as readonly AnyNode[];\n\n const kinds = nodes.map((n) => classifyKind(n, routing));\n const selectionKey = nodes.map((n) => n.id).join(',');\n const sections = effectiveSections(kinds, properties);\n\n if (nodes.length === 0) {\n return <div className={[s.root, className].filter(Boolean).join(' ')}>{emptyState}</div>;\n }\n\n /** Writes, into every selected node, what `next` derives from the value\n * that node holds at the leaf's path. */\n const commit = (leaf: PanelLeaf, next: (prev: unknown) => unknown): void => {\n const [head, ...rest] = leaf.path.split('.');\n if (rest.length === 0) return;\n const ids = selection.current.map(asNodeId);\n scene.batch(`Edit ${leaf.leaf.name}`, () => {\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const value = next(nodeValueAt(node as AnyNode, leaf.path));\n // `setAtPath` spreads plain objects/arrays down the schema path;\n // consumer `pose`/`data` are assumed plain-object-shaped along\n // that path (a class instance's prototype would be dropped by\n // the spread) — the `as TPose`/`as TData` casts below rely on it.\n if (head === 'pose') {\n scene.setPose(id, setAtPath(node.pose as object, rest, value) as TPose);\n } else if (head === 'data') {\n scene.update(id, { data: setAtPath(node.data as object, rest, value) as TData });\n }\n }\n });\n };\n\n return (\n <div className={[s.root, className].filter(Boolean).join(' ')}>\n <header className={s.header}>\n {nodes.length === 1 ? (\n <span className={s.kind}>{kindLabel(kinds[0])}</span>\n ) : (\n <>\n <span className={s.kind}>{nodes.length} selected</span>\n <span className={s.breakdown}>{kindBreakdown(kinds)}</span>\n </>\n )}\n </header>\n {sections.map((section) => {\n // Render controls before emitting row chrome so a null-rendering\n // leaf (custom renderer opting out) collapses its cell — and the\n // whole row / section when nothing survives. PrefsForm precedent.\n const rows = section.rows\n .map((row) => {\n const controls = renderCells(\n row.leaves.map((panelLeaf) =>\n leafCell(\n panelLeaf,\n row.leaves.length > 1\n ? `${row.label} ${panelLeaf.leaf.name}`\n : panelLeaf.leaf.name,\n nodes,\n renderers,\n commit,\n selectionKey,\n ),\n ),\n renderers,\n );\n return { row, controls };\n })\n .filter(({ controls }) => controls.length > 0);\n if (rows.length === 0) return null;\n return (\n <section key={section.key} className={s.section}>\n {section.name !== '' && <h3 className={s.sectionTitle}>{section.name}</h3>}\n {rows.map(({ row, controls }) => (\n // A `block` leaf brings its own chrome — it spans the section\n // instead of sitting in a labeled row's control cell. Under a\n // headless section nothing else names it, so it heads itself;\n // an object leaf already renders its own heading.\n row.leaves.length === 1 && row.leaves[0].leaf.block ? (\n <div key={row.leaves[0].path}>\n {section.name === '' && row.leaves[0].leaf.kind !== 'object' && (\n <h4 className={s.sectionTitle} title={row.leaves[0].leaf.description}>\n {row.leaves[0].leaf.name}\n </h4>\n )}\n {controls[0].content}\n </div>\n ) : (\n <div key={row.leaves[0].path} className={s.row}>\n <span className={s.rowLabel} title={row.leaves[0].leaf.description}>\n {row.label}\n </span>\n <span className={s.rowControls}>\n {controls.map(({ key, content }) => (\n <Fragment key={key}>{content}</Fragment>\n ))}\n </span>\n </div>\n )\n ))}\n </section>\n );\n })}\n </div>\n );\n}\n\n\nfunction leafCell(\n panelLeaf: PanelLeaf,\n ariaLabel: string,\n nodes: readonly AnyNode[],\n renderers: Record<string, PropertyRenderer> | undefined,\n commit: (leaf: PanelLeaf, next: (prev: unknown) => unknown) => void,\n selectionKey: string,\n): LeafCell {\n const { path, leaf } = panelLeaf;\n const aggregated = aggregateValue(nodes, path);\n const mixed = aggregated === MIXED;\n const value = mixed ? undefined : aggregated;\n\n const ctx: PropertyRenderContext = {\n path,\n pref: leaf,\n value,\n mixed,\n unset: !mixed && aggregated === undefined,\n each: nodes.map((n) => nodeValueAt(n, path)),\n setValue: (v) => commit(panelLeaf, () => v),\n update: (fn) => commit(panelLeaf, fn),\n valueAt: (p) => {\n const at = aggregateValue(nodes, p);\n return at === MIXED ? { value: undefined, mixed: true } : { value: at, mixed: false };\n },\n selectionKey,\n };\n\n return {\n key: path,\n leaf,\n ctx,\n ariaLabel,\n render: () => {\n const custom = renderers?.[path] ?? renderers?.[leaf.kind];\n if (custom) return custom(ctx);\n return renderBuiltin(ctx, ariaLabel, renderers, selectionKey);\n },\n };\n}\n\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, u as r } from "./model-
|
|
1
|
+
import { a as e, c as t, d as n, u as r } from "./model-B6zuQg6b.js";
|
|
2
2
|
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
3
3
|
var s = {
|
|
4
4
|
root: "_root_1h7p0_13",
|
|
@@ -85,4 +85,4 @@ function f(e, t, n, i, a, o) {
|
|
|
85
85
|
//#endregion
|
|
86
86
|
export { c as t };
|
|
87
87
|
|
|
88
|
-
//# sourceMappingURL=ToolOptionsBar-
|
|
88
|
+
//# sourceMappingURL=ToolOptionsBar-BJKwnemU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolOptionsBar-CZTYPoC2.js","names":["s","_Fragment","isGroup","pairRows","renderCells","renderBuiltin","ToolOptionsBar_module_default","ToolOptionsBar","props","cls","className","label","schema","barRows","BarRow","row","children","leaves","collect","key","child","group","path","prefix","cells","barCell","panelLeaf","values","mixed","renderers","onChange","Fragment","needsLabel","cell","leaf","isMixed","ctx","v","fn","p","custom","ariaLabel"],"sources":["../../src/components/ToolOptionsBar/ToolOptionsBar.module.css","../../src/components/ToolOptionsBar/ToolOptionsBar.tsx"],"sourcesContent":["/* Horizontal strip reserved above the workspace for contextual tool\n options. Fixed height so mounting/unmounting content never resizes\n the workspace beneath it — that's the whole reason this row exists\n as permanent chrome rather than an on-demand overlay.\n\n Overflow: when controls exceed the available width, `.controls`\n scrolls horizontally rather than wrapping (which would grow the row\n and defeat the fixed-height guarantee above) or clipping (which\n would hide controls with no way to reach them). A flyout/long-press\n affordance was considered and rejected — grouped, directly visible\n layouts are preferred in this repo. */\n\n.root {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-4);\n height: var(--wzl-tb-height);\n padding: 0 var(--wzl-space-4);\n background: var(--wzl-surface);\n border-bottom: 1px solid var(--wzl-border);\n color: var(--wzl-fg);\n overflow: hidden;\n}\n\n.label {\n flex: 0 0 auto;\n font-size: var(--wzl-font-size-sm);\n font-weight: var(--wzl-font-weight-bold);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--wzl-fg-muted);\n white-space: nowrap;\n}\n\n.controls {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-3);\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n}\n\n/* Firefox gets a thin scrollbar for free via `scrollbar-width` above.\n * Chromium/WebKit need it spelled out — on a classic (non-overlay)\n * scrollbar theme (default on Windows Chrome/Edge), an unstyled\n * horizontal scrollbar is ~15-17px tall and renders *inside* this box,\n * clipping/off-centering the row's controls. That would reintroduce the\n * exact height instability the scroll-over-wrap overflow choice above\n * exists to avoid — just at the row's inner edge instead of its outer\n * one. Pin it thin and out of the way instead. */\n.controls::-webkit-scrollbar {\n height: 4px;\n}\n.controls::-webkit-scrollbar-track {\n background: transparent;\n}\n.controls::-webkit-scrollbar-thumb {\n background: var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n}\n\n/* A schema-drawn control sits on the line as-is; the cell exists only to keep\n it from being squeezed by the row's flex context. */\n.cell {\n display: flex;\n align-items: center;\n flex: none;\n}\n\n.cellLabel {\n flex: none;\n margin-right: var(--wzl-space-2);\n font-size: var(--wzl-font-size-sm);\n color: var(--wzl-fg-muted);\n white-space: nowrap;\n}\n\n/* Fields are read beside their label on one line, so they carry only the\n digits they need rather than the panel's column width. */\n.controls {\n --wzl-number-field-width: 5ch;\n}\n\n/* The strip decides how much room a control gets, because its budget is one\n line rather than a column: a field carries the digits it needs and an\n opacity track stops short of pushing its neighbours off the end. Both are\n the panel's widths otherwise, which are sized for a two-column grid. */\n.cell input[type='text'] {\n inline-size: var(--wzl-number-field-width, 5ch);\n}\n\n.cell input[type='range'] {\n inline-size: 64px;\n flex: none;\n}\n","import type { ReactNode } from 'react';\nimport type { ToolPrefGroup, ToolPrefLeaf } from '@weasel-js/core';\nimport {\n renderBuiltin,\n renderCells,\n type LeafCell,\n type PropertyRenderContext,\n type PropertyRenderer,\n} from '../SelectionPanel/renderLeaf';\nimport { isGroup, pairRows, type PanelLeaf, type PanelRow } from '../SelectionPanel/model';\nimport s from './ToolOptionsBar.module.css';\n\n/** Props for {@link ToolOptionsBar}. */\nexport interface ToolOptionsBarProps {\n /**\n * Context label for the row — e.g. the active tool's name. Rendered\n * before the children and doubles as the toolbar's `aria-label`. Omit\n * it and the row still renders (it's a permanently reserved slot), just\n * without an accessible name.\n *\n * Using `label` for both the visible text and `aria-label` means a\n * screen reader announces it twice when landing on the toolbar (\"Text,\n * toolbar\", then \"Text\" again when browsing into the visible span).\n * `aria-labelledby` would avoid the echo at the cost of id plumbing and\n * an awkward no-`id`-to-point-at case when `label` is omitted. The\n * double-announcement is common enough for labeled landmark regions\n * that it's left as-is — considered, not missed.\n */\n label?: string;\n /**\n * The active tool's options. Leaves draw through the same leaf → control\n * mapping `SelectionPanel` uses, so a kind gets its control decided in one\n * place; groups organize without drawing, since the bar is one line.\n */\n schema?: ToolPrefGroup;\n /** Each leaf's value, keyed by dotted path. A path with no entry is unset. */\n values?: Readonly<Record<string, unknown>>;\n /** A leaf edit, as the dotted path it was declared at and its new value. */\n onChange?: (path: string, value: unknown) => void;\n /** Paths whose sources disagree — drawn as the control's mixed state. */\n mixed?: ReadonlySet<string>;\n /** Control overrides, by dotted path or by leaf kind, as the panel takes. */\n renderers?: Record<string, PropertyRenderer>;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * Horizontal strip reserved above the workspace for contextual tool options.\n * Given a `schema` it draws the active tool's options itself; the children\n * slot stays open for a tenant that is not a tool's options at all, such as\n * loupe controls.\n *\n * The row always renders, even empty, because the app reserves its height\n * permanently rather than mounting it on demand — mounting mid-edit would\n * resize the workspace under a canvas that's sized to the page.\n *\n * `role=\"toolbar\"` without roving tabindex: the ARIA authoring practices\n * expect arrow-key roving focus across a toolbar's items, but a bar of\n * compound controls (`NumberField`, `Select`, `ColorField`) must leave the\n * arrow keys to them — they edit their own value with them. That is true of\n * schema-drawn controls as much as of arbitrary children, so the bar keeps\n * plain DOM tab order and `useRovingTabIndex` documents this as its opt-out.\n */\nexport function ToolOptionsBar(props: ToolOptionsBarProps) {\n const { label, schema, values, onChange, mixed, renderers, children, className } = props;\n const cls = [s.root, className].filter(Boolean).join(' ');\n return (\n <div className={cls} role=\"toolbar\" aria-label={label}>\n {label !== undefined && <span className={s.label}>{label}</span>}\n <div className={s.controls}>\n {schema !== undefined &&\n barRows(schema).map((row) => (\n <BarRow\n key={row.leaves[0]!.path}\n row={row}\n values={values}\n mixed={mixed}\n renderers={renderers}\n onChange={onChange}\n />\n ))}\n {children}\n </div>\n </div>\n );\n}\n\n/**\n * The schema's visible leaves as rows, addressed by dotted path. Groups are\n * organizational here — a bar is one line, so a nested group contributes its\n * leaves and no chrome — but its path segment still names them, so a leaf\n * reads back at the path the tool declared it at.\n */\nfunction barRows(schema: ToolPrefGroup): PanelRow[] {\n const leaves: PanelLeaf[] = [];\n const collect = (group: ToolPrefGroup, prefix: string): void => {\n for (const [key, child] of Object.entries(group.children)) {\n const path = prefix === '' ? key : `${prefix}.${key}`;\n if (isGroup(child)) collect(child, path);\n else if (child.hidden !== true) leaves.push({ path, leaf: child });\n }\n };\n collect(schema, '');\n return pairRows(leaves);\n}\n\n/** One row of the bar: a single control, or a run of flags as one bar. */\nfunction BarRow({\n row,\n values,\n mixed,\n renderers,\n onChange,\n}: {\n row: PanelRow;\n values: Readonly<Record<string, unknown>> | undefined;\n mixed: ReadonlySet<string> | undefined;\n renderers: Record<string, PropertyRenderer> | undefined;\n onChange: ((path: string, value: unknown) => void) | undefined;\n}) {\n // The bar draws no row label, so a control carries its own name rather than\n // the panel's qualified \"<pair> <leaf>\" — the run's bar carries the pair.\n const cells = row.leaves.map((panelLeaf) =>\n barCell(panelLeaf, panelLeaf.leaf.name, values, mixed, renderers, onChange),\n );\n return (\n <>\n {renderCells(cells, renderers).map((cell) => (\n <span key={cell.key} className={s.cell}>\n {needsLabel(cell.leaf) && (\n <span className={s.cellLabel} aria-hidden=\"true\">\n {cell.leaf.short ?? cell.leaf.name}\n </span>\n )}\n {cell.content}\n </span>\n ))}\n </>\n );\n}\n\n/**\n * Whether a control needs telling from its neighbours in words.\n *\n * Only a bare value box does: a number or a string is a box of characters,\n * and two of them side by side are indistinguishable. Everything else shows\n * what it is — a swatch is a color, a font picker names the family, a flag\n * run and a segmented control label their own segments — so a word in front\n * would repeat what the control already says. `name` stays the accessible\n * name either way, so this costs a screen reader nothing.\n */\nfunction needsLabel(leaf: ToolPrefLeaf): boolean {\n return leaf.kind === 'number' || leaf.kind === 'string';\n}\n\nfunction barCell(\n panelLeaf: PanelLeaf,\n ariaLabel: string,\n values: Readonly<Record<string, unknown>> | undefined,\n mixed: ReadonlySet<string> | undefined,\n renderers: Record<string, PropertyRenderer> | undefined,\n onChange: ((path: string, value: unknown) => void) | undefined,\n): LeafCell {\n const { path, leaf } = panelLeaf;\n const isMixed = mixed?.has(path) === true;\n const ctx: PropertyRenderContext = {\n path,\n pref: leaf,\n value: isMixed ? undefined : values?.[path],\n mixed: isMixed,\n unset: !isMixed && values?.[path] === undefined,\n each: isMixed ? [] : [values?.[path]],\n setValue: (v) => onChange?.(path, v),\n // One set of values, so there is one prior value to derive from.\n update: (fn) => onChange?.(path, fn(isMixed ? undefined : values?.[path])),\n valueAt: (p) => ({ value: values?.[p], mixed: mixed?.has(p) === true }),\n };\n return {\n key: path,\n leaf: leaf as ToolPrefLeaf,\n ctx,\n ariaLabel,\n render: () => {\n const custom = renderers?.[path] ?? renderers?.[leaf.kind];\n return custom ? custom(ctx) : renderBuiltin(ctx, ariaLabel, renderers, path);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;ACgEA,SAAgBO,EAAe,GAA4B;CACzD,IAAM,EAAE,UAAO,WAAQ,WAAQ,aAAU,UAAO,cAAW,aAAU,iBAAcC,GAC7EC,IAAM,CAACT,EAAE,MAAMU,CAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CACxD,OACE,kBAAC,OAAD;EAAK,WAAWD;EAAK,MAAK;EAAU,cAAYE;EAAhD,UAAA,CACGA,MAAU,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAWX,EAAE;GAAQ,UAAAW;EAAY,CAAA,GAC/D,kBAAC,OAAD;GAAK,WAAWX,EAAE;GAAlB,UAAA,CACGY,MAAW,KAAA,KACVC,EAAQD,CAAM,CAAC,CAAC,KAAK,MACnB,kBAACE,GAAD;IAEO;IACG;IACD;IACI;IACD;GACX,GANMC,EAAI,OAAO,EAAE,CAAE,IAMrB,CACF,GACFC,CACE;EACF,CAAA,CAAA;;AAET;AAQA,SAASH,EAAQ,GAAmC;CAClD,IAAMI,IAAsB,CAAC,GACvBC,KAAW,GAAsB,MAAyB;EAC9D,KAAK,IAAM,CAACC,GAAKC,MAAU,OAAO,QAAQC,EAAM,QAAQ,GAAG;GACzD,IAAMC,IAAOC,MAAW,KAAKJ,IAAM,GAAGI,EAAO,GAAGJ;GAChD,AAAIjB,EAAQkB,CAAK,IAAGF,EAAQE,GAAOE,CAAI,IAC9BF,EAAM,WAAW,MAAMH,EAAO,KAAK;IAAE;IAAM,MAAMG;GAAM,CAAC;EACnE;CACF;CAEA,OADAF,EAAQN,GAAQ,EAAE,GACXT,EAASc,CAAM;AACxB;AAGA,SAASH,EAAO,EACd,QACA,WACA,UACA,cACA,eAOC;CAGD,IAAMU,IAAQT,EAAI,OAAO,KAAK,MAC5BU,EAAQC,GAAWA,EAAU,KAAK,MAAMC,GAAQC,GAAOC,GAAWC,CAAQ,CAC5E;CACA,OACE,kBAAA7B,GAAA,EAAA,UACGG,EAAYoB,GAAOK,CAAS,CAAC,CAAC,KAAK,MAClC,kBAAC,QAAD;EAAqB,WAAW7B,EAAE;EAAlC,UAAA,CACGgC,EAAWC,EAAK,IAAI,KACnB,kBAAC,QAAD;GAAM,WAAWjC,EAAE;GAAW,eAAY;GACvC,UAAAiC,EAAK,KAAK,SAASA,EAAK,KAAK;EAC1B,CAAA,GAEPA,EAAK,OACF;CAPK,GAAAA,EAAK,GAOV,CACP,EACD,CAAA;AAEN;AAYA,SAASD,EAAW,GAA6B;CAC/C,OAAOE,EAAK,SAAS,YAAYA,EAAK,SAAS;AACjD;AAEA,SAAST,EACP,GACA,GACA,GACA,GACA,GACA,GACU;CACV,IAAM,EAAE,SAAM,YAASC,GACjBS,IAAUP,GAAO,IAAIN,CAAI,MAAM,IAC/Bc,IAA6B;EACjC;EACA,MAAMF;EACN,OAAOC,IAAU,KAAA,IAAYR,IAASL;EACtC,OAAOa;EACP,OAAO,CAACA,KAAWR,IAASL,OAAU,KAAA;EACtC,MAAMa,IAAU,CAAC,IAAI,CAACR,IAASL,EAAK;EACpC,WAAW,MAAMQ,IAAWR,GAAMe,CAAC;EAEnC,SAAS,MAAOP,IAAWR,GAAMgB,EAAGH,IAAU,KAAA,IAAYR,IAASL,EAAK,CAAC;EACzE,UAAU,OAAO;GAAE,OAAOK,IAASY;GAAI,OAAOX,GAAO,IAAIW,CAAC,MAAM;EAAK;CACvE;CACA,OAAO;EACL,KAAKjB;EACC;EACN;EACA;EACA,cAAc;GACZ,IAAMkB,IAASX,IAAYP,MAASO,IAAYK,EAAK;GACrD,OAAOM,IAASA,EAAOJ,CAAG,IAAI/B,EAAc+B,GAAKK,GAAWZ,GAAWP,CAAI;EAC7E;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"ToolOptionsBar-BJKwnemU.js","names":["s","_Fragment","isGroup","pairRows","renderCells","renderBuiltin","ToolOptionsBar_module_default","ToolOptionsBar","props","cls","className","label","schema","barRows","BarRow","row","children","leaves","collect","key","child","group","path","prefix","cells","barCell","panelLeaf","values","mixed","renderers","onChange","Fragment","needsLabel","cell","leaf","isMixed","ctx","v","fn","p","custom","ariaLabel"],"sources":["../../src/components/ToolOptionsBar/ToolOptionsBar.module.css","../../src/components/ToolOptionsBar/ToolOptionsBar.tsx"],"sourcesContent":["/* Horizontal strip reserved above the workspace for contextual tool\n options. Fixed height so mounting/unmounting content never resizes\n the workspace beneath it — that's the whole reason this row exists\n as permanent chrome rather than an on-demand overlay.\n\n Overflow: when controls exceed the available width, `.controls`\n scrolls horizontally rather than wrapping (which would grow the row\n and defeat the fixed-height guarantee above) or clipping (which\n would hide controls with no way to reach them). A flyout/long-press\n affordance was considered and rejected — grouped, directly visible\n layouts are preferred in this repo. */\n\n.root {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-4);\n height: var(--wzl-tb-height);\n padding: 0 var(--wzl-space-4);\n background: var(--wzl-surface);\n border-bottom: 1px solid var(--wzl-border);\n color: var(--wzl-fg);\n overflow: hidden;\n}\n\n.label {\n flex: 0 0 auto;\n font-size: var(--wzl-font-size-sm);\n font-weight: var(--wzl-font-weight-bold);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--wzl-fg-muted);\n white-space: nowrap;\n}\n\n.controls {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-3);\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n}\n\n/* Firefox gets a thin scrollbar for free via `scrollbar-width` above.\n * Chromium/WebKit need it spelled out — on a classic (non-overlay)\n * scrollbar theme (default on Windows Chrome/Edge), an unstyled\n * horizontal scrollbar is ~15-17px tall and renders *inside* this box,\n * clipping/off-centering the row's controls. That would reintroduce the\n * exact height instability the scroll-over-wrap overflow choice above\n * exists to avoid — just at the row's inner edge instead of its outer\n * one. Pin it thin and out of the way instead. */\n.controls::-webkit-scrollbar {\n height: 4px;\n}\n.controls::-webkit-scrollbar-track {\n background: transparent;\n}\n.controls::-webkit-scrollbar-thumb {\n background: var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n}\n\n/* A schema-drawn control sits on the line as-is; the cell exists only to keep\n it from being squeezed by the row's flex context. */\n.cell {\n display: flex;\n align-items: center;\n flex: none;\n}\n\n.cellLabel {\n flex: none;\n margin-right: var(--wzl-space-2);\n font-size: var(--wzl-font-size-sm);\n color: var(--wzl-fg-muted);\n white-space: nowrap;\n}\n\n/* Fields are read beside their label on one line, so they carry only the\n digits they need rather than the panel's column width. */\n.controls {\n --wzl-number-field-width: 5ch;\n}\n\n/* The strip decides how much room a control gets, because its budget is one\n line rather than a column: a field carries the digits it needs and an\n opacity track stops short of pushing its neighbours off the end. Both are\n the panel's widths otherwise, which are sized for a two-column grid. */\n.cell input[type='text'] {\n inline-size: var(--wzl-number-field-width, 5ch);\n}\n\n.cell input[type='range'] {\n inline-size: 64px;\n flex: none;\n}\n","import type { ReactNode } from 'react';\nimport type { ToolPrefGroup, ToolPrefLeaf } from '@weasel-js/core';\nimport {\n renderBuiltin,\n renderCells,\n type LeafCell,\n type PropertyRenderContext,\n type PropertyRenderer,\n} from '../SelectionPanel/renderLeaf';\nimport { isGroup, pairRows, type PanelLeaf, type PanelRow } from '../SelectionPanel/model';\nimport s from './ToolOptionsBar.module.css';\n\n/** Props for {@link ToolOptionsBar}. */\nexport interface ToolOptionsBarProps {\n /**\n * Context label for the row — e.g. the active tool's name. Rendered\n * before the children and doubles as the toolbar's `aria-label`. Omit\n * it and the row still renders (it's a permanently reserved slot), just\n * without an accessible name.\n *\n * Using `label` for both the visible text and `aria-label` means a\n * screen reader announces it twice when landing on the toolbar (\"Text,\n * toolbar\", then \"Text\" again when browsing into the visible span).\n * `aria-labelledby` would avoid the echo at the cost of id plumbing and\n * an awkward no-`id`-to-point-at case when `label` is omitted. The\n * double-announcement is common enough for labeled landmark regions\n * that it's left as-is — considered, not missed.\n */\n label?: string;\n /**\n * The active tool's options. Leaves draw through the same leaf → control\n * mapping `SelectionPanel` uses, so a kind gets its control decided in one\n * place; groups organize without drawing, since the bar is one line.\n */\n schema?: ToolPrefGroup;\n /** Each leaf's value, keyed by dotted path. A path with no entry is unset. */\n values?: Readonly<Record<string, unknown>>;\n /** A leaf edit, as the dotted path it was declared at and its new value. */\n onChange?: (path: string, value: unknown) => void;\n /** Paths whose sources disagree — drawn as the control's mixed state. */\n mixed?: ReadonlySet<string>;\n /** Control overrides, by dotted path or by leaf kind, as the panel takes. */\n renderers?: Record<string, PropertyRenderer>;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * Horizontal strip reserved above the workspace for contextual tool options.\n * Given a `schema` it draws the active tool's options itself; the children\n * slot stays open for a tenant that is not a tool's options at all, such as\n * loupe controls.\n *\n * The row always renders, even empty, because the app reserves its height\n * permanently rather than mounting it on demand — mounting mid-edit would\n * resize the workspace under a canvas that's sized to the page.\n *\n * `role=\"toolbar\"` without roving tabindex: the ARIA authoring practices\n * expect arrow-key roving focus across a toolbar's items, but a bar of\n * compound controls (`NumberField`, `Select`, `ColorField`) must leave the\n * arrow keys to them — they edit their own value with them. That is true of\n * schema-drawn controls as much as of arbitrary children, so the bar keeps\n * plain DOM tab order and `useRovingTabIndex` documents this as its opt-out.\n */\nexport function ToolOptionsBar(props: ToolOptionsBarProps) {\n const { label, schema, values, onChange, mixed, renderers, children, className } = props;\n const cls = [s.root, className].filter(Boolean).join(' ');\n return (\n <div className={cls} role=\"toolbar\" aria-label={label}>\n {label !== undefined && <span className={s.label}>{label}</span>}\n <div className={s.controls}>\n {schema !== undefined &&\n barRows(schema).map((row) => (\n <BarRow\n key={row.leaves[0]!.path}\n row={row}\n values={values}\n mixed={mixed}\n renderers={renderers}\n onChange={onChange}\n />\n ))}\n {children}\n </div>\n </div>\n );\n}\n\n/**\n * The schema's visible leaves as rows, addressed by dotted path. Groups are\n * organizational here — a bar is one line, so a nested group contributes its\n * leaves and no chrome — but its path segment still names them, so a leaf\n * reads back at the path the tool declared it at.\n */\nfunction barRows(schema: ToolPrefGroup): PanelRow[] {\n const leaves: PanelLeaf[] = [];\n const collect = (group: ToolPrefGroup, prefix: string): void => {\n for (const [key, child] of Object.entries(group.children)) {\n const path = prefix === '' ? key : `${prefix}.${key}`;\n if (isGroup(child)) collect(child, path);\n else if (child.hidden !== true) leaves.push({ path, leaf: child });\n }\n };\n collect(schema, '');\n return pairRows(leaves);\n}\n\n/** One row of the bar: a single control, or a run of flags as one bar. */\nfunction BarRow({\n row,\n values,\n mixed,\n renderers,\n onChange,\n}: {\n row: PanelRow;\n values: Readonly<Record<string, unknown>> | undefined;\n mixed: ReadonlySet<string> | undefined;\n renderers: Record<string, PropertyRenderer> | undefined;\n onChange: ((path: string, value: unknown) => void) | undefined;\n}) {\n // The bar draws no row label, so a control carries its own name rather than\n // the panel's qualified \"<pair> <leaf>\" — the run's bar carries the pair.\n const cells = row.leaves.map((panelLeaf) =>\n barCell(panelLeaf, panelLeaf.leaf.name, values, mixed, renderers, onChange),\n );\n return (\n <>\n {renderCells(cells, renderers).map((cell) => (\n <span key={cell.key} className={s.cell}>\n {needsLabel(cell.leaf) && (\n <span className={s.cellLabel} aria-hidden=\"true\">\n {cell.leaf.short ?? cell.leaf.name}\n </span>\n )}\n {cell.content}\n </span>\n ))}\n </>\n );\n}\n\n/**\n * Whether a control needs telling from its neighbours in words.\n *\n * Only a bare value box does: a number or a string is a box of characters,\n * and two of them side by side are indistinguishable. Everything else shows\n * what it is — a swatch is a color, a font picker names the family, a flag\n * run and a segmented control label their own segments — so a word in front\n * would repeat what the control already says. `name` stays the accessible\n * name either way, so this costs a screen reader nothing.\n */\nfunction needsLabel(leaf: ToolPrefLeaf): boolean {\n return leaf.kind === 'number' || leaf.kind === 'string';\n}\n\nfunction barCell(\n panelLeaf: PanelLeaf,\n ariaLabel: string,\n values: Readonly<Record<string, unknown>> | undefined,\n mixed: ReadonlySet<string> | undefined,\n renderers: Record<string, PropertyRenderer> | undefined,\n onChange: ((path: string, value: unknown) => void) | undefined,\n): LeafCell {\n const { path, leaf } = panelLeaf;\n const isMixed = mixed?.has(path) === true;\n const ctx: PropertyRenderContext = {\n path,\n pref: leaf,\n value: isMixed ? undefined : values?.[path],\n mixed: isMixed,\n unset: !isMixed && values?.[path] === undefined,\n each: isMixed ? [] : [values?.[path]],\n setValue: (v) => onChange?.(path, v),\n // One set of values, so there is one prior value to derive from.\n update: (fn) => onChange?.(path, fn(isMixed ? undefined : values?.[path])),\n valueAt: (p) => ({ value: values?.[p], mixed: mixed?.has(p) === true }),\n };\n return {\n key: path,\n leaf: leaf as ToolPrefLeaf,\n ctx,\n ariaLabel,\n render: () => {\n const custom = renderers?.[path] ?? renderers?.[leaf.kind];\n return custom ? custom(ctx) : renderBuiltin(ctx, ariaLabel, renderers, path);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;ACgEA,SAAgBO,EAAe,GAA4B;CACzD,IAAM,EAAE,UAAO,WAAQ,WAAQ,aAAU,UAAO,cAAW,aAAU,iBAAcC,GAC7EC,IAAM,CAACT,EAAE,MAAMU,CAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CACxD,OACE,kBAAC,OAAD;EAAK,WAAWD;EAAK,MAAK;EAAU,cAAYE;EAAhD,UAAA,CACGA,MAAU,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAWX,EAAE;GAAQ,UAAAW;EAAY,CAAA,GAC/D,kBAAC,OAAD;GAAK,WAAWX,EAAE;GAAlB,UAAA,CACGY,MAAW,KAAA,KACVC,EAAQD,CAAM,CAAC,CAAC,KAAK,MACnB,kBAACE,GAAD;IAEO;IACG;IACD;IACI;IACD;GACX,GANMC,EAAI,OAAO,EAAE,CAAE,IAMrB,CACF,GACFC,CACE;EACF,CAAA,CAAA;;AAET;AAQA,SAASH,EAAQ,GAAmC;CAClD,IAAMI,IAAsB,CAAC,GACvBC,KAAW,GAAsB,MAAyB;EAC9D,KAAK,IAAM,CAACC,GAAKC,MAAU,OAAO,QAAQC,EAAM,QAAQ,GAAG;GACzD,IAAMC,IAAOC,MAAW,KAAKJ,IAAM,GAAGI,EAAO,GAAGJ;GAChD,AAAIjB,EAAQkB,CAAK,IAAGF,EAAQE,GAAOE,CAAI,IAC9BF,EAAM,WAAW,MAAMH,EAAO,KAAK;IAAE;IAAM,MAAMG;GAAM,CAAC;EACnE;CACF;CAEA,OADAF,EAAQN,GAAQ,EAAE,GACXT,EAASc,CAAM;AACxB;AAGA,SAASH,EAAO,EACd,QACA,WACA,UACA,cACA,eAOC;CAGD,IAAMU,IAAQT,EAAI,OAAO,KAAK,MAC5BU,EAAQC,GAAWA,EAAU,KAAK,MAAMC,GAAQC,GAAOC,GAAWC,CAAQ,CAC5E;CACA,OACE,kBAAA7B,GAAA,EAAA,UACGG,EAAYoB,GAAOK,CAAS,CAAC,CAAC,KAAK,MAClC,kBAAC,QAAD;EAAqB,WAAW7B,EAAE;EAAlC,UAAA,CACGgC,EAAWC,EAAK,IAAI,KACnB,kBAAC,QAAD;GAAM,WAAWjC,EAAE;GAAW,eAAY;GACvC,UAAAiC,EAAK,KAAK,SAASA,EAAK,KAAK;EAC1B,CAAA,GAEPA,EAAK,OACF;CAPK,GAAAA,EAAK,GAOV,CACP,EACD,CAAA;AAEN;AAYA,SAASD,EAAW,GAA6B;CAC/C,OAAOE,EAAK,SAAS,YAAYA,EAAK,SAAS;AACjD;AAEA,SAAST,EACP,GACA,GACA,GACA,GACA,GACA,GACU;CACV,IAAM,EAAE,SAAM,YAASC,GACjBS,IAAUP,GAAO,IAAIN,CAAI,MAAM,IAC/Bc,IAA6B;EACjC;EACA,MAAMF;EACN,OAAOC,IAAU,KAAA,IAAYR,IAASL;EACtC,OAAOa;EACP,OAAO,CAACA,KAAWR,IAASL,OAAU,KAAA;EACtC,MAAMa,IAAU,CAAC,IAAI,CAACR,IAASL,EAAK;EACpC,WAAW,MAAMQ,IAAWR,GAAMe,CAAC;EAEnC,SAAS,MAAOP,IAAWR,GAAMgB,EAAGH,IAAU,KAAA,IAAYR,IAASL,EAAK,CAAC;EACzE,UAAU,OAAO;GAAE,OAAOK,IAASY;GAAI,OAAOX,GAAO,IAAIW,CAAC,MAAM;EAAK;CACvE;CACA,OAAO;EACL,KAAKjB;EACC;EACN;EACA;EACA,cAAc;GACZ,IAAMkB,IAASX,IAAYP,MAASO,IAAYK,EAAK;GACrD,OAAOM,IAASA,EAAOJ,CAAG,IAAI/B,EAAc+B,GAAKK,GAAWZ,GAAWP,CAAI;EAC7E;CACF;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as e, t } from "./Icon--B1ME-6c.js";
|
|
2
|
-
import { r as n, t as r } from "./PropertyField-
|
|
2
|
+
import { r as n, t as r } from "./PropertyField-PMrqBA0n.js";
|
|
3
3
|
import { t as i } from "./ToggleBar-Z2jZiguF.js";
|
|
4
4
|
import { t as a } from "./prefField-BE_Ytr2H.js";
|
|
5
5
|
import { MIXED as o } from "@weasel-js/core";
|
|
@@ -383,4 +383,4 @@ function O(e) {
|
|
|
383
383
|
//#endregion
|
|
384
384
|
export { x as a, C as c, p as d, d as f, w as i, E as l, D as n, O as o, b as r, T as s, o as t, h as u };
|
|
385
385
|
|
|
386
|
-
//# sourceMappingURL=model-
|
|
386
|
+
//# sourceMappingURL=model-B6zuQg6b.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-qsAFK7_0.js","names":["s","Fragment","ICON_PATHS","drawnGlyph","PropertyControl","ToggleBar","prefFieldProps","Fragment$1","SelectionPanel_module_default","isFlagCell","cell","leaf","renderers","renderCells","out","i","cells","content","j","run","c","FlagBar","flags","perNodeState","on","f","field","now","next","renderBuiltin","ctx","pref","ObjectLeaf","siblingPath","ariaLabel","settledOnly","selectionKey","nested","dot","path","key","siblings","held","eachObject","v","allGrouped","child","rowsOf","isRow","pending","flush","prev","block","children","labeled","inner","indent","childPath","update","base","fn","_dropped","rest","childCtx","o","custom","rendered","Icon","entry","rows","classifyKind","node","routing","isGroup","n","flatten","sections","untitled","collect","group","into","schema","leaves","pairRows","item","pair","effectiveSections","uniq","kinds","byName","entries","schemas","k","s","first","flatFirst","leafKey","l","restKeys","r","keep","set","section","row","nodeValueAt","segments","head","cursor","setAtPath","root","value","aggregateValue","nodes","MIXED","kindBreakdown","counts"],"sources":["../../src/components/SelectionPanel/SelectionPanel.module.css","../../src/components/SelectionPanel/renderLeaf.tsx","../../src/components/SelectionPanel/model.ts"],"sourcesContent":[".root {\n display: flex;\n flex-direction: column;\n font-size: var(--wzl-font-size-sm);\n color: var(--wzl-fg);\n}\n\n.header {\n display: flex;\n align-items: baseline;\n gap: var(--wzl-space-4);\n padding: var(--wzl-space-4) 0;\n border-block-end: 1px solid var(--wzl-border);\n}\n\n.kind {\n font-weight: var(--wzl-font-weight-bold);\n font-size: var(--wzl-font-size);\n}\n\n.breakdown {\n font-size: var(--wzl-font-size-sm);\n opacity: 0.6;\n}\n\n.section {\n padding: var(--wzl-space-3) 0 var(--wzl-space-4);\n border-block-end: 1px solid var(--wzl-border);\n}\n\n.section:last-child {\n border-block-end: none;\n}\n\n.sectionTitle {\n margin: 0 0 var(--wzl-space-3);\n font-size: var(--wzl-font-size-xs);\n font-weight: var(--wzl-font-weight-bold);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n opacity: 0.55;\n}\n\n.row {\n display: grid;\n grid-template-columns: 64px 1fr;\n gap: var(--wzl-space-3);\n align-items: center;\n margin-block-end: 5px;\n}\n\n.rowLabel {\n opacity: 0.75;\n}\n\n.rowControls {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wzl-space-3);\n align-items: center;\n min-inline-size: 0;\n}\n\n/* Glyph plus its control, held together so the pair row wraps between\n fields rather than between a field and the icon naming it. */\n.namedField {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-2);\n min-inline-size: 0;\n}\n\n.namedField > svg {\n flex: none;\n opacity: 0.75;\n}\n\n.blockRow {\n margin-block-end: 5px;\n}\n\n/* Fields paired into one row split it evenly, the way Position's x/y do. */\n.rowControls > .namedField {\n flex: 1 1 0;\n}\n\n/* Except around a segmented bar, which is sized by its segments: an equal\n share of the row is wider than the buttons want to be. */\n.rowControls > .namedField:has(> .toggle) {\n flex: 0 1 auto;\n}\n\n.rowControls > * {\n flex: 1;\n min-inline-size: 0;\n}\n\n/* Bars sharing a row split the leftover between them, so the gap stays fixed\n and the width lands on the segments. A bar with the row to itself is sized\n by its segments instead — a third of 240px is a plate, not a button. */\n.rowControls > .toggle {\n flex: 1 1 auto;\n}\n\n.rowControls > .toggle:only-child {\n flex: 0 1 auto;\n}\n\n/* A flag bar is sized by its glyphs rather than by a share of the row: an\n even split would fling one-letter segments to the margins. */\n.rowControls > .flagToggle {\n flex: 0 0 auto;\n}\n\n.number :global(input),\n.select {\n font-size: var(--wzl-font-size-sm);\n}\n\n.unrenderable {\n font-size: var(--wzl-font-size-sm);\n opacity: 0.5;\n font-style: italic;\n}\n\n/* A `block` leaf that renders its own rows — an object leaf's fields. */\n.objectLeaf {\n margin-block: var(--wzl-space-4) var(--wzl-space-2);\n}\n\n.objectLeaf .sectionTitle {\n margin-block: 7px 5px;\n}\n\n/* Fields under a heading inside an object read as belonging to it. */\n.objectGroup {\n padding-inline-start: var(--wzl-space-4);\n border-inline-start: 1px solid var(--wzl-border);\n margin-block-end: var(--wzl-space-2);\n}\n\n/* The slider's track; the row only says how it shares the space. */\n.slider {\n flex: 1;\n}\n","/**\n * A selection's leaves as cells: one `ToolPrefLeaf` and its aggregated value\n * in, one rendered cell out, plus the run logic that turns a row of paired\n * toggles into a single segmented bar and the rows of an object leaf.\n *\n * The control itself is `PropertyControl`'s, through `prefFieldProps`; what\n * is decided here is how a selection surface sizes and composes it.\n * `ToolOptionsBar` renders a tool's options through the same functions.\n */\nimport { Fragment, type ReactNode } from 'react';\nimport type {\n ToolPrefBoolean,\n ToolPrefGroup,\n ToolPrefLeaf,\n ToolPrefObject,\n} from '@weasel-js/core';\nimport { prefFieldProps, type PrefFieldState } from '../Prefs/prefField';\nimport { drawnGlyph, PropertyControl, type PropertyBooleanFieldProps } from '../Properties/PropertyField';\nimport { ToggleBar } from '../ToggleBar';\nimport { Icon } from '../../icons/Icon';\nimport { ICON_PATHS, type IconName } from '../../icons/paths';\nimport s from './SelectionPanel.module.css';\n\n\n/** What a {@link PropertyRenderer} is given for the property it is rendering. */\nexport interface PropertyRenderContext {\n /** Dotted node path of the leaf (`pose.x`, `data.fill`). */\n path: string;\n /** The schema leaf. App renderers narrow it to their own kind shape. */\n pref: ToolPrefLeaf;\n /** Aggregated value across the selection; `undefined` when mixed or unset. */\n value: unknown;\n /** True when selected nodes disagree at this path. */\n mixed: boolean;\n /**\n * True when the nodes agree and what they agree on is nothing — the field\n * is absent, and whatever paints comes from a fallback further down.\n *\n * Distinct from `mixed`, and both leave `value` undefined. A control must\n * not substitute its schema default here: doing so shows a value the node\n * does not hold, and the next edit writes that invention back.\n */\n unset: boolean;\n /**\n * The fields of the object this leaf is a field of — `undefined` for a\n * top-level leaf, and for a field of an object the node does not hold.\n *\n * What a control needs when its own field doesn't carry the whole answer:\n * a stroke's dash lengths are multiples of the stroke's width, so reading\n * the array as a style takes both.\n */\n siblings?: Record<string, unknown>;\n /** The value each selected node holds at this path, in selection order —\n * what `value` aggregates, and what a mixed selection still has. */\n each: readonly unknown[];\n /**\n * For a field of an object leaf, each node's own object (parallel to\n * `each`), which is still there when the nodes' objects differ and\n * `siblings` is not.\n */\n eachSiblings?: readonly (Record<string, unknown> | undefined)[];\n /** Commit a value — fans out to every selected node in one undo step. */\n setValue: (value: unknown) => void;\n /**\n * Commit a value derived from what each node holds at this path, in one undo\n * step. What `setValue` cannot do across a selection whose nodes differ: a\n * field of an object leaf has to land in each node's own object, not in one\n * object written over all of them. `siblings` is that node's object, for a\n * field of an object leaf.\n */\n update: (fn: (prev: unknown, siblings?: Record<string, unknown>) => unknown) => void;\n /**\n * The aggregated value at another node path — what `value` and `mixed` are\n * for this leaf's own path, for any path.\n *\n * A control whose subject spans more than one field needs it: a font picker\n * reporting which variant will actually paint has to read the node's weight\n * and style. Reading it off `value` is impossible — a leaf is handed one\n * field.\n */\n valueAt: (path: string) => { value: unknown; mixed: boolean };\n /**\n * The ids of the nodes the values were read from, joined. It changes exactly\n * when the selection does, so a control holding scratch that belongs to one\n * selection can key on it and be remounted rather than carry it to the next.\n * Absent where the values come from no selection, as in a tool options bar.\n */\n selectionKey?: string;\n}\n\n/**\n * Renders the control for one property across the whole selection.\n * Returning `null` collapses the leaf.\n */\nexport type PropertyRenderer = (ctx: PropertyRenderContext) => ReactNode;\n\n\n/**\n * One leaf with its context built but nothing rendered yet.\n *\n * Deferring the render is what lets a run of sibling flags become one bar:\n * whether a leaf joins its neighbors is a fact about the run, which no leaf\n * rendering itself can see.\n */\nexport interface LeafCell {\n key: string;\n leaf: ToolPrefLeaf;\n ctx: PropertyRenderContext;\n /** The accessible name this leaf's control carries — qualified by the row's\n * pair where the row holds more than one leaf. */\n ariaLabel: string;\n /** Renders the leaf on its own, chrome and all. */\n render: () => ReactNode;\n}\n\n/** A cell's content, once the run it belongs to has been decided. */\nexport interface RenderedCell {\n key: string;\n /** The leaf that names the cell — the run's first, for a flag bar. */\n leaf: ToolPrefLeaf;\n block: boolean;\n content: ReactNode;\n}\n\n/**\n * True for a leaf that can join a flag bar: a paired toggle boolean the\n * consumer has not claimed with a renderer of its own.\n */\nexport function isFlagCell(cell: LeafCell, renderers?: Record<string, PropertyRenderer>): boolean {\n const { leaf } = cell;\n return (\n leaf.kind === 'boolean' &&\n (leaf as ToolPrefBoolean).control === 'toggle' &&\n leaf.pair !== undefined &&\n renderers?.[cell.ctx.path] === undefined &&\n renderers?.[leaf.kind] === undefined\n );\n}\n\n/**\n * Renders a row's cells, collapsing each run of adjacent same-`pair` toggle\n * booleans into one segmented bar — the idiom the enum toggle a row away\n * already uses, rather than one detached pill per flag.\n */\nexport function renderCells(\n cells: readonly LeafCell[],\n renderers?: Record<string, PropertyRenderer>,\n): RenderedCell[] {\n const out: RenderedCell[] = [];\n for (let i = 0; i < cells.length; ) {\n const cell = cells[i];\n if (!isFlagCell(cell, renderers)) {\n const content = cell.render();\n if (content != null) {\n out.push({ key: cell.key, leaf: cell.leaf, block: cell.leaf.block === true, content });\n }\n i += 1;\n continue;\n }\n let j = i + 1;\n while (\n j < cells.length &&\n isFlagCell(cells[j], renderers) &&\n cells[j].leaf.pair === cell.leaf.pair\n ) {\n j += 1;\n }\n const run = cells.slice(i, j);\n out.push({\n key: cell.key,\n leaf: cell.leaf,\n block: run.every((c) => c.leaf.block === true),\n content: <FlagBar run={run} ariaLabel={cell.leaf.pair as string} />,\n });\n i = j;\n }\n return out;\n}\n\n/**\n * A run of paired boolean flags as one multi-select bar — U / S / O, the way\n * every text editor draws them.\n */\nfunction FlagBar({ run, ariaLabel }: { run: readonly LeafCell[]; ariaLabel: string }): ReactNode {\n // Each flag through the same field props a lone one gets, so an `encoding`\n // (Italic stored as `fontStyle`) reads and writes the same way in a bar.\n const flags = run.map((c) => {\n const field = prefFieldProps(c.leaf, {\n value: c.ctx.value,\n mixed: c.ctx.mixed,\n unset: c.ctx.unset,\n siblings: c.ctx.siblings,\n setValue: c.ctx.setValue,\n ...perNodeState(c.ctx),\n }) as PropertyBooleanFieldProps;\n return { cell: c, field };\n });\n const on = flags.filter((f) => !f.field.mixed && f.field.value === true).map((f) => f.cell.key);\n return (\n <ToggleBar<string>\n mode=\"multiple\"\n size=\"sm\"\n variant=\"flat\"\n className={s.flagToggle}\n ariaLabel={ariaLabel}\n items={flags.map(({ cell, field }) => ({\n value: cell.key,\n label: field.glyph ?? cell.leaf.name.slice(0, 1),\n ariaLabel: cell.ariaLabel,\n tooltip: drawnGlyph(field.glyph) === undefined ? undefined : cell.leaf.name,\n }))}\n value={on}\n // Each flag owns its own path, so only the segment that moved is\n // written — committing the run would write two fields nobody touched,\n // and inside an object leaf would fabricate values for the other two.\n mixedValues={flags.filter((f) => f.field.mixed).map((f) => f.cell.key)}\n onChange={(next) => {\n for (const { cell, field } of flags) {\n const now = next.includes(cell.key);\n if (now !== on.includes(cell.key)) field.onChange(now);\n }\n }}\n />\n );\n}\n\nexport function renderBuiltin(\n ctx: PropertyRenderContext,\n ariaLabel: string,\n renderers?: Record<string, PropertyRenderer>,\n /** Identifies the current selection, so a control holding per-selection\n * scratch (the paint kind memory) is remounted rather than carried over. */\n selectionKey = '',\n /** True for a field of an object leaf. Such a field writes itself into its\n * parent, so it can only hold values its parent's type permits — which is\n * what rules \"none\" out for a nested paint. */\n nested = false,\n): ReactNode {\n const { pref, value, mixed, unset, setValue } = ctx;\n if (pref.kind === 'object') {\n // One value with fields hanging off it. Each child writes the parent\n // whole, so a field is never set on a half-built object.\n return <ObjectLeaf ctx={ctx} renderers={renderers} selectionKey={selectionKey} />;\n }\n const field = prefFieldProps(pref, {\n value,\n mixed,\n unset,\n siblings: ctx.siblings,\n setValue,\n ...perNodeState(ctx),\n // The font pickers read the rest of the font from the leaves beside\n // them: the family's substitution probe runs at the weight and style\n // that will paint, and a weight lists what its family has. A mixed\n // selection has no single one; the probe falls back to 400/normal there.\n fontVariant: {\n weight: ctx.valueAt(siblingPath(ctx.path, 'fontWeight')).value,\n style: ctx.valueAt(siblingPath(ctx.path, 'fontStyle')).value,\n },\n fontFamily: ctx.valueAt(siblingPath(ctx.path, 'fontFamily')).value,\n });\n if (field === null) {\n return <span className={s.unrenderable}>({pref.kind}: no renderer)</span>;\n }\n switch (field.kind) {\n case 'boolean':\n // A panel's flag is a switch unless it asks otherwise.\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n control={field.control ?? 'switch'}\n className={field.control === 'toggle' ? s.flagToggle : undefined}\n />\n );\n case 'number':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n className={field.control === 'slider' ? s.slider : s.number}\n // A typed number writes once it is entered, as text does; a slider\n // writes as it moves.\n onInput={field.control === 'slider' ? field.onInput : settledOnly}\n />\n );\n case 'string':\n // Settled values only: a live write per keystroke would be one undo\n // step per character.\n return <PropertyControl {...field} name={ariaLabel} onInput={settledOnly} />;\n case 'enum':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n // A clearable toggle is a set of exclusive flags, and sits beside\n // other flags sized the way they are.\n className={\n field.control === 'toggle'\n ? field.onClear ? s.flagToggle : s.toggle\n : field.control === 'radio' ? undefined : s.select\n }\n />\n );\n case 'font-family':\n case 'font-weight':\n return <PropertyControl {...field} name={ariaLabel} className={s.select} />;\n case 'paint':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n // Per-kind switch memory is scratch for one selection; carrying it\n // across would recall the previous node's gradient.\n key={selectionKey}\n control=\"inline\"\n // A nested paint writes one field of its parent, and no kit paint\n // field is optional — `Stroke.paint` is required. Removing the\n // whole parent is a different edit than repainting it, so the\n // control must not offer one as the other.\n allowNone={!nested}\n />\n );\n case 'color':\n // One write per gesture, not one per step of the picker.\n return <PropertyControl {...field} name={ariaLabel} onInput={settledOnly} />;\n }\n}\n\nconst settledOnly = (): void => {};\n\n/** The dotted path of the leaf `key` beside the one at `path`. */\nfunction siblingPath(path: string, key: string): string {\n const dot = path.lastIndexOf('.');\n return dot < 0 ? key : `${path.slice(0, dot + 1)}${key}`;\n}\n\n/** What an `encoding` needs to read and write each node against its own\n * object — nothing for a leaf that is not a field of one. */\nfunction perNodeState(ctx: PropertyRenderContext): Pick<PrefFieldState, 'perNode' | 'update'> {\n if (ctx.eachSiblings === undefined) return {};\n const siblings = ctx.eachSiblings;\n return {\n perNode: ctx.each.map((value, i) => ({ value, siblings: siblings[i] })),\n update: ctx.update,\n };\n}\n\n/**\n * Renders an object leaf: a titled block whose rows are the object's own\n * fields. A child's edit commits the parent object, never the child's path —\n * writing into a path whose value is not an object yet would corrupt it.\n */\n/** One labeled row inside an object leaf; `pair` merges adjacent fields into\n * it, exactly as the section rows merge theirs. */\ninterface ObjectRow {\n key: string;\n pair?: string;\n label: string;\n title?: string;\n /** Every field in the row brought its own chrome, so the row drops the\n * label column and spans the block. One unlabeled field in an otherwise\n * labeled pair would leave the row named after half of itself. */\n block: boolean;\n controls: ReactNode[];\n}\n\n/**\n * Renders an object leaf: the object's own fields, as rows. A child's edit\n * commits the parent object, never the child's path — writing into a path\n * whose value is not an object yet would corrupt it.\n */\nfunction ObjectLeaf({\n ctx,\n renderers,\n selectionKey = '',\n}: {\n ctx: PropertyRenderContext;\n renderers?: Record<string, PropertyRenderer>;\n selectionKey?: string;\n}): ReactNode {\n const pref = ctx.pref as ToolPrefObject;\n const held = typeof ctx.value === 'object' && ctx.value !== null\n ? (ctx.value as Record<string, unknown>)\n : undefined;\n const eachObject = ctx.each.map((v) =>\n typeof v === 'object' && v !== null ? (v as Record<string, unknown>) : undefined,\n );\n\n // A value whose fields are entirely grouped is titled by those groups — its\n // own heading would stack onto the first one and name nothing new.\n const allGrouped = Object.values(pref.children).every((child) => !('kind' in child));\n\n // `indent` is false when nothing visible sits above these rows: depth is\n // drawn only where a label marks it.\n const rowsOf = (\n children: Record<string, ToolPrefLeaf | ToolPrefGroup>,\n indent: boolean,\n ): ReactNode[] => {\n const out: (ReactNode | ObjectRow)[] = [];\n const isRow = (v: ReactNode | ObjectRow): v is ObjectRow =>\n typeof v === 'object' && v !== null && 'controls' in v;\n\n // Cells accumulate unrendered so a run of adjacent same-`pair` flags can\n // be seen as a run before any of them draws; a group heading between two\n // fields ends the run, exactly as it ends a paired row.\n let pending: LeafCell[] = [];\n const flush = (): void => {\n for (const { key, leaf, block, content } of renderCells(pending, renderers)) {\n const prev = out[out.length - 1];\n if (leaf.pair !== undefined && isRow(prev) && prev.pair === leaf.pair) {\n prev.controls.push(<Fragment key={key}>{content}</Fragment>);\n prev.block &&= block;\n continue;\n }\n out.push({\n key,\n pair: leaf.pair,\n label: leaf.pair ?? leaf.name,\n title: leaf.description,\n block,\n controls: [<Fragment key={key}>{content}</Fragment>],\n });\n }\n pending = [];\n };\n\n for (const [key, child] of Object.entries(children)) {\n // A group among the children organises the fields under a heading\n // without contributing to the path — the rule group keys follow at the\n // top level.\n if (!('kind' in child)) {\n const labeled = child.name !== '';\n const inner = rowsOf(child.children, labeled);\n if (inner.length === 0) continue;\n flush();\n out.push(\n <div key={`group:${key}`} className={labeled && indent ? s.objectGroup : undefined}>\n {labeled && <h5 className={s.sectionTitle}>{child.name}</h5>}\n {inner}\n </div>,\n );\n continue;\n }\n const childPath = `${ctx.path}.${key}`;\n // Nodes whose objects differ still agree or disagree field by field: two\n // styles at different sizes are both upright, and saying \"mixed\" for\n // every field of them hides that.\n const field = ctx.mixed ? ctx.valueAt(childPath) : { value: held?.[key], mixed: false };\n // Each node's own object, with this one field changed. Deriving it per\n // node is what keeps a multi-selection edit from writing one node's\n // object over all the others.\n const update = (fn: (prev: unknown, siblings?: Record<string, unknown>) => unknown): void =>\n ctx.update((prev) => {\n // The node holds no object yet, so writing one field has to\n // materialize the rest: the leaf's `default` is what a complete\n // value looks like. Starting from `{}` instead committed the one\n // field on its own — a `data.stroke` of `{ width: 2 }` with no\n // `paint`, which the type forbids and the painter threw on, taking\n // the whole frame and the visible document with it.\n const base: Record<string, unknown> =\n typeof prev === 'object' && prev !== null\n ? (prev as Record<string, unknown>)\n : (pref.fromScalar?.(prev)\n ?? (typeof pref.default === 'object' && pref.default !== null\n ? { ...(pref.default as Record<string, unknown>) }\n : {}));\n const v = fn(base[key], base);\n if (v === undefined) {\n // A field written as absent is absent — leaving the key holding\n // `undefined` says the object has a dash of nothing.\n const { [key]: _dropped, ...rest } = base;\n return rest;\n }\n return { ...base, [key]: v };\n });\n const childCtx: PropertyRenderContext = {\n path: childPath,\n pref: child,\n value: field.mixed ? undefined : field.value,\n mixed: field.mixed,\n // A field of an object the node does not hold is unset, and so is one\n // the object omits — `data.stroke` absent leaves every stroke field\n // with nothing behind it, not with the schema's defaults.\n unset: !field.mixed && field.value === undefined,\n siblings: held,\n each: eachObject.map((o) => o?.[key]),\n eachSiblings: eachObject,\n valueAt: ctx.valueAt,\n selectionKey: ctx.selectionKey,\n update,\n setValue: (v) => update(() => v),\n };\n pending.push({\n key: childPath,\n leaf: child,\n ctx: childCtx,\n ariaLabel: child.name,\n render: () => {\n const custom = renderers?.[childPath] ?? renderers?.[child.kind];\n const rendered = custom\n ? custom(childCtx)\n : renderBuiltin(childCtx, child.name, renderers, selectionKey, true);\n if (rendered == null) return null;\n // Neither a paired row nor a block one gives a field its own label\n // column, so the glyph is all that names it on screen. The control\n // already carries `name` as its accessible name, which leaves the\n // glyph decorative.\n const unlabeled = child.pair !== undefined || child.block === true;\n const glyph = unlabeled && child.icon && child.icon in ICON_PATHS;\n if (!glyph) return rendered;\n return (\n <span className={s.namedField}>\n <Icon name={child.icon as IconName} size={14} />\n {rendered}\n </span>\n );\n },\n });\n }\n flush();\n return out.map((entry) =>\n typeof entry === 'object' && entry !== null && 'controls' in entry ? (\n entry.block ? (\n <div key={entry.key} className={`${s.rowControls} ${s.blockRow}`} title={entry.title}>\n {entry.controls}\n </div>\n ) : (\n <div key={entry.key} className={s.row}>\n <span className={s.rowLabel} title={entry.title}>{entry.label}</span>\n <span className={s.rowControls}>{entry.controls}</span>\n </div>\n )\n ) : (\n entry\n ),\n );\n };\n\n const rows = rowsOf(pref.children, !allGrouped);\n if (rows.length === 0) return null;\n return (\n <div className={s.objectLeaf}>\n {!allGrouped && <h4 className={s.sectionTitle}>{pref.name}</h4>}\n {rows}\n </div>\n );\n}\n","// Pure selection→panel derivations for SelectionPanel. Kept free of\n// React so intersection/aggregation semantics are unit-testable.\n//\n// Path convention (see core `NodePropertiesEntry`): a leaf's OWN KEY in\n// the schema is its node path — a dotted path of any depth rooted at\n// `pose` or `data` (`pose.x`, `data.fill`, `data.style.fontSize`). Group\n// keys are organizational only.\n\nimport {\n MIXED,\n type Mixed,\n type NodePropertiesEntry,\n type NodeRoutingEntry,\n type SceneNode,\n type ToolPrefGroup,\n type ToolPrefLeaf,\n} from '@weasel-js/core';\n\n// Re-exported under its existing public name here: the kit-wide \"these\n// values disagree\" sentinel (`@weasel-js/core`'s `MIXED`) also covers\n// multi-node aggregation, so this panel doesn't get its own symbol.\nexport { MIXED };\nexport type { Mixed };\n\nexport type AnyNode = SceneNode<unknown, string, unknown>;\n\n/**\n * One editable property in a {@link SelectionPanel}, paired with the dotted\n * node path it reads and writes.\n */\nexport interface PanelLeaf {\n /** Dotted node path — the leaf's key in the schema. */\n path: string;\n leaf: ToolPrefLeaf;\n}\n\n/**\n * One labeled row of a panel section. Holds several leaves when the schema\n * paired them.\n */\nexport interface PanelRow {\n label: string;\n leaves: PanelLeaf[];\n}\n\n/**\n * A titled block of rows, derived from one top-level group of a properties\n * schema. `key` is that group's schema key.\n */\nexport interface PanelSection {\n key: string;\n name: string;\n rows: PanelRow[];\n}\n\n/** Derive a node's kind: containers are `'group'`; leaves classify their\n * `data` through the routing entries (first match wins, `'unknown'`\n * otherwise) — same semantics as core's `NodeRouting.classify`. */\nexport function classifyKind(\n node: AnyNode,\n routing: readonly NodeRoutingEntry[],\n): string {\n if (node.kind === 'container') return 'group';\n for (const entry of routing) {\n if (entry.matches(node.data)) return entry.name;\n }\n return 'unknown';\n}\n\nexport function isGroup(n: ToolPrefLeaf | ToolPrefGroup): n is ToolPrefGroup {\n return !('kind' in n);\n}\n\n/** Flatten a schema to sections of leaves. Nested groups fold into their\n * top-level section; top-level leaves get an untitled leading section. */\nfunction flatten(schema: ToolPrefGroup): PanelSection[] {\n const sections: PanelSection[] = [];\n const untitled: PanelLeaf[] = [];\n\n const collect = (group: ToolPrefGroup, into: PanelLeaf[]): void => {\n for (const [key, child] of Object.entries(group.children)) {\n if (isGroup(child)) collect(child, into);\n else into.push({ path: key, leaf: child });\n }\n };\n\n for (const [key, child] of Object.entries(schema.children)) {\n if (isGroup(child)) {\n const leaves: PanelLeaf[] = [];\n collect(child, leaves);\n sections.push({ key, name: child.name, rows: pairRows(leaves) });\n } else {\n untitled.push({ path: key, leaf: child });\n }\n }\n if (untitled.length > 0) {\n sections.unshift({ key: '', name: '', rows: pairRows(untitled) });\n }\n return sections;\n}\n\n/** Merge consecutive leaves sharing a `pair` id into one labeled row. */\nexport function pairRows(leaves: readonly PanelLeaf[]): PanelRow[] {\n const rows: PanelRow[] = [];\n for (const item of leaves) {\n const pair = item.leaf.pair;\n const prev = rows[rows.length - 1];\n if (pair !== undefined && prev !== undefined && prev.label === pair) {\n prev.leaves.push(item);\n } else {\n rows.push({ label: pair ?? item.leaf.name, leaves: [item] });\n }\n }\n return rows;\n}\n\n/**\n * The schema the panel shows for a set of kinds: one kind → its full\n * schema; several → the intersection by (path, leaf kind), shaped by the\n * first kind's section/row layout. Kinds without a registered schema\n * contribute nothing, so their presence collapses the intersection.\n */\nexport function effectiveSections(\n kinds: readonly string[],\n entries: readonly NodePropertiesEntry[],\n): PanelSection[] {\n const uniq = [...new Set(kinds)];\n if (uniq.length === 0) return [];\n const byName = new Map(entries.map((e) => [e.name, e]));\n const schemas = uniq.map((k) => byName.get(k)?.schema);\n if (schemas.some((s) => s === undefined)) return [];\n const [first, ...rest] = schemas as ToolPrefGroup[];\n\n const flatFirst = flatten(first);\n if (rest.length === 0) return flatFirst;\n\n // '\\0' delimiter: path and kind strings are unconstrained, so a\n // printable separator could collide with real content.\n const leafKey = (l: PanelLeaf): string => `${l.path}\\0${l.leaf.kind}`;\n const restKeys = rest.map(\n (schema) =>\n new Set(\n flatten(schema)\n .flatMap((s) => s.rows)\n .flatMap((r) => r.leaves)\n .map(leafKey),\n ),\n );\n const keep = (l: PanelLeaf): boolean =>\n restKeys.every((set) => set.has(leafKey(l)));\n\n return flatFirst\n .map((section) => ({\n ...section,\n rows: section.rows\n .map((row) => ({ ...row, leaves: row.leaves.filter(keep) }))\n .filter((row) => row.leaves.length > 0),\n }))\n .filter((section) => section.rows.length > 0);\n}\n\n/** Read a node value at a dotted path of any depth (`pose.x`,\n * `data.style.fontSize`, `data.style.fill.color`). Returns `undefined` if\n * the path is dotless (mirrors `commit`'s no-op on a malformed schema\n * key) or if any intermediate segment is missing or not an object. */\nexport function nodeValueAt(node: AnyNode, path: string): unknown {\n const segments = path.split('.');\n if (segments.length < 2) return undefined;\n const head = segments[0];\n let cursor: unknown = head === 'pose' ? node.pose : head === 'data' ? node.data : undefined;\n for (let i = 1; i < segments.length; i++) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[segments[i]];\n }\n return cursor;\n}\n\n/** Immutably set `value` at a dotted path within `root`, cloning each level\n * on the way down so React sees new object identities. Arrays stay arrays\n * (an array intermediate is shallow-copied, not flattened into `{0: ...}`)\n * — this is the write-side twin of `nodeValueAt`, so a schema author who\n * gets a working read gets a working write. */\nexport function setAtPath(root: object, segments: readonly string[], value: unknown): object {\n const [head, ...rest] = segments;\n if (rest.length === 0) {\n return Array.isArray(root)\n ? Object.assign([...root], { [head]: value })\n : { ...root, [head]: value };\n }\n const child = (root as Record<string, unknown>)[head];\n const childObj = Array.isArray(child)\n ? [...child]\n : child != null && typeof child === 'object'\n ? { ...child }\n : {};\n const nested = setAtPath(childObj, rest, value);\n return Array.isArray(root)\n ? Object.assign([...root], { [head]: nested })\n : { ...root, [head]: nested };\n}\n\n/** Aggregate a path across nodes: the shared value, or `MIXED`. */\nexport function aggregateValue(\n nodes: readonly AnyNode[],\n path: string,\n): unknown | Mixed {\n let value: unknown;\n let first = true;\n for (const node of nodes) {\n const v = nodeValueAt(node, path);\n if (first) {\n value = v;\n first = false;\n } else if (!Object.is(v, value)) {\n return MIXED;\n }\n }\n return value;\n}\n\n/** `'rect ×2 · text'` — kinds in first-seen order with counts. */\nexport function kindBreakdown(kinds: readonly string[]): string {\n const counts = new Map<string, number>();\n for (const k of kinds) counts.set(k, (counts.get(k) ?? 0) + 1);\n return [...counts.entries()]\n .map(([k, n]) => (n > 1 ? `${k} ×${n}` : k))\n .join(' · ');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"model-B6zuQg6b.js","names":["s","Fragment","ICON_PATHS","drawnGlyph","PropertyControl","ToggleBar","prefFieldProps","Fragment$1","SelectionPanel_module_default","isFlagCell","cell","leaf","renderers","renderCells","out","i","cells","content","j","run","c","FlagBar","flags","perNodeState","on","f","field","now","next","renderBuiltin","ctx","pref","ObjectLeaf","siblingPath","ariaLabel","settledOnly","selectionKey","nested","dot","path","key","siblings","held","eachObject","v","allGrouped","child","rowsOf","isRow","pending","flush","prev","block","children","labeled","inner","indent","childPath","update","base","fn","_dropped","rest","childCtx","o","custom","rendered","Icon","entry","rows","classifyKind","node","routing","isGroup","n","flatten","sections","untitled","collect","group","into","schema","leaves","pairRows","item","pair","effectiveSections","uniq","kinds","byName","entries","schemas","k","s","first","flatFirst","leafKey","l","restKeys","r","keep","set","section","row","nodeValueAt","segments","head","cursor","setAtPath","root","value","aggregateValue","nodes","MIXED","kindBreakdown","counts"],"sources":["../../src/components/SelectionPanel/SelectionPanel.module.css","../../src/components/SelectionPanel/renderLeaf.tsx","../../src/components/SelectionPanel/model.ts"],"sourcesContent":[".root {\n display: flex;\n flex-direction: column;\n font-size: var(--wzl-font-size-sm);\n color: var(--wzl-fg);\n}\n\n.header {\n display: flex;\n align-items: baseline;\n gap: var(--wzl-space-4);\n padding: var(--wzl-space-4) 0;\n border-block-end: 1px solid var(--wzl-border);\n}\n\n.kind {\n font-weight: var(--wzl-font-weight-bold);\n font-size: var(--wzl-font-size);\n}\n\n.breakdown {\n font-size: var(--wzl-font-size-sm);\n opacity: 0.6;\n}\n\n.section {\n padding: var(--wzl-space-3) 0 var(--wzl-space-4);\n border-block-end: 1px solid var(--wzl-border);\n}\n\n.section:last-child {\n border-block-end: none;\n}\n\n.sectionTitle {\n margin: 0 0 var(--wzl-space-3);\n font-size: var(--wzl-font-size-xs);\n font-weight: var(--wzl-font-weight-bold);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n opacity: 0.55;\n}\n\n.row {\n display: grid;\n grid-template-columns: 64px 1fr;\n gap: var(--wzl-space-3);\n align-items: center;\n margin-block-end: 5px;\n}\n\n.rowLabel {\n opacity: 0.75;\n}\n\n.rowControls {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wzl-space-3);\n align-items: center;\n min-inline-size: 0;\n}\n\n/* Glyph plus its control, held together so the pair row wraps between\n fields rather than between a field and the icon naming it. */\n.namedField {\n display: flex;\n align-items: center;\n gap: var(--wzl-space-2);\n min-inline-size: 0;\n}\n\n.namedField > svg {\n flex: none;\n opacity: 0.75;\n}\n\n.blockRow {\n margin-block-end: 5px;\n}\n\n/* Fields paired into one row split it evenly, the way Position's x/y do. */\n.rowControls > .namedField {\n flex: 1 1 0;\n}\n\n/* Except around a segmented bar, which is sized by its segments: an equal\n share of the row is wider than the buttons want to be. */\n.rowControls > .namedField:has(> .toggle) {\n flex: 0 1 auto;\n}\n\n.rowControls > * {\n flex: 1;\n min-inline-size: 0;\n}\n\n/* Bars sharing a row split the leftover between them, so the gap stays fixed\n and the width lands on the segments. A bar with the row to itself is sized\n by its segments instead — a third of 240px is a plate, not a button. */\n.rowControls > .toggle {\n flex: 1 1 auto;\n}\n\n.rowControls > .toggle:only-child {\n flex: 0 1 auto;\n}\n\n/* A flag bar is sized by its glyphs rather than by a share of the row: an\n even split would fling one-letter segments to the margins. */\n.rowControls > .flagToggle {\n flex: 0 0 auto;\n}\n\n.number :global(input),\n.select {\n font-size: var(--wzl-font-size-sm);\n}\n\n.unrenderable {\n font-size: var(--wzl-font-size-sm);\n opacity: 0.5;\n font-style: italic;\n}\n\n/* A `block` leaf that renders its own rows — an object leaf's fields. */\n.objectLeaf {\n margin-block: var(--wzl-space-4) var(--wzl-space-2);\n}\n\n.objectLeaf .sectionTitle {\n margin-block: 7px 5px;\n}\n\n/* Fields under a heading inside an object read as belonging to it. */\n.objectGroup {\n padding-inline-start: var(--wzl-space-4);\n border-inline-start: 1px solid var(--wzl-border);\n margin-block-end: var(--wzl-space-2);\n}\n\n/* The slider's track; the row only says how it shares the space. */\n.slider {\n flex: 1;\n}\n","/**\n * A selection's leaves as cells: one `ToolPrefLeaf` and its aggregated value\n * in, one rendered cell out, plus the run logic that turns a row of paired\n * toggles into a single segmented bar and the rows of an object leaf.\n *\n * The control itself is `PropertyControl`'s, through `prefFieldProps`; what\n * is decided here is how a selection surface sizes and composes it.\n * `ToolOptionsBar` renders a tool's options through the same functions.\n */\nimport { Fragment, type ReactNode } from 'react';\nimport type {\n ToolPrefBoolean,\n ToolPrefGroup,\n ToolPrefLeaf,\n ToolPrefObject,\n} from '@weasel-js/core';\nimport { prefFieldProps, type PrefFieldState } from '../Prefs/prefField';\nimport { drawnGlyph, PropertyControl, type PropertyBooleanFieldProps } from '../Properties/PropertyField';\nimport { ToggleBar } from '../ToggleBar';\nimport { Icon } from '../../icons/Icon';\nimport { ICON_PATHS, type IconName } from '../../icons/paths';\nimport s from './SelectionPanel.module.css';\n\n\n/** What a {@link PropertyRenderer} is given for the property it is rendering. */\nexport interface PropertyRenderContext {\n /** Dotted node path of the leaf (`pose.x`, `data.fill`). */\n path: string;\n /** The schema leaf. App renderers narrow it to their own kind shape. */\n pref: ToolPrefLeaf;\n /** Aggregated value across the selection; `undefined` when mixed or unset. */\n value: unknown;\n /** True when selected nodes disagree at this path. */\n mixed: boolean;\n /**\n * True when the nodes agree and what they agree on is nothing — the field\n * is absent, and whatever paints comes from a fallback further down.\n *\n * Distinct from `mixed`, and both leave `value` undefined. A control must\n * not substitute its schema default here: doing so shows a value the node\n * does not hold, and the next edit writes that invention back.\n */\n unset: boolean;\n /**\n * The fields of the object this leaf is a field of — `undefined` for a\n * top-level leaf, and for a field of an object the node does not hold.\n *\n * What a control needs when its own field doesn't carry the whole answer:\n * a stroke's dash lengths are multiples of the stroke's width, so reading\n * the array as a style takes both.\n */\n siblings?: Record<string, unknown>;\n /** The value each selected node holds at this path, in selection order —\n * what `value` aggregates, and what a mixed selection still has. */\n each: readonly unknown[];\n /**\n * For a field of an object leaf, each node's own object (parallel to\n * `each`), which is still there when the nodes' objects differ and\n * `siblings` is not.\n */\n eachSiblings?: readonly (Record<string, unknown> | undefined)[];\n /** Commit a value — fans out to every selected node in one undo step. */\n setValue: (value: unknown) => void;\n /**\n * Commit a value derived from what each node holds at this path, in one undo\n * step. What `setValue` cannot do across a selection whose nodes differ: a\n * field of an object leaf has to land in each node's own object, not in one\n * object written over all of them. `siblings` is that node's object, for a\n * field of an object leaf.\n */\n update: (fn: (prev: unknown, siblings?: Record<string, unknown>) => unknown) => void;\n /**\n * The aggregated value at another node path — what `value` and `mixed` are\n * for this leaf's own path, for any path.\n *\n * A control whose subject spans more than one field needs it: a font picker\n * reporting which variant will actually paint has to read the node's weight\n * and style. Reading it off `value` is impossible — a leaf is handed one\n * field.\n */\n valueAt: (path: string) => { value: unknown; mixed: boolean };\n /**\n * The ids of the nodes the values were read from, joined. It changes exactly\n * when the selection does, so a control holding scratch that belongs to one\n * selection can key on it and be remounted rather than carry it to the next.\n * Absent where the values come from no selection, as in a tool options bar.\n */\n selectionKey?: string;\n}\n\n/**\n * Renders the control for one property across the whole selection.\n * Returning `null` collapses the leaf.\n */\nexport type PropertyRenderer = (ctx: PropertyRenderContext) => ReactNode;\n\n\n/**\n * One leaf with its context built but nothing rendered yet.\n *\n * Deferring the render is what lets a run of sibling flags become one bar:\n * whether a leaf joins its neighbors is a fact about the run, which no leaf\n * rendering itself can see.\n */\nexport interface LeafCell {\n key: string;\n leaf: ToolPrefLeaf;\n ctx: PropertyRenderContext;\n /** The accessible name this leaf's control carries — qualified by the row's\n * pair where the row holds more than one leaf. */\n ariaLabel: string;\n /** Renders the leaf on its own, chrome and all. */\n render: () => ReactNode;\n}\n\n/** A cell's content, once the run it belongs to has been decided. */\nexport interface RenderedCell {\n key: string;\n /** The leaf that names the cell — the run's first, for a flag bar. */\n leaf: ToolPrefLeaf;\n block: boolean;\n content: ReactNode;\n}\n\n/**\n * True for a leaf that can join a flag bar: a paired toggle boolean the\n * consumer has not claimed with a renderer of its own.\n */\nexport function isFlagCell(cell: LeafCell, renderers?: Record<string, PropertyRenderer>): boolean {\n const { leaf } = cell;\n return (\n leaf.kind === 'boolean' &&\n (leaf as ToolPrefBoolean).control === 'toggle' &&\n leaf.pair !== undefined &&\n renderers?.[cell.ctx.path] === undefined &&\n renderers?.[leaf.kind] === undefined\n );\n}\n\n/**\n * Renders a row's cells, collapsing each run of adjacent same-`pair` toggle\n * booleans into one segmented bar — the idiom the enum toggle a row away\n * already uses, rather than one detached pill per flag.\n */\nexport function renderCells(\n cells: readonly LeafCell[],\n renderers?: Record<string, PropertyRenderer>,\n): RenderedCell[] {\n const out: RenderedCell[] = [];\n for (let i = 0; i < cells.length; ) {\n const cell = cells[i];\n if (!isFlagCell(cell, renderers)) {\n const content = cell.render();\n if (content != null) {\n out.push({ key: cell.key, leaf: cell.leaf, block: cell.leaf.block === true, content });\n }\n i += 1;\n continue;\n }\n let j = i + 1;\n while (\n j < cells.length &&\n isFlagCell(cells[j], renderers) &&\n cells[j].leaf.pair === cell.leaf.pair\n ) {\n j += 1;\n }\n const run = cells.slice(i, j);\n out.push({\n key: cell.key,\n leaf: cell.leaf,\n block: run.every((c) => c.leaf.block === true),\n content: <FlagBar run={run} ariaLabel={cell.leaf.pair as string} />,\n });\n i = j;\n }\n return out;\n}\n\n/**\n * A run of paired boolean flags as one multi-select bar — U / S / O, the way\n * every text editor draws them.\n */\nfunction FlagBar({ run, ariaLabel }: { run: readonly LeafCell[]; ariaLabel: string }): ReactNode {\n // Each flag through the same field props a lone one gets, so an `encoding`\n // (Italic stored as `fontStyle`) reads and writes the same way in a bar.\n const flags = run.map((c) => {\n const field = prefFieldProps(c.leaf, {\n value: c.ctx.value,\n mixed: c.ctx.mixed,\n unset: c.ctx.unset,\n siblings: c.ctx.siblings,\n setValue: c.ctx.setValue,\n ...perNodeState(c.ctx),\n }) as PropertyBooleanFieldProps;\n return { cell: c, field };\n });\n const on = flags.filter((f) => !f.field.mixed && f.field.value === true).map((f) => f.cell.key);\n return (\n <ToggleBar<string>\n mode=\"multiple\"\n size=\"sm\"\n variant=\"flat\"\n className={s.flagToggle}\n ariaLabel={ariaLabel}\n items={flags.map(({ cell, field }) => ({\n value: cell.key,\n label: field.glyph ?? cell.leaf.name.slice(0, 1),\n ariaLabel: cell.ariaLabel,\n tooltip: drawnGlyph(field.glyph) === undefined ? undefined : cell.leaf.name,\n }))}\n value={on}\n // Each flag owns its own path, so only the segment that moved is\n // written — committing the run would write two fields nobody touched,\n // and inside an object leaf would fabricate values for the other two.\n mixedValues={flags.filter((f) => f.field.mixed).map((f) => f.cell.key)}\n onChange={(next) => {\n for (const { cell, field } of flags) {\n const now = next.includes(cell.key);\n if (now !== on.includes(cell.key)) field.onChange(now);\n }\n }}\n />\n );\n}\n\nexport function renderBuiltin(\n ctx: PropertyRenderContext,\n ariaLabel: string,\n renderers?: Record<string, PropertyRenderer>,\n /** Identifies the current selection, so a control holding per-selection\n * scratch (the paint kind memory) is remounted rather than carried over. */\n selectionKey = '',\n /** True for a field of an object leaf. Such a field writes itself into its\n * parent, so it can only hold values its parent's type permits — which is\n * what rules \"none\" out for a nested paint. */\n nested = false,\n): ReactNode {\n const { pref, value, mixed, unset, setValue } = ctx;\n if (pref.kind === 'object') {\n // One value with fields hanging off it. Each child writes the parent\n // whole, so a field is never set on a half-built object.\n return <ObjectLeaf ctx={ctx} renderers={renderers} selectionKey={selectionKey} />;\n }\n const field = prefFieldProps(pref, {\n value,\n mixed,\n unset,\n siblings: ctx.siblings,\n setValue,\n ...perNodeState(ctx),\n // The font pickers read the rest of the font from the leaves beside\n // them: the family's substitution probe runs at the weight and style\n // that will paint, and a weight lists what its family has. A mixed\n // selection has no single one; the probe falls back to 400/normal there.\n fontVariant: {\n weight: ctx.valueAt(siblingPath(ctx.path, 'fontWeight')).value,\n style: ctx.valueAt(siblingPath(ctx.path, 'fontStyle')).value,\n },\n fontFamily: ctx.valueAt(siblingPath(ctx.path, 'fontFamily')).value,\n });\n if (field === null) {\n return <span className={s.unrenderable}>({pref.kind}: no renderer)</span>;\n }\n switch (field.kind) {\n case 'boolean':\n // A panel's flag is a switch unless it asks otherwise.\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n control={field.control ?? 'switch'}\n className={field.control === 'toggle' ? s.flagToggle : undefined}\n />\n );\n case 'number':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n className={field.control === 'slider' ? s.slider : s.number}\n // A typed number writes once it is entered, as text does; a slider\n // writes as it moves.\n onInput={field.control === 'slider' ? field.onInput : settledOnly}\n />\n );\n case 'string':\n // Settled values only: a live write per keystroke would be one undo\n // step per character.\n return <PropertyControl {...field} name={ariaLabel} onInput={settledOnly} />;\n case 'enum':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n // A clearable toggle is a set of exclusive flags, and sits beside\n // other flags sized the way they are.\n className={\n field.control === 'toggle'\n ? field.onClear ? s.flagToggle : s.toggle\n : field.control === 'radio' ? undefined : s.select\n }\n />\n );\n case 'font-family':\n case 'font-weight':\n return <PropertyControl {...field} name={ariaLabel} className={s.select} />;\n case 'paint':\n return (\n <PropertyControl\n {...field}\n name={ariaLabel}\n // Per-kind switch memory is scratch for one selection; carrying it\n // across would recall the previous node's gradient.\n key={selectionKey}\n control=\"inline\"\n // A nested paint writes one field of its parent, and no kit paint\n // field is optional — `Stroke.paint` is required. Removing the\n // whole parent is a different edit than repainting it, so the\n // control must not offer one as the other.\n allowNone={!nested}\n />\n );\n case 'color':\n // One write per gesture, not one per step of the picker.\n return <PropertyControl {...field} name={ariaLabel} onInput={settledOnly} />;\n }\n}\n\nconst settledOnly = (): void => {};\n\n/** The dotted path of the leaf `key` beside the one at `path`. */\nfunction siblingPath(path: string, key: string): string {\n const dot = path.lastIndexOf('.');\n return dot < 0 ? key : `${path.slice(0, dot + 1)}${key}`;\n}\n\n/** What an `encoding` needs to read and write each node against its own\n * object — nothing for a leaf that is not a field of one. */\nfunction perNodeState(ctx: PropertyRenderContext): Pick<PrefFieldState, 'perNode' | 'update'> {\n if (ctx.eachSiblings === undefined) return {};\n const siblings = ctx.eachSiblings;\n return {\n perNode: ctx.each.map((value, i) => ({ value, siblings: siblings[i] })),\n update: ctx.update,\n };\n}\n\n/**\n * Renders an object leaf: a titled block whose rows are the object's own\n * fields. A child's edit commits the parent object, never the child's path —\n * writing into a path whose value is not an object yet would corrupt it.\n */\n/** One labeled row inside an object leaf; `pair` merges adjacent fields into\n * it, exactly as the section rows merge theirs. */\ninterface ObjectRow {\n key: string;\n pair?: string;\n label: string;\n title?: string;\n /** Every field in the row brought its own chrome, so the row drops the\n * label column and spans the block. One unlabeled field in an otherwise\n * labeled pair would leave the row named after half of itself. */\n block: boolean;\n controls: ReactNode[];\n}\n\n/**\n * Renders an object leaf: the object's own fields, as rows. A child's edit\n * commits the parent object, never the child's path — writing into a path\n * whose value is not an object yet would corrupt it.\n */\nfunction ObjectLeaf({\n ctx,\n renderers,\n selectionKey = '',\n}: {\n ctx: PropertyRenderContext;\n renderers?: Record<string, PropertyRenderer>;\n selectionKey?: string;\n}): ReactNode {\n const pref = ctx.pref as ToolPrefObject;\n const held = typeof ctx.value === 'object' && ctx.value !== null\n ? (ctx.value as Record<string, unknown>)\n : undefined;\n const eachObject = ctx.each.map((v) =>\n typeof v === 'object' && v !== null ? (v as Record<string, unknown>) : undefined,\n );\n\n // A value whose fields are entirely grouped is titled by those groups — its\n // own heading would stack onto the first one and name nothing new.\n const allGrouped = Object.values(pref.children).every((child) => !('kind' in child));\n\n // `indent` is false when nothing visible sits above these rows: depth is\n // drawn only where a label marks it.\n const rowsOf = (\n children: Record<string, ToolPrefLeaf | ToolPrefGroup>,\n indent: boolean,\n ): ReactNode[] => {\n const out: (ReactNode | ObjectRow)[] = [];\n const isRow = (v: ReactNode | ObjectRow): v is ObjectRow =>\n typeof v === 'object' && v !== null && 'controls' in v;\n\n // Cells accumulate unrendered so a run of adjacent same-`pair` flags can\n // be seen as a run before any of them draws; a group heading between two\n // fields ends the run, exactly as it ends a paired row.\n let pending: LeafCell[] = [];\n const flush = (): void => {\n for (const { key, leaf, block, content } of renderCells(pending, renderers)) {\n const prev = out[out.length - 1];\n if (leaf.pair !== undefined && isRow(prev) && prev.pair === leaf.pair) {\n prev.controls.push(<Fragment key={key}>{content}</Fragment>);\n prev.block &&= block;\n continue;\n }\n out.push({\n key,\n pair: leaf.pair,\n label: leaf.pair ?? leaf.name,\n title: leaf.description,\n block,\n controls: [<Fragment key={key}>{content}</Fragment>],\n });\n }\n pending = [];\n };\n\n for (const [key, child] of Object.entries(children)) {\n // A group among the children organises the fields under a heading\n // without contributing to the path — the rule group keys follow at the\n // top level.\n if (!('kind' in child)) {\n const labeled = child.name !== '';\n const inner = rowsOf(child.children, labeled);\n if (inner.length === 0) continue;\n flush();\n out.push(\n <div key={`group:${key}`} className={labeled && indent ? s.objectGroup : undefined}>\n {labeled && <h5 className={s.sectionTitle}>{child.name}</h5>}\n {inner}\n </div>,\n );\n continue;\n }\n const childPath = `${ctx.path}.${key}`;\n // Nodes whose objects differ still agree or disagree field by field: two\n // styles at different sizes are both upright, and saying \"mixed\" for\n // every field of them hides that.\n const field = ctx.mixed ? ctx.valueAt(childPath) : { value: held?.[key], mixed: false };\n // Each node's own object, with this one field changed. Deriving it per\n // node is what keeps a multi-selection edit from writing one node's\n // object over all the others.\n const update = (fn: (prev: unknown, siblings?: Record<string, unknown>) => unknown): void =>\n ctx.update((prev) => {\n // The node holds no object yet, so writing one field has to\n // materialize the rest: the leaf's `default` is what a complete\n // value looks like. Starting from `{}` instead committed the one\n // field on its own — a `data.stroke` of `{ width: 2 }` with no\n // `paint`, which the type forbids and the painter threw on, taking\n // the whole frame and the visible document with it.\n const base: Record<string, unknown> =\n typeof prev === 'object' && prev !== null\n ? (prev as Record<string, unknown>)\n : (pref.fromScalar?.(prev)\n ?? (typeof pref.default === 'object' && pref.default !== null\n ? { ...(pref.default as Record<string, unknown>) }\n : {}));\n const v = fn(base[key], base);\n if (v === undefined) {\n // A field written as absent is absent — leaving the key holding\n // `undefined` says the object has a dash of nothing.\n const { [key]: _dropped, ...rest } = base;\n return rest;\n }\n return { ...base, [key]: v };\n });\n const childCtx: PropertyRenderContext = {\n path: childPath,\n pref: child,\n value: field.mixed ? undefined : field.value,\n mixed: field.mixed,\n // A field of an object the node does not hold is unset, and so is one\n // the object omits — `data.stroke` absent leaves every stroke field\n // with nothing behind it, not with the schema's defaults.\n unset: !field.mixed && field.value === undefined,\n siblings: held,\n each: eachObject.map((o) => o?.[key]),\n eachSiblings: eachObject,\n valueAt: ctx.valueAt,\n selectionKey: ctx.selectionKey,\n update,\n setValue: (v) => update(() => v),\n };\n pending.push({\n key: childPath,\n leaf: child,\n ctx: childCtx,\n ariaLabel: child.name,\n render: () => {\n const custom = renderers?.[childPath] ?? renderers?.[child.kind];\n const rendered = custom\n ? custom(childCtx)\n : renderBuiltin(childCtx, child.name, renderers, selectionKey, true);\n if (rendered == null) return null;\n // Neither a paired row nor a block one gives a field its own label\n // column, so the glyph is all that names it on screen. The control\n // already carries `name` as its accessible name, which leaves the\n // glyph decorative.\n const unlabeled = child.pair !== undefined || child.block === true;\n const glyph = unlabeled && child.icon && child.icon in ICON_PATHS;\n if (!glyph) return rendered;\n return (\n <span className={s.namedField}>\n <Icon name={child.icon as IconName} size={14} />\n {rendered}\n </span>\n );\n },\n });\n }\n flush();\n return out.map((entry) =>\n typeof entry === 'object' && entry !== null && 'controls' in entry ? (\n entry.block ? (\n <div key={entry.key} className={`${s.rowControls} ${s.blockRow}`} title={entry.title}>\n {entry.controls}\n </div>\n ) : (\n <div key={entry.key} className={s.row}>\n <span className={s.rowLabel} title={entry.title}>{entry.label}</span>\n <span className={s.rowControls}>{entry.controls}</span>\n </div>\n )\n ) : (\n entry\n ),\n );\n };\n\n const rows = rowsOf(pref.children, !allGrouped);\n if (rows.length === 0) return null;\n return (\n <div className={s.objectLeaf}>\n {!allGrouped && <h4 className={s.sectionTitle}>{pref.name}</h4>}\n {rows}\n </div>\n );\n}\n","// Pure selection→panel derivations for SelectionPanel. Kept free of\n// React so intersection/aggregation semantics are unit-testable.\n//\n// Path convention (see core `NodePropertiesEntry`): a leaf's OWN KEY in\n// the schema is its node path — a dotted path of any depth rooted at\n// `pose` or `data` (`pose.x`, `data.fill`, `data.style.fontSize`). Group\n// keys are organizational only.\n\nimport {\n MIXED,\n type Mixed,\n type NodePropertiesEntry,\n type NodeRoutingEntry,\n type SceneNode,\n type ToolPrefGroup,\n type ToolPrefLeaf,\n} from '@weasel-js/core';\n\n// Re-exported under its existing public name here: the kit-wide \"these\n// values disagree\" sentinel (`@weasel-js/core`'s `MIXED`) also covers\n// multi-node aggregation, so this panel doesn't get its own symbol.\nexport { MIXED };\nexport type { Mixed };\n\nexport type AnyNode = SceneNode<unknown, string, unknown>;\n\n/**\n * One editable property in a {@link SelectionPanel}, paired with the dotted\n * node path it reads and writes.\n */\nexport interface PanelLeaf {\n /** Dotted node path — the leaf's key in the schema. */\n path: string;\n leaf: ToolPrefLeaf;\n}\n\n/**\n * One labeled row of a panel section. Holds several leaves when the schema\n * paired them.\n */\nexport interface PanelRow {\n label: string;\n leaves: PanelLeaf[];\n}\n\n/**\n * A titled block of rows, derived from one top-level group of a properties\n * schema. `key` is that group's schema key.\n */\nexport interface PanelSection {\n key: string;\n name: string;\n rows: PanelRow[];\n}\n\n/** Derive a node's kind: containers are `'group'`; leaves classify their\n * `data` through the routing entries (first match wins, `'unknown'`\n * otherwise) — same semantics as core's `NodeRouting.classify`. */\nexport function classifyKind(\n node: AnyNode,\n routing: readonly NodeRoutingEntry[],\n): string {\n if (node.kind === 'container') return 'group';\n for (const entry of routing) {\n if (entry.matches(node.data)) return entry.name;\n }\n return 'unknown';\n}\n\nexport function isGroup(n: ToolPrefLeaf | ToolPrefGroup): n is ToolPrefGroup {\n return !('kind' in n);\n}\n\n/** Flatten a schema to sections of leaves. Nested groups fold into their\n * top-level section; top-level leaves get an untitled leading section. */\nfunction flatten(schema: ToolPrefGroup): PanelSection[] {\n const sections: PanelSection[] = [];\n const untitled: PanelLeaf[] = [];\n\n const collect = (group: ToolPrefGroup, into: PanelLeaf[]): void => {\n for (const [key, child] of Object.entries(group.children)) {\n if (isGroup(child)) collect(child, into);\n else into.push({ path: key, leaf: child });\n }\n };\n\n for (const [key, child] of Object.entries(schema.children)) {\n if (isGroup(child)) {\n const leaves: PanelLeaf[] = [];\n collect(child, leaves);\n sections.push({ key, name: child.name, rows: pairRows(leaves) });\n } else {\n untitled.push({ path: key, leaf: child });\n }\n }\n if (untitled.length > 0) {\n sections.unshift({ key: '', name: '', rows: pairRows(untitled) });\n }\n return sections;\n}\n\n/** Merge consecutive leaves sharing a `pair` id into one labeled row. */\nexport function pairRows(leaves: readonly PanelLeaf[]): PanelRow[] {\n const rows: PanelRow[] = [];\n for (const item of leaves) {\n const pair = item.leaf.pair;\n const prev = rows[rows.length - 1];\n if (pair !== undefined && prev !== undefined && prev.label === pair) {\n prev.leaves.push(item);\n } else {\n rows.push({ label: pair ?? item.leaf.name, leaves: [item] });\n }\n }\n return rows;\n}\n\n/**\n * The schema the panel shows for a set of kinds: one kind → its full\n * schema; several → the intersection by (path, leaf kind), shaped by the\n * first kind's section/row layout. Kinds without a registered schema\n * contribute nothing, so their presence collapses the intersection.\n */\nexport function effectiveSections(\n kinds: readonly string[],\n entries: readonly NodePropertiesEntry[],\n): PanelSection[] {\n const uniq = [...new Set(kinds)];\n if (uniq.length === 0) return [];\n const byName = new Map(entries.map((e) => [e.name, e]));\n const schemas = uniq.map((k) => byName.get(k)?.schema);\n if (schemas.some((s) => s === undefined)) return [];\n const [first, ...rest] = schemas as ToolPrefGroup[];\n\n const flatFirst = flatten(first);\n if (rest.length === 0) return flatFirst;\n\n // '\\0' delimiter: path and kind strings are unconstrained, so a\n // printable separator could collide with real content.\n const leafKey = (l: PanelLeaf): string => `${l.path}\\0${l.leaf.kind}`;\n const restKeys = rest.map(\n (schema) =>\n new Set(\n flatten(schema)\n .flatMap((s) => s.rows)\n .flatMap((r) => r.leaves)\n .map(leafKey),\n ),\n );\n const keep = (l: PanelLeaf): boolean =>\n restKeys.every((set) => set.has(leafKey(l)));\n\n return flatFirst\n .map((section) => ({\n ...section,\n rows: section.rows\n .map((row) => ({ ...row, leaves: row.leaves.filter(keep) }))\n .filter((row) => row.leaves.length > 0),\n }))\n .filter((section) => section.rows.length > 0);\n}\n\n/** Read a node value at a dotted path of any depth (`pose.x`,\n * `data.style.fontSize`, `data.style.fill.color`). Returns `undefined` if\n * the path is dotless (mirrors `commit`'s no-op on a malformed schema\n * key) or if any intermediate segment is missing or not an object. */\nexport function nodeValueAt(node: AnyNode, path: string): unknown {\n const segments = path.split('.');\n if (segments.length < 2) return undefined;\n const head = segments[0];\n let cursor: unknown = head === 'pose' ? node.pose : head === 'data' ? node.data : undefined;\n for (let i = 1; i < segments.length; i++) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[segments[i]];\n }\n return cursor;\n}\n\n/** Immutably set `value` at a dotted path within `root`, cloning each level\n * on the way down so React sees new object identities. Arrays stay arrays\n * (an array intermediate is shallow-copied, not flattened into `{0: ...}`)\n * — this is the write-side twin of `nodeValueAt`, so a schema author who\n * gets a working read gets a working write. */\nexport function setAtPath(root: object, segments: readonly string[], value: unknown): object {\n const [head, ...rest] = segments;\n if (rest.length === 0) {\n return Array.isArray(root)\n ? Object.assign([...root], { [head]: value })\n : { ...root, [head]: value };\n }\n const child = (root as Record<string, unknown>)[head];\n const childObj = Array.isArray(child)\n ? [...child]\n : child != null && typeof child === 'object'\n ? { ...child }\n : {};\n const nested = setAtPath(childObj, rest, value);\n return Array.isArray(root)\n ? Object.assign([...root], { [head]: nested })\n : { ...root, [head]: nested };\n}\n\n/** Aggregate a path across nodes: the shared value, or `MIXED`. */\nexport function aggregateValue(\n nodes: readonly AnyNode[],\n path: string,\n): unknown | Mixed {\n let value: unknown;\n let first = true;\n for (const node of nodes) {\n const v = nodeValueAt(node, path);\n if (first) {\n value = v;\n first = false;\n } else if (!Object.is(v, value)) {\n return MIXED;\n }\n }\n return value;\n}\n\n/** `'rect ×2 · text'` — kinds in first-seen order with counts. */\nexport function kindBreakdown(kinds: readonly string[]): string {\n const counts = new Map<string, number>();\n for (const k of kinds) counts.set(k, (counts.get(k) ?? 0) + 1);\n return [...counts.entries()]\n .map(([k, n]) => (n > 1 ? `${k} ×${n}` : k))\n .join(' · ');\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "../../chunks/LayerList-
|
|
1
|
+
import { n as e, r as t, t as n } from "../../chunks/LayerList-BqeWFuGc.js";
|
|
2
2
|
export { e as LayerList, t as moveLayers, n as useSceneLayerList };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "../../chunks/prefField-BE_Ytr2H.js";
|
|
2
|
-
import { i as s, n as c, r as l, t as u } from "../../chunks/Prefs-
|
|
2
|
+
import { i as s, n as c, r as l, t as u } from "../../chunks/Prefs-BnVEyfWO.js";
|
|
3
3
|
export { u as PrefsDialog, c as PrefsForm, s as PrefsPane, l as PrefsRail, n as filterPrefSubtree, i as isPrefLeaf, t as prefDisplayBounds, o as prefFieldProps, e as prefRailItems, r as prefValueAtPath, a as visiblePrefSubtree };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as e, n as t, r as n, t as r } from "../../chunks/Properties-
|
|
2
|
-
import { a as i, c as a, i as o, l as s, n as c, o as l, s as u, t as d } from "../../chunks/PropertyField-
|
|
1
|
+
import { i as e, n as t, r as n, t as r } from "../../chunks/Properties-BoG2MMHR.js";
|
|
2
|
+
import { a as i, c as a, i as o, l as s, n as c, o as l, s as u, t as d } from "../../chunks/PropertyField-PMrqBA0n.js";
|
|
3
3
|
export { e as CurveField, n as DialogRow, d as PropertyControl, c as PropertyField, t as PropertyGroup, o as PropertyHelp, i as PropertyList, l as PropertyNote, u as PropertyPanel, a as PropertyRow, s as PropertySpan, r as Subpanel };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as e, l as t, n, o as r, r as i, s as a, t as o } from "../../chunks/model-
|
|
2
|
-
import { t as s } from "../../chunks/SelectionPanel-
|
|
1
|
+
import { i as e, l as t, n, o as r, r as i, s as a, t as o } from "../../chunks/model-B6zuQg6b.js";
|
|
2
|
+
import { t as s } from "../../chunks/SelectionPanel-COZ6z9_P.js";
|
|
3
3
|
export { o as MIXED, s as SelectionPanel, n as aggregateValue, i as classifyKind, e as effectiveSections, r as kindBreakdown, a as nodeValueAt, t as setAtPath };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as e } from "../../chunks/ToolOptionsBar-
|
|
1
|
+
import { t as e } from "../../chunks/ToolOptionsBar-BJKwnemU.js";
|
|
2
2
|
export { e as ToolOptionsBar };
|
package/dist/index.js
CHANGED
|
@@ -25,9 +25,9 @@ import { a as Wt, i as Gt, n as Kt, r as qt, t as Jt } from "./chunks/CurveEdito
|
|
|
25
25
|
import { i as Yt, n as Xt, r as Zt, t as Qt } from "./chunks/createKeyframeLayer-DSVf13Su.js";
|
|
26
26
|
import { n as $t, r as en, t as tn } from "./chunks/handles-CqVANLtu.js";
|
|
27
27
|
import { t as nn } from "./chunks/snap-DRjbOiG9.js";
|
|
28
|
-
import { i as rn, n as an, r as on, t as sn } from "./chunks/Properties-
|
|
28
|
+
import { i as rn, n as an, r as on, t as sn } from "./chunks/Properties-BoG2MMHR.js";
|
|
29
29
|
import { t as cn } from "./chunks/Dialog-DKfsk5VN.js";
|
|
30
|
-
import { a as ln, c as un, i as dn, l as fn, n as pn, o as mn, s as hn, t as gn } from "./chunks/PropertyField-
|
|
30
|
+
import { a as ln, c as un, i as dn, l as fn, n as pn, o as mn, s as hn, t as gn } from "./chunks/PropertyField-PMrqBA0n.js";
|
|
31
31
|
import { a as _n, i as U, n as vn, o as yn, r as bn, t as xn } from "./chunks/RecentColorsProvider-DHcfk25u.js";
|
|
32
32
|
import { t as Sn } from "./chunks/InlineRange-DPjIsJB3.js";
|
|
33
33
|
import { t as Cn } from "./chunks/ColorField-BwlelY26.js";
|
|
@@ -48,7 +48,7 @@ import { t as qn } from "./chunks/PaintField-BdmQM53v.js";
|
|
|
48
48
|
import { n as Jn, t as Yn } from "./chunks/Field-DiX_WByH.js";
|
|
49
49
|
import { n as Xn, t as Zn } from "./chunks/RadioGroup-B70_M-gT.js";
|
|
50
50
|
import { t as Qn } from "./chunks/Switch-CSYmcM8y.js";
|
|
51
|
-
import { n as $n, r as er, t as tr } from "./chunks/LayerList-
|
|
51
|
+
import { n as $n, r as er, t as tr } from "./chunks/LayerList-BqeWFuGc.js";
|
|
52
52
|
import { t as nr } from "./chunks/ItemList-CRu9y7hL.js";
|
|
53
53
|
import { t as rr } from "./chunks/ListEditor-OQDzG4A5.js";
|
|
54
54
|
import { t as ir } from "./chunks/ColorModeControl-DaVJ-SyX.js";
|
|
@@ -56,16 +56,16 @@ import { n as ar, t as or } from "./chunks/ThemeSwitcher-DiBmMh_B.js";
|
|
|
56
56
|
import { t as sr } from "./chunks/OptionsBar-DeTAjVb7.js";
|
|
57
57
|
import { t as cr } from "./chunks/ButtonBar-CIMKPPRS.js";
|
|
58
58
|
import { a as lr, i as ur, n as dr, o as fr, r as pr, s as mr, t as hr } from "./chunks/prefField-BE_Ytr2H.js";
|
|
59
|
-
import { i as gr, l as _r, n as vr, o as yr, r as br, s as xr, t as Sr } from "./chunks/model-
|
|
60
|
-
import { t as Cr } from "./chunks/ToolOptionsBar-
|
|
59
|
+
import { i as gr, l as _r, n as vr, o as yr, r as br, s as xr, t as Sr } from "./chunks/model-B6zuQg6b.js";
|
|
60
|
+
import { t as Cr } from "./chunks/ToolOptionsBar-BJKwnemU.js";
|
|
61
61
|
import { t as wr } from "./chunks/FillStrokeSwatch-C4h3rUFM.js";
|
|
62
62
|
import { t as Tr } from "./chunks/SwatchGrid-9ZPiDmxd.js";
|
|
63
63
|
import { a as Er, i as Dr, n as Or, r as kr, t as Ar } from "./chunks/SwatchStrip-Bi9iPLoh.js";
|
|
64
64
|
import { t as jr } from "./chunks/Powerline-6wIFmDik.js";
|
|
65
65
|
import { n as Mr, t as Nr } from "./chunks/GestureRoute-BXESrHI0.js";
|
|
66
66
|
import { t as Pr } from "./chunks/FallthroughDiagram-CS1IhQaU.js";
|
|
67
|
-
import { a as Fr, i as Ir, n as Lr, o as Rr, r as zr, t as Br } from "./chunks/Prefs-
|
|
68
|
-
import { t as Vr } from "./chunks/SelectionPanel-
|
|
67
|
+
import { a as Fr, i as Ir, n as Lr, o as Rr, r as zr, t as Br } from "./chunks/Prefs-BnVEyfWO.js";
|
|
68
|
+
import { t as Vr } from "./chunks/SelectionPanel-COZ6z9_P.js";
|
|
69
69
|
import { t as Hr } from "./chunks/ResizeHandle-CbAgCQOt.js";
|
|
70
70
|
import { t as Ur } from "./chunks/Sidebar-BZ0m4afn.js";
|
|
71
71
|
import { t as Wr } from "./chunks/SidebarPanel-AXQ0jRDx.js";
|