isotone 1.1.2 → 1.1.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/core.d.ts CHANGED
@@ -212,7 +212,7 @@ interface OverlayVariantStatus extends OverlayVariant {
212
212
  missing: string[];
213
213
  }
214
214
  /**
215
- * A knob: a range or colour axis the host declares in `.isotone/knobs.json`
215
+ * A knob: a range, colour or select axis the host declares in `.isotone/knobs.json`
216
216
  * next to its overlay variants. The panel renders it as a slider (or a colour
217
217
  * pick) on the routes it names, and the value reaches the page as a CSS
218
218
  * custom property on `<html>`, `--knob-<id>`, with the axis unit appended
@@ -220,8 +220,12 @@ interface OverlayVariantStatus extends OverlayVariant {
220
220
  * committed default: `var(--knob-fade-from, 0%)`. "Save as defaults" files
221
221
  * the moved knobs in the same `[set-defaults]` note as the variant picks, so
222
222
  * the agent writes the numbers into the source and the knob file.
223
+ *
224
+ * A select knob's value is its option id, which CSS cannot branch on through a
225
+ * custom property, so it also lands as `data-knob-<id>` on `<html>`. Code that
226
+ * draws for itself listens for {@link KNOBS_EVENT} on the document.
223
227
  */
224
- type KnobAxis = (RangeVariantAxis | ColorVariantAxis) & {
228
+ type KnobAxis = (RangeVariantAxis | ColorVariantAxis | SelectVariantAxis) & {
225
229
  /** Route pattern(s) the knob belongs on, like a variant's `route`; absent means every route. */
226
230
  route?: string | string[];
227
231
  };
@@ -235,7 +239,11 @@ interface OverlayVariantsList {
235
239
  }
236
240
  /** The custom property a knob writes: `--knob-<id>`. */
237
241
  declare function knobVar(id: string): string;
238
- /** The property's value: a range carries its unit, a colour is its hex. */
242
+ /** Fired on `<html>`, bubbling, when a knob's value changes. `detail` is
243
+ * `{ id, value }` with the same value the custom property holds, or `null`
244
+ * when the knob went back to its default. */
245
+ declare const KNOBS_EVENT = "isotone:knob";
246
+ /** The property's value: a range carries its unit, a colour is its hex, a select is the option id. */
239
247
  declare function knobCssValue(axis: KnobAxis, value: string): string;
240
248
  /** The knobs that belong on `route`. */
241
249
  declare function knobsFor(knobs: KnobAxis[] | undefined, route: string): KnobAxis[];
@@ -245,6 +253,10 @@ declare function applyKnobValues(knobs: KnobAxis[], values: VariantSelections, r
245
253
  setProperty(name: string, value: string): void;
246
254
  removeProperty(name: string): void;
247
255
  };
256
+ setAttribute?(name: string, value: string): void;
257
+ removeAttribute?(name: string): void;
258
+ getAttribute?(name: string): string | null;
259
+ dispatchEvent?(event: Event): boolean;
248
260
  }): void;
249
261
  /** The knobs that sit off their default, with the value the panel showed. */
