@weasel-js/labkit 1.5.2 → 1.6.0
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/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-BchGMC2Y.d.ts} +102 -11
- package/dist/_dts/{frac-B4hFJMiN.d.ts → frac-DTRq7Xpb.d.ts} +18 -10
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-B0--8PEM.d.ts} +2 -2
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-B7y8hX1D.d.ts} +2 -2
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → usePanZoom-Bq73pwFe.d.ts} +2 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-3FI7Y4R4.js} +4 -4
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-BVL7Y574.js} +22 -5
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-H5VIV7MQ.js} +26 -22
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-EMXAIRUJ.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-EMXAIRUJ.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +50 -7
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +83 -102
- package/dist/index.js +430 -458
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +407 -204
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +298 -174
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +20 -1
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +30 -3
- package/src/controls/ControlPanel.tsx +116 -337
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.ts +11 -26
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +38 -15
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/passthrough/weasel-ui.ts +25 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +2 -1
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/auto.ts","../src/config/path.ts","../src/config/autoConfig.ts"],"names":["n"],"mappings":"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","file":"chunk-REOBGYMC.js","sourcesContent":["/**\n * Written at a config path to mean \"do not pin this — let the instrument\n * decide\". Accepted by `setConfig`, by a trial's `configSeed`, and by\n * `.initial()` in a schema. It is normalized into the trial's set of unpinned\n * paths on the way into the store and is never itself stored, so a serialized\n * trial contains no sentinel and the last pinned value survives.\n */\nexport const auto: unique symbol = Symbol('weasel.auto');\n\n/** The type of the `auto` sentinel, for widening a value parameter. */\nexport type Auto = typeof auto;\n\nexport function isAuto(value: unknown): value is Auto {\n return value === auto;\n}\n","import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';\n\n/**\n * Reading and writing a config tree by dotted path. A schema's leaves address\n * their values this way, so a leaf nested under `f.group` and a flat one are\n * reached by exactly the same call.\n */\n\n/** A plain object a config tree can be walked into. An array, a `Date`, a\n * `Map` or a class instance is a leaf value: a walk that descended into one\n * would rebuild it as a bare `{}`. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/** The value at a dotted path. `undefined` when a segment is missing or the\n * walk hits something that is not a record. */\nexport function valueAtPath(config: unknown, path: string): unknown {\n let cur = config;\n for (const segment of path.split('.')) {\n if (!isRecord(cur)) return undefined;\n cur = cur[segment];\n }\n return cur;\n}\n\n/** Whether a dotted path is actually present, which a `undefined` value is\n * not enough to tell you. */\nexport function hasConfigPath(config: unknown, path: string): boolean {\n let cur = config;\n for (const segment of path.split('.')) {\n if (!isRecord(cur) || !(segment in cur)) return false;\n cur = cur[segment];\n }\n return true;\n}\n\nfunction writeIn(node: unknown, segments: readonly string[], value: unknown): unknown {\n const [head, ...rest] = segments;\n if (head === undefined) return value;\n const base = isRecord(node) ? node : {};\n return { ...base, [head]: rest.length === 0 ? value : writeIn(base[head], rest, value) };\n}\n\n/** A copy of `config` with `value` written at a dotted path. Every record on\n * the way down is copied and the input is left alone; a missing — or\n * non-record — intermediate is replaced with the branch the path implies. */\nexport function withValueAtPath<T>(config: T, path: string, value: unknown): T {\n return writeIn(config, path.split('.'), value) as T;\n}\n\n/**\n * `stored` with every gap filled from `defaults`, down the whole tree.\n *\n * This is how a config written before its schema grew a branch still loads:\n * the branch arrives at its defaults instead of `undefined`, and a key the\n * defaults no longer mention is kept rather than dropped, so nothing is lost\n * to a schema change the author has not finished making.\n */\nexport function fillConfigDefaults<T>(stored: unknown, defaults: T): T {\n if (!isRecord(defaults)) return (stored === undefined ? defaults : stored) as T;\n const base = isRecord(stored) ? stored : {};\n const out: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(defaults)) {\n out[key] = fillConfigDefaults(base[key], value);\n }\n return out as T;\n}\n\n/** A node in a resolved schema tree, by dotted path. Structural rather than\n * `isPrefLeaf` so this stays free of a runtime import. */\nexport function schemaNodeAtPath(group: PrefGroup, path: string): PrefLeaf | PrefGroup | undefined {\n let node: PrefLeaf | PrefGroup | undefined = group;\n for (const segment of path.split('.')) {\n if (node === undefined || !('children' in node)) return undefined;\n node = node.children[segment];\n }\n return node;\n}\n","import { isPrefLeaf, type PrefGroup, type PrefLeaf } from '@weasel-js/ui';\nimport { isAuto } from './auto';\nimport { isRecord, withValueAtPath } from './path';\nimport type { ResolvedConfig } from './types';\n\ntype Resolver = (config: Record<string, unknown>) => unknown;\n\n/** Reads a labkit-only extra off a resolved leaf. `PrefLeaf` has no field for\n * these; they ride as extra keys and survive the resolve walk. */\nfunction extra<T>(leaf: PrefLeaf, key: string): T | undefined {\n return (leaf as unknown as Record<string, T | undefined>)[key];\n}\n\n/** Every leaf in a resolved schema, in schema order, with its dotted path. */\nfunction* leaves(group: PrefGroup, at = ''): Generator<[string, PrefLeaf]> {\n for (const [key, child] of Object.entries(group.children)) {\n const path = at === '' ? key : `${at}.${key}`;\n if (isPrefLeaf(child)) yield [path, child];\n else yield* leaves(child, path);\n }\n}\n\n/** The dotted paths a schema declares as starting auto — every leaf that said\n * `.initial(auto)`. This is what seeds a new trial's unpinned set. */\nexport function autoPathsOf(resolved: ResolvedConfig): string[] {\n const out: string[] = [];\n for (const [path, leaf] of leaves(resolved.group)) {\n if (extra<boolean>(leaf, 'unpinned')) out.push(path);\n }\n return out;\n}\n\n/**\n * The config an instrument reads: the stored one, with every unpinned path\n * either computed by its resolver or removed.\n *\n * Resolution is demand-driven — a resolver reading another unpinned path\n * forces that one first, whatever order the schema declared them in — so a\n * cycle is a real cycle and not an ordering accident.\n */\nexport function resolveAutoConfig<TC>(\n resolved: ResolvedConfig,\n config: TC,\n autoPaths: ReadonlySet<string>,\n): TC {\n if (autoPaths.size === 0) return config;\n\n const resolvers = new Map<string, Resolver | undefined>();\n for (const [path, leaf] of leaves(resolved.group)) {\n if (autoPaths.has(path)) resolvers.set(path, extra<Resolver>(leaf, 'autoResolve'));\n }\n if (resolvers.size === 0) return config;\n\n // Drop first, so a resolver reading a still-unresolved auto path sees\n // `undefined` rather than a stale pinned value it would silently believe.\n let out = config as unknown as Record<string, unknown>;\n for (const path of resolvers.keys()) out = dropAtPath(out, path);\n\n const inFlight = new Set<string>();\n const done = new Set<string>();\n\n const need = (path: string): void => {\n if (done.has(path)) return;\n if (inFlight.has(path)) {\n throw new Error(`[labkit] auto resolver cycle at \"${path}\"`);\n }\n const resolve = resolvers.get(path);\n if (!resolve) {\n done.add(path);\n return;\n }\n inFlight.add(path);\n try {\n // Read `out` only after the resolver has run: it forces its own\n // dependencies, and each of those replaced `out`.\n const value = resolve(demand(() => out, resolvers, need));\n out = withValueAtPath(out, path, value);\n } finally {\n inFlight.delete(path);\n }\n done.add(path);\n };\n\n for (const path of resolvers.keys()) need(path);\n return out as unknown as TC;\n}\n\n/**\n * The config a resolver reads. A top-level key that is itself unpinned is\n * forced before it is handed back, which is what makes declaration order\n * irrelevant and turns a genuine loop into a named error.\n *\n * Only top-level keys are trapped: a resolver reaching `c.grid.size` gets the\n * already-dropped branch, and a nested auto path it depends on resolves by\n * schema order alone.\n */\nfunction demand(\n read: () => Record<string, unknown>,\n resolvers: ReadonlyMap<string, Resolver | undefined>,\n need: (path: string) => void,\n): Record<string, unknown> {\n return new Proxy(read(), {\n get(_target, key) {\n if (typeof key === 'string' && resolvers.has(key)) need(key);\n return Reflect.get(read(), key);\n },\n has(_target, key) {\n return Reflect.has(read(), key);\n },\n ownKeys() {\n return Reflect.ownKeys(read());\n },\n getOwnPropertyDescriptor(_target, key) {\n return Reflect.getOwnPropertyDescriptor(read(), key);\n },\n });\n}\n\n/** A copy of `config` with the dotted path removed, copying every record on\n * the way down. A path that is not there comes back unchanged. */\nfunction dropAtPath(config: Record<string, unknown>, path: string): Record<string, unknown> {\n const [head, ...rest] = path.split('.');\n if (head === undefined || !(head in config)) return config;\n if (rest.length === 0) {\n const { [head]: _dropped, ...kept } = config;\n return kept;\n }\n const child = config[head];\n if (!isRecord(child)) return config;\n return { ...config, [head]: dropAtPath(child, rest.join('.')) };\n}\n\n/**\n * What writing `value` at `path` makes of a trial's raw config and its set of\n * unpinned paths. `auto` pins nothing and joins the set; any other value is\n * written and leaves it. Both the store and the chrome go through this, so\n * they cannot disagree about what a write meant.\n */\nexport function applyConfigWrite<TC>(\n config: TC,\n autoPaths: readonly string[],\n path: string,\n value: unknown,\n): { config: TC; autoPaths: readonly string[] } {\n if (isAuto(value)) {\n if (autoPaths.includes(path)) return { config, autoPaths };\n return { config, autoPaths: [...autoPaths, path] };\n }\n return {\n config: withValueAtPath(config, path, value),\n autoPaths: autoPaths.includes(path) ? autoPaths.filter((p) => p !== path) : autoPaths,\n };\n}\n"]}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
/** One card in a layer stack: its identity, the label shown when collapsed,
|
|
5
|
-
* and the optional select hoisted into its header. */
|
|
6
|
-
interface LayerStackItem {
|
|
7
|
-
/** Stable id used for keys, onRemove, onReorder. Numeric to match
|
|
8
|
-
* common id-from-nextId conventions; string ids also work. */
|
|
9
|
-
id: number | string;
|
|
10
|
-
/** Short kind label rendered in the header when no primary select
|
|
11
|
-
* is hoisted (e.g. "shadow", "stroke"). Omit it for items that have no
|
|
12
|
-
* kind; `label` then names the card. */
|
|
13
|
-
kind?: string;
|
|
14
|
-
/** Header text, when the item's name is not its kind. Falls back to
|
|
15
|
-
* `kind`. */
|
|
16
|
-
label?: string;
|
|
17
|
-
/** When present, hoist this select into the card header so the user
|
|
18
|
-
* can switch mode/shape without expanding. */
|
|
19
|
-
primaryValue?: string;
|
|
20
|
-
primaryOptions?: string[];
|
|
21
|
-
/** Accent CSS color used as the left border / index-badge fill. Sets
|
|
22
|
-
* --wzl-layer-stack-accent on the card, which re-binds --wzl-accent
|
|
23
|
-
* for everything inside it. */
|
|
24
|
-
accent?: string;
|
|
25
|
-
/** Optional badge text rendered before the primary control
|
|
26
|
-
* (e.g. tail index "1", "2", "3"). When omitted a drag handle
|
|
27
|
-
* glyph renders in its place. */
|
|
28
|
-
badge?: string;
|
|
29
|
-
/** Initial expanded state. Defaults to true for newly-added items. */
|
|
30
|
-
defaultExpanded?: boolean;
|
|
31
|
-
}
|
|
32
|
-
/** Props for `<LayerStack>`. */
|
|
33
|
-
interface LayerStackProps {
|
|
34
|
-
/** Heading above the list. Omit it, along with the palette, for a stack
|
|
35
|
-
* whose surroundings already name it. */
|
|
36
|
-
title?: string;
|
|
37
|
-
items: LayerStackItem[];
|
|
38
|
-
/** Kinds the user can add via the header palette. Omit both this and
|
|
39
|
-
* `onAdd` for a stack the user cannot add to. */
|
|
40
|
-
paletteKinds?: string[];
|
|
41
|
-
onAdd?: (kind: string) => void;
|
|
42
|
-
/** Omit for items the user cannot remove; the card then has no ✕. */
|
|
43
|
-
onRemove?: (id: number | string) => void;
|
|
44
|
-
onReorder: (orderedIds: Array<number | string>) => void;
|
|
45
|
-
/** Required for `primaryValue` to render as a select — without a handler
|
|
46
|
-
* the control could not be changed. */
|
|
47
|
-
onPrimaryChange?: (id: number | string, nextValue: string) => void;
|
|
48
|
-
/** Render the body controls for each item. */
|
|
49
|
-
renderBody: (item: LayerStackItem) => ReactNode;
|
|
50
|
-
/** Shown in place of the list when `items` is empty. Defaults to a line
|
|
51
|
-
* that points at the palette, or a bare one when there is no palette. */
|
|
52
|
-
emptyLabel?: ReactNode;
|
|
53
|
-
/** Hide the title + palette row (used when an outer wrap renders its
|
|
54
|
-
* own head — see speech-balloons Tails panel). */
|
|
55
|
-
hideHead?: boolean;
|
|
56
|
-
/** Appended to the root element's class list. The module's own class names
|
|
57
|
-
* are hashed, so this is the supported way to reach the stack from a
|
|
58
|
-
* consumer stylesheet. */
|
|
59
|
-
className?: string;
|
|
60
|
-
}
|
|
61
|
-
/** A drag-reorderable stack of expandable cards, with a palette in the header
|
|
62
|
-
* for adding more. The body of each card is the caller's to render. */
|
|
63
|
-
declare function LayerStack({ className, title, items, paletteKinds, onAdd, onRemove, onReorder, onPrimaryChange, renderBody, emptyLabel, hideHead, }: LayerStackProps): react_jsx_runtime.JSX.Element;
|
|
64
|
-
|
|
65
|
-
export { LayerStack };
|
|
66
|
-
export type { LayerStackItem, LayerStackProps };
|
package/dist/ui/layers/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/src/lab/useSystemMode.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import type { LabMode } from '../state/types';
|
|
3
|
-
|
|
4
|
-
function query(): MediaQueryList | null {
|
|
5
|
-
if (typeof window === 'undefined' || !window.matchMedia) return null;
|
|
6
|
-
return window.matchMedia('(prefers-color-scheme: light)');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Resolve a `LabMode` to the mode the theme resolver understands.
|
|
11
|
-
*
|
|
12
|
-
* `auto` is not a mode — it means "read the OS", live. An explicit choice
|
|
13
|
-
* opts out of the subscription entirely.
|
|
14
|
-
*/
|
|
15
|
-
export function useResolvedMode(mode: LabMode): 'light' | 'dark' {
|
|
16
|
-
const [system, setSystem] = useState<'light' | 'dark'>(() =>
|
|
17
|
-
query()?.matches ? 'light' : 'dark',
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (mode !== 'auto') return;
|
|
22
|
-
const mq = query();
|
|
23
|
-
if (!mq) return;
|
|
24
|
-
const onChange = (e: MediaQueryListEvent) => setSystem(e.matches ? 'light' : 'dark');
|
|
25
|
-
mq.addEventListener('change', onChange);
|
|
26
|
-
return () => mq.removeEventListener('change', onChange);
|
|
27
|
-
}, [mode]);
|
|
28
|
-
|
|
29
|
-
return mode === 'auto' ? system : mode;
|
|
30
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// No padding of its own: whatever mounts the list insets it. The sidebar
|
|
2
|
-
// section body already does, and a second inset here put the grip 22px off a
|
|
3
|
-
// 161px-wide sidebar's edge.
|
|
4
|
-
.lk-layer-list {
|
|
5
|
-
display: flex;
|
|
6
|
-
flex-direction: column;
|
|
7
|
-
gap: 1px;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.lk-layer-list__empty {
|
|
11
|
-
color: var(--wzl-fg-muted);
|
|
12
|
-
font-size: var(--wzl-font-size-sm);
|
|
13
|
-
font-style: italic;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.lk-layer-list__row {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
gap: var(--wzl-space-xs);
|
|
20
|
-
padding: 1px 2px;
|
|
21
|
-
border-radius: var(--wzl-radius-sm);
|
|
22
|
-
user-select: none;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.lk-layer-list__row:hover {
|
|
26
|
-
background: var(--wzl-surface-hover);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.lk-layer-list__row--dragging {
|
|
30
|
-
background: var(--wzl-surface-pressed);
|
|
31
|
-
opacity: 0.7;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.lk-layer-list__row--pinned {
|
|
35
|
-
opacity: 0.7;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Prefixed with `.lk-root` to outrank base.less's `:where(button)` default,
|
|
39
|
-
// which would otherwise wrap this grip in a 24px chrome box with a border, an
|
|
40
|
-
// elevated fill and 12px of side padding — around a glyph a third that size.
|
|
41
|
-
.lk-root .lk-layer-list__handle {
|
|
42
|
-
display: inline-flex;
|
|
43
|
-
align-items: center;
|
|
44
|
-
height: auto;
|
|
45
|
-
// Transparent, so the grab target is comfortable while nothing but the dots
|
|
46
|
-
// is drawn. The negative margin keeps it from widening the row.
|
|
47
|
-
padding: 4px;
|
|
48
|
-
margin: -4px;
|
|
49
|
-
border: 0;
|
|
50
|
-
border-radius: 0;
|
|
51
|
-
background: transparent;
|
|
52
|
-
backdrop-filter: none;
|
|
53
|
-
-webkit-backdrop-filter: none;
|
|
54
|
-
cursor: grab;
|
|
55
|
-
color: var(--wzl-fg-muted);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.lk-root .lk-layer-list__handle:hover {
|
|
59
|
-
color: var(--wzl-fg);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.lk-layer-list__handle:active {
|
|
63
|
-
cursor: grabbing;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.lk-layer-list__lock {
|
|
67
|
-
color: var(--wzl-fg-muted);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.lk-layer-list__label {
|
|
71
|
-
flex: 1;
|
|
72
|
-
font-size: var(--wzl-font-size-sm);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.lk-layer-list input[type='checkbox'] {
|
|
76
|
-
accent-color: var(--wzl-accent);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.lk-layer-list__children {
|
|
80
|
-
display: flex;
|
|
81
|
-
flex-direction: column;
|
|
82
|
-
gap: 1px;
|
|
83
|
-
// Indents by the twisty column, so a child's own twisty starts where its
|
|
84
|
-
// parent's label does.
|
|
85
|
-
padding-left: var(--wzl-space-md);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Holds the twisty column open on rows that have nothing to expand, so labels
|
|
89
|
-
// line up down the level.
|
|
90
|
-
.lk-layer-list__twisty-gap {
|
|
91
|
-
flex: 0 0 auto;
|
|
92
|
-
width: var(--wzl-disclosure-target, var(--wzl-control-h-sm));
|
|
93
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { LayerList, type LayerTreeNode } from './LayerList';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof LayerList> = {
|
|
6
|
-
title: 'labkit/Layers/LayerList',
|
|
7
|
-
component: LayerList,
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
type Story = StoryObj<typeof LayerList>;
|
|
11
|
-
|
|
12
|
-
const FLAT: LayerTreeNode[] = [
|
|
13
|
-
{ id: 'plants', label: 'Plants' },
|
|
14
|
-
{ id: 'grid', label: 'Grid' },
|
|
15
|
-
{ id: 'legend', label: 'Legend', alwaysOn: true },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const NESTED: LayerTreeNode[] = [
|
|
19
|
-
{
|
|
20
|
-
id: 'ingest',
|
|
21
|
-
label: 'Ingest',
|
|
22
|
-
children: [
|
|
23
|
-
{ id: 'ingest.fetch', label: 'Fetch' },
|
|
24
|
-
{ id: 'ingest.parse', label: 'Parse' },
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 'solve',
|
|
29
|
-
label: 'Solve',
|
|
30
|
-
defaultCollapsed: true,
|
|
31
|
-
children: [
|
|
32
|
-
{ id: 'solve.seed', label: 'Seed' },
|
|
33
|
-
{
|
|
34
|
-
id: 'solve.relax',
|
|
35
|
-
label: 'Relax',
|
|
36
|
-
children: [
|
|
37
|
-
{ id: 'solve.relax.sweep', label: 'Sweep' },
|
|
38
|
-
{ id: 'solve.relax.check', label: 'Check' },
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
{ id: 'report', label: 'Report', alwaysOn: true },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
function Demo({ seed }: { seed: LayerTreeNode[] }) {
|
|
47
|
-
const [layers, setLayers] = useState(seed);
|
|
48
|
-
const [visibility, setVisibility] = useState<Record<string, boolean>>({});
|
|
49
|
-
return (
|
|
50
|
-
<div style={{ width: 220 }}>
|
|
51
|
-
<LayerList
|
|
52
|
-
layers={layers}
|
|
53
|
-
visibility={visibility}
|
|
54
|
-
onReorder={setLayers}
|
|
55
|
-
onToggle={(id, visible) => setVisibility((v) => ({ ...v, [id]: visible }))}
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** No node has children, so no row spends width on a twisty column. */
|
|
62
|
-
export const Flat: Story = { render: () => <Demo seed={FLAT} /> };
|
|
63
|
-
|
|
64
|
-
/** Dragging a row reorders it among its own siblings — a drag never moves a
|
|
65
|
-
* layer to a different parent. */
|
|
66
|
-
export const Tree: Story = { render: () => <Demo seed={NESTED} /> };
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type { LayerDescriptor } from '../instrument/types';
|
|
4
|
-
import { LayerList, type LayerListProps, type LayerTreeNode } from './LayerList';
|
|
5
|
-
|
|
6
|
-
describe('<LayerList>', () => {
|
|
7
|
-
it('renders one row per layer', () => {
|
|
8
|
-
const layers: LayerDescriptor[] = [
|
|
9
|
-
{ id: 'a', label: 'A' },
|
|
10
|
-
{ id: 'b', label: 'B' },
|
|
11
|
-
];
|
|
12
|
-
const { container } = render(
|
|
13
|
-
<LayerList
|
|
14
|
-
layers={layers}
|
|
15
|
-
visibility={{ a: true, b: true }}
|
|
16
|
-
onReorder={vi.fn()}
|
|
17
|
-
onToggle={vi.fn()}
|
|
18
|
-
/>,
|
|
19
|
-
);
|
|
20
|
-
expect(container.querySelectorAll('.lk-layer-list__row')).toHaveLength(2);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('renders empty state when no layers', () => {
|
|
24
|
-
render(<LayerList layers={[]} visibility={{}} onReorder={vi.fn()} onToggle={vi.fn()} />);
|
|
25
|
-
expect(screen.getByText('No layers')).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('alwaysOn rows have no checkbox and show lock icon', () => {
|
|
29
|
-
const layers: LayerDescriptor[] = [{ id: 'a', label: 'A', alwaysOn: true }];
|
|
30
|
-
render(<LayerList layers={layers} visibility={{}} onReorder={vi.fn()} onToggle={vi.fn()} />);
|
|
31
|
-
expect(screen.queryByLabelText('Toggle A')).toBeNull();
|
|
32
|
-
expect(screen.getByLabelText('Always on')).toBeInTheDocument();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('onToggle fires on checkbox click', () => {
|
|
36
|
-
const onToggle = vi.fn();
|
|
37
|
-
const layers: LayerDescriptor[] = [{ id: 'a', label: 'A' }];
|
|
38
|
-
render(
|
|
39
|
-
<LayerList
|
|
40
|
-
layers={layers}
|
|
41
|
-
visibility={{ a: true }}
|
|
42
|
-
onReorder={vi.fn()}
|
|
43
|
-
onToggle={onToggle}
|
|
44
|
-
/>,
|
|
45
|
-
);
|
|
46
|
-
fireEvent.click(screen.getByLabelText('Toggle A'));
|
|
47
|
-
expect(onToggle).toHaveBeenCalledWith('a', false);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const twoLayers: LayerDescriptor[] = [
|
|
52
|
-
{ id: 'a', label: 'A' },
|
|
53
|
-
{ id: 'b', label: 'B' },
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function dispatchOn(target: EventTarget, type: string, init: PointerEventInit) {
|
|
57
|
-
act(() => {
|
|
58
|
-
target.dispatchEvent(new PointerEvent(type, { pointerId: 1, bubbles: true, ...init }));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** jsdom measures every row at zero height, so `startDrag` falls back to a
|
|
63
|
-
* pitch of 1: one pixel of travel is one row. */
|
|
64
|
-
function renderDraggable(onReorder = vi.fn()) {
|
|
65
|
-
const result = render(
|
|
66
|
-
<LayerList
|
|
67
|
-
layers={twoLayers}
|
|
68
|
-
visibility={{ a: true, b: true }}
|
|
69
|
-
onReorder={onReorder}
|
|
70
|
-
onToggle={vi.fn()}
|
|
71
|
-
/>,
|
|
72
|
-
);
|
|
73
|
-
return { ...result, onReorder, handle: screen.getByLabelText('Reorder A') };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Index of the row the drag currently marks, or -1 when nothing is dragging. */
|
|
77
|
-
function draggingRow(container: HTMLElement): number {
|
|
78
|
-
const rows = Array.from(container.querySelectorAll('.lk-layer-list__row'));
|
|
79
|
-
return rows.findIndex((r) => r.classList.contains('lk-layer-list__row--dragging'));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe('<LayerList> reordering', () => {
|
|
83
|
-
it('reorders from a release that lands off the handle', () => {
|
|
84
|
-
const { onReorder, handle } = renderDraggable();
|
|
85
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
86
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
87
|
-
dispatchOn(document, 'pointerup', { clientY: 1 });
|
|
88
|
-
expect(onReorder).toHaveBeenCalledTimes(1);
|
|
89
|
-
expect(onReorder.mock.calls[0][0].map((l: LayerDescriptor) => l.id)).toEqual(['b', 'a']);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// The other half of the rule — a detached origin does cancel — is owned by
|
|
93
|
-
// pointerSession.test.ts, which exercises it without React in the way.
|
|
94
|
-
it('keeps dragging when capture is lost and the handle is still in the document', () => {
|
|
95
|
-
const { onReorder, handle, container } = renderDraggable();
|
|
96
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
97
|
-
// Chrome releases capture a beat before it delivers pointerup, so ending
|
|
98
|
-
// here throws away a release that has already been dispatched.
|
|
99
|
-
dispatchOn(handle, 'lostpointercapture', {});
|
|
100
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
101
|
-
expect(draggingRow(container)).toBe(1);
|
|
102
|
-
dispatchOn(document, 'pointerup', { clientY: 1 });
|
|
103
|
-
expect(onReorder).toHaveBeenCalledTimes(1);
|
|
104
|
-
expect(onReorder.mock.calls[0][0].map((l: LayerDescriptor) => l.id)).toEqual(['b', 'a']);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('reads a move with nothing held as the release that never arrived', () => {
|
|
108
|
-
const { onReorder, handle } = renderDraggable();
|
|
109
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
110
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
111
|
-
dispatchOn(document, 'pointermove', { buttons: 0, clientY: 40 });
|
|
112
|
-
expect(onReorder).toHaveBeenCalledTimes(1);
|
|
113
|
-
expect(onReorder.mock.calls[0][0].map((l: LayerDescriptor) => l.id)).toEqual(['b', 'a']);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('drops its listeners when the list unmounts mid-drag', () => {
|
|
117
|
-
const { onReorder, handle, unmount } = renderDraggable();
|
|
118
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
119
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
120
|
-
unmount();
|
|
121
|
-
dispatchOn(document, 'pointerup', { clientY: 1 });
|
|
122
|
-
expect(onReorder).not.toHaveBeenCalled();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('does not capture a second pointer into the same drag', () => {
|
|
126
|
-
const { onReorder, handle } = renderDraggable();
|
|
127
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
128
|
-
dispatchOn(document, 'pointermove', { pointerId: 2, buttons: 1, clientY: 40 });
|
|
129
|
-
dispatchOn(document, 'pointerup', { pointerId: 1, clientY: 0 });
|
|
130
|
-
expect(onReorder).not.toHaveBeenCalled();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const tree: LayerTreeNode[] = [
|
|
135
|
-
{
|
|
136
|
-
id: 'a',
|
|
137
|
-
label: 'A',
|
|
138
|
-
children: [
|
|
139
|
-
{ id: 'a1', label: 'A1' },
|
|
140
|
-
{ id: 'a2', label: 'A2' },
|
|
141
|
-
],
|
|
142
|
-
},
|
|
143
|
-
{ id: 'b', label: 'B' },
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
function renderTree(props: Partial<LayerListProps> = {}) {
|
|
147
|
-
return render(
|
|
148
|
-
<LayerList layers={tree} visibility={{}} onReorder={vi.fn()} onToggle={vi.fn()} {...props} />,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
describe('<LayerList> nesting', () => {
|
|
153
|
-
it('renders a row for each descendant of an expanded node', () => {
|
|
154
|
-
const { container } = renderTree();
|
|
155
|
-
expect(container.querySelectorAll('.lk-layer-list__row')).toHaveLength(4);
|
|
156
|
-
expect(screen.getByText('A1')).toBeInTheDocument();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('hides the children of a collapsed node', () => {
|
|
160
|
-
const { container } = renderTree();
|
|
161
|
-
fireEvent.click(screen.getByLabelText('A sublayers'));
|
|
162
|
-
expect(container.querySelectorAll('.lk-layer-list__row')).toHaveLength(2);
|
|
163
|
-
expect(screen.queryByText('A1')).toBeNull();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('starts collapsed when the node says so', () => {
|
|
167
|
-
render(
|
|
168
|
-
<LayerList
|
|
169
|
-
layers={[{ ...tree[0], defaultCollapsed: true }, tree[1]]}
|
|
170
|
-
visibility={{}}
|
|
171
|
-
onReorder={vi.fn()}
|
|
172
|
-
onToggle={vi.fn()}
|
|
173
|
-
/>,
|
|
174
|
-
);
|
|
175
|
-
expect(screen.queryByText('A1')).toBeNull();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('takes collapse from collapsedIds and reports the next set', () => {
|
|
179
|
-
const onCollapsedChange = vi.fn();
|
|
180
|
-
renderTree({ collapsedIds: ['a'], onCollapsedChange });
|
|
181
|
-
expect(screen.queryByText('A1')).toBeNull();
|
|
182
|
-
fireEvent.click(screen.getByLabelText('A sublayers'));
|
|
183
|
-
expect(onCollapsedChange).toHaveBeenCalledWith([]);
|
|
184
|
-
// Controlled: the click alone does not open it.
|
|
185
|
-
expect(screen.queryByText('A1')).toBeNull();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('ties the twisty to the subtree it reveals', () => {
|
|
189
|
-
const { container } = renderTree();
|
|
190
|
-
const controls = screen.getByLabelText('A sublayers').getAttribute('aria-controls');
|
|
191
|
-
expect(controls).toBeTruthy();
|
|
192
|
-
expect(container.querySelector(`[id="${controls}"]`)).toHaveClass('lk-layer-list__children');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('spends no twisty column on a flat list', () => {
|
|
196
|
-
const { container } = render(
|
|
197
|
-
<LayerList layers={twoLayers} visibility={{}} onReorder={vi.fn()} onToggle={vi.fn()} />,
|
|
198
|
-
);
|
|
199
|
-
expect(container.querySelector('.lk-layer-list__twisty-gap')).toBeNull();
|
|
200
|
-
expect(screen.queryByLabelText(/sublayers$/)).toBeNull();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('holds the twisty column open on childless siblings', () => {
|
|
204
|
-
const { container } = renderTree();
|
|
205
|
-
expect(container.querySelectorAll('.lk-layer-list__twisty-gap')).toHaveLength(3);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('reorders within a subtree and leaves the rest of the tree alone', () => {
|
|
209
|
-
const onReorder = vi.fn();
|
|
210
|
-
renderTree({ onReorder });
|
|
211
|
-
const handle = screen.getByLabelText('Reorder A1');
|
|
212
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
213
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
214
|
-
dispatchOn(document, 'pointerup', { clientY: 1 });
|
|
215
|
-
expect(onReorder).toHaveBeenCalledTimes(1);
|
|
216
|
-
const next = onReorder.mock.calls[0][0] as LayerTreeNode[];
|
|
217
|
-
expect(next.map((l) => l.id)).toEqual(['a', 'b']);
|
|
218
|
-
expect(next[0].children?.map((l) => l.id)).toEqual(['a2', 'a1']);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('reorders the top level without disturbing a subtree', () => {
|
|
222
|
-
const onReorder = vi.fn();
|
|
223
|
-
renderTree({ onReorder });
|
|
224
|
-
const handle = screen.getByLabelText('Reorder A');
|
|
225
|
-
fireEvent.pointerDown(handle, { pointerId: 1, buttons: 1, clientY: 0 });
|
|
226
|
-
dispatchOn(document, 'pointermove', { buttons: 1, clientY: 1 });
|
|
227
|
-
dispatchOn(document, 'pointerup', { clientY: 1 });
|
|
228
|
-
const next = onReorder.mock.calls[0][0] as LayerTreeNode[];
|
|
229
|
-
expect(next.map((l) => l.id)).toEqual(['b', 'a']);
|
|
230
|
-
expect(next[1].children?.map((l) => l.id)).toEqual(['a1', 'a2']);
|
|
231
|
-
});
|
|
232
|
-
});
|