250
262
  declare function knobDelta(knobs: KnobAxis[], values: VariantSelections): {
@@ -344,8 +356,25 @@ interface IterationElement {
344
356
  tag: string;
345
357
  /** Trimmed text-content snippet (may be empty for visual elements). */
346
358
  text: string;
347
- /** Best-effort CSS path for locating the element in code. */
359
+ /** CSS path to the element. A pin made in a browser matches only this element. */
348
360
  selector: string;
361
+ /** Identifying attributes: role, name, aria-label, data-*, class and so on. */
362
+ attributes?: Record<string, string>;
363
+ /** React components around the element, outermost first. */
364
+ component?: string[];
365
+ /** `file:line` of the JSX, where the host's React still records it. */
366
+ source?: string;
367
+ /** Labelled or headed ancestors, outermost first. */
368
+ within?: string[];
369
+ /** The closest ancestor's text, for an element with none of its own. */
370
+ near?: string;
371
+ /** Where it sat on the page when pinned, in CSS pixels from the document's corner. */
372
+ rect?: {
373
+ x: number;
374
+ y: number;
375
+ w: number;
376
+ h: number;
377
+ };
349
378
  }
350
379
  /** A durable reference to a selector or one of its options, not a DOM path. */
351
380
  interface VariantReference {
@@ -399,12 +428,18 @@ interface NodeLike {
399
428
  textContent?: string | null;
400
429
  /** One-based child position, when the adapter can provide it cheaply. */
401
430
  indexInParent?: number;
431
+ /** Whether a selector finds this node and nothing else. Without one, the
432
+ * path stops at an id or the depth cap, unchecked. */
433
+ isOnlyMatch?(selector: string): boolean;
434
+ /** What else identifies the node, beyond its path. */
435
+ context?(): Partial<IterationElement>;
402
436
  }
403
437
  /**
404
- * Best-effort descriptor of a picked element for locating it in code. Walks
405
- * up the tree collecting `#id` (preferred) or `tag:nth-child(n)` segments up
406
- * to a shallow depth, stopping at `BODY` or the depth cap. Kept framework-free
407
- * so it is unit-testable.
438
+ * Descriptor of a picked element for locating it in code. Walks up the tree
439
+ * collecting `#id` (preferred) or `tag:nth-child(n)` segments. The panel finds
440
+ * pins again with `querySelector`, which takes the first match, so with a real
441
+ * DOM the walk goes on past `maxDepth` until the path matches this element
442
+ * alone. Kept framework-free so it is unit-testable.
408
443
  */
409
444
  declare function describeIterationElement(el: NodeLike, maxDepth?: number): IterationElement;
410
445
  declare function elementAsNode(el: Element): NodeLike;
@@ -451,4 +486,4 @@ declare function createIterationStore(cfg?: IterationStoreConfig): {
451
486
  readonly hasUnsyncedNotes: boolean;
452
487
  };
453
488
 
454
- export { type ColorSwatch, type ColorVariantAxis, type IterationElement, type IterationNote, type IterationStoreConfig, KNOBS_STORAGE_KEY, type KnobAxis, type LiveVariantSelection, type NodeLike, type NoteContext, OVERLAY_DEFAULT_OPTION, OVERLAY_GROUP_ID, type OverlayGroup, type OverlayVariant, type OverlayVariantStatus, type OverlayVariantsList, type RangeVariantAxis, type RouteGateOptions, type SaveLocation, type SelectVariantAxis, VARIANTS_ENDPOINT, type VariantAction, type VariantAxis, type VariantAxisCondition, type VariantOption, type VariantReference, type VariantRemoval, type VariantSelections, type VariantsSource, applyKnobValues, axisDefault, createIterationStore, createOverlayVariantsSource, createVariantsSource, defaultSelections, describeIterationElement, elementAsNode, formatRangeValue, isAxisVisible, isDirty, isRouteVisible, isValidSelection, knobCssValue, knobDelta, knobVar, knobsFor, overlayAxes, overlayGroups, parseVariantParam, serializeVariantParam, variantAppliesTo, visibleAxes };
489
+ export { type ColorSwatch, type ColorVariantAxis, type IterationElement, type IterationNote, type IterationStoreConfig, KNOBS_EVENT, KNOBS_STORAGE_KEY, type KnobAxis, type LiveVariantSelection, type NodeLike, type NoteContext, OVERLAY_DEFAULT_OPTION, OVERLAY_GROUP_ID, type OverlayGroup, type OverlayVariant, type OverlayVariantStatus, type OverlayVariantsList, type RangeVariantAxis, type RouteGateOptions, type SaveLocation, type SelectVariantAxis, VARIANTS_ENDPOINT, type VariantAction, type VariantAxis, type VariantAxisCondition, type VariantOption, type VariantReference, type VariantRemoval, type VariantSelections, type VariantsSource, applyKnobValues, axisDefault, createIterationStore, createOverlayVariantsSource, createVariantsSource, defaultSelections, describeIterationElement, elementAsNode, formatRangeValue, isAxisVisible, isDirty, isRouteVisible, isValidSelection, knobCssValue, knobDelta, knobVar, knobsFor, overlayAxes, overlayGroups, parseVariantParam, serializeVariantParam, variantAppliesTo, visibleAxes };
package/dist/core.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  describeIterationElement,
4
4
  elementAsNode,
5
5
  isRouteVisible
6
- } from "./chunk-FKEKBZ3E.js";
6
+ } from "./chunk-K5RAC2BT.js";
7
7
  import {
8
+ KNOBS_EVENT,
8
9
  KNOBS_STORAGE_KEY,
9
10
  OVERLAY_DEFAULT_OPTION,
10
11
  OVERLAY_GROUP_ID,
@@ -28,9 +29,10 @@ import {
28
29
  serializeVariantParam,
29
30
  variantAppliesTo,
30
31
  visibleAxes
31
- } from "./chunk-PUXQGUMQ.js";
32
+ } from "./chunk-RKZWO37H.js";
32
33
  import "./chunk-A4DX2V4C.js";
33
34
  export {
35
+ KNOBS_EVENT,
34
36
  KNOBS_STORAGE_KEY,
35
37
  OVERLAY_DEFAULT_OPTION,
36
38
  OVERLAY_GROUP_ID,
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  describeIterationElement,
5
5
  elementAsNode,
6
6
  isRouteVisible
7
- } from "./chunk-FKEKBZ3E.js";
7
+ } from "./chunk-K5RAC2BT.js";
8
8
  import {
9
9
  FEEDBACK_STORAGE_KEY,
10
10
  KNOBS_STORAGE_KEY,
@@ -32,7 +32,7 @@ import {
32
32
  serializeVariantParam,
33
33
  visibleAxes,
34
34
  writeBrowserStorage
35
- } from "./chunk-PUXQGUMQ.js";
35
+ } from "./chunk-RKZWO37H.js";
36
36
  import {
37
37
  MAX_FEEDBACK_LENGTH
38
38
  } from "./chunk-A4DX2V4C.js";
@@ -1887,6 +1887,16 @@ function IterationPanel({
1887
1887
  );
1888
1888
  const [hoverRect, setHoverRect] = useState3(null);
1889
1889
  const [rects, setRects] = useState3([]);
1890
+ const pinnedElements = useRef4(/* @__PURE__ */ new Map());
1891
+ const locate = useCallback((selector) => {
1892
+ const pinned = pinnedElements.current.get(selector);
1893
+ if (pinned?.isConnected) return pinned;
1894
+ try {
1895
+ return document.querySelector(selector);
1896
+ } catch {
1897
+ return null;
1898
+ }
1899
+ }, []);
1890
1900
  const [text, setText] = useState3("");
1891
1901
  const [notes, setNotes] = useState3([]);
1892
1902
  const [scope, setScope] = useState3(null);
@@ -2358,6 +2368,7 @@ function IterationPanel({
2358
2368
  };
2359
2369
  const toggleTarget = (el) => {
2360
2370
  const target = describeIterationElement(elementAsNode(el));
2371
+ pinnedElements.current.set(target.selector, el);
2361
2372
  const added = !targetsRef.current.some((t) => t.selector === target.selector);
2362
2373
  setTargets(
2363
2374
  (prev) => prev.some((t) => t.selector === target.selector) ? prev.filter((t) => t.selector !== target.selector) : prev.length < 50 ? [...prev, target] : prev
@@ -2453,14 +2464,11 @@ function IterationPanel({
2453
2464
  const place = () => {
2454
2465
  frame = void 0;
2455
2466
  let { x, y } = anchor;
2456
- try {
2457
- const el = document.querySelector(anchor.selector);
2458
- if (el) {
2459
- const r = el.getBoundingClientRect();
2460
- x = r.left + anchor.dx;
2461
- y = r.top + anchor.dy;
2462
- }
2463
- } catch {
2467
+ const el = locate(anchor.selector);
2468
+ if (el) {
2469
+ const r = el.getBoundingClientRect();
2470
+ x = r.left + anchor.dx;
2471
+ y = r.top + anchor.dy;
2464
2472
  }
2465
2473
  const vw = document.documentElement.clientWidth || innerWidth;
2466
2474
  const vh = innerHeight;
@@ -2516,7 +2524,7 @@ function IterationPanel({
2516
2524
  window.removeEventListener("scroll", schedule, true);
2517
2525
  window.removeEventListener("resize", schedule);
2518
2526
  };
2519
- }, [anchor]);
2527
+ }, [anchor, locate]);
2520
2528
  useLayoutEffect4(() => {
2521
2529
  const last = floatNode.current;
2522
2530
  const root = rootRef.current;
@@ -2543,6 +2551,10 @@ function IterationPanel({
2543
2551
  ghost.remove();
2544
2552
  };
2545
2553
  }, [floating]);
2554
+ useEffect3(() => {
2555
+ const kept = new Set(targets.map((t) => t.selector));
2556
+ for (const selector of pinnedElements.current.keys()) if (!kept.has(selector)) pinnedElements.current.delete(selector);
2557
+ }, [targets]);
2546
2558
  useEffect3(() => {
2547
2559
  if (!picking && !floating) {
2548
2560
  setRects([]);
@@ -2553,22 +2565,18 @@ function IterationPanel({
2553
2565
  frame = void 0;
2554
2566
  setRects(
2555
2567
  targets.flatMap((t) => {
2556
- try {
2557
- const el = document.querySelector(t.selector);
2558
- if (!el) return [];
2559
- const r = el.getBoundingClientRect();
2560
- return [
2561
- {
2562
- top: r.top,
2563
- left: r.left,
2564
- width: r.width,
2565
- height: r.height,
2566
- selector: t.selector
2567
- }
2568
- ];
2569
- } catch {
2570
- return [];
2571
- }
2568
+ const el = locate(t.selector);
2569
+ if (!el) return [];
2570
+ const r = el.getBoundingClientRect();
2571
+ return [
2572
+ {
2573
+ top: r.top,
2574
+ left: r.left,
2575
+ width: r.width,
2576
+ height: r.height,
2577
+ selector: t.selector
2578
+ }
2579
+ ];
2572
2580
  })
2573
2581
  );
2574
2582
  };
@@ -2583,7 +2591,7 @@ function IterationPanel({
2583
2591
  window.removeEventListener("scroll", schedule, true);
2584
2592
  window.removeEventListener("resize", schedule);
2585
2593
  };
2586
- }, [targets, picking, floating]);
2594
+ }, [targets, picking, floating, locate]);
2587
2595
  const variantsPanel = variants && /* @__PURE__ */ jsxs4(
2588
2596
  "section",
2589
2597
  {
@@ -3154,7 +3162,7 @@ function overlaySourceWithSave(getRoute, endpoint, key) {
3154
3162
  if (!endpoint) return source;
3155
3163
  const notes = createIterationStore({ endpoint, key });
3156
3164
  source.saveDefaults = async (selections) => {
3157
- const picked = source.getAxes().filter((axis) => axis.kind === void 0 || axis.kind === "select").flatMap((axis) => {
3165
+ const picked = source.getAxes().filter((axis) => (axis.kind === void 0 || axis.kind === "select") && !source.knobs.axes.includes(axis)).flatMap((axis) => {
3158
3166
  const id = selections[axis.id];
3159
3167
  if (!id || id === OVERLAY_DEFAULT_OPTION) return [];
3160
3168
  const variant = source.list?.variants.find((v) => v.id === id);