@evinvest/uikit 0.26.0 → 0.27.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.
Files changed (57) hide show
  1. package/README.md +142 -8
  2. package/dist/components/account-menu.js +70 -0
  3. package/dist/components/account-menu.js.map +1 -0
  4. package/dist/components/alert-dialog.js +4 -3
  5. package/dist/components/alert-dialog.js.map +1 -1
  6. package/dist/components/app-shell.js +11 -0
  7. package/dist/components/app-shell.js.map +1 -1
  8. package/dist/components/command.js +129 -20
  9. package/dist/components/command.js.map +1 -1
  10. package/dist/components/context-menu.js +8 -8
  11. package/dist/components/context-menu.js.map +1 -1
  12. package/dist/components/dialog.js +4 -3
  13. package/dist/components/dialog.js.map +1 -1
  14. package/dist/components/drawer.js +9 -5
  15. package/dist/components/drawer.js.map +1 -1
  16. package/dist/components/dropdown-menu.js +13 -10
  17. package/dist/components/dropdown-menu.js.map +1 -1
  18. package/dist/components/info-tip.js +2 -2
  19. package/dist/components/info-tip.js.map +1 -1
  20. package/dist/components/menubar.js +18 -5
  21. package/dist/components/menubar.js.map +1 -1
  22. package/dist/components/navigation-menu.js +2 -2
  23. package/dist/components/navigation-menu.js.map +1 -1
  24. package/dist/components/popover.js +2 -2
  25. package/dist/components/popover.js.map +1 -1
  26. package/dist/components/select.js +2 -2
  27. package/dist/components/select.js.map +1 -1
  28. package/dist/components/sheet.js +4 -3
  29. package/dist/components/sheet.js.map +1 -1
  30. package/dist/components/sonner.js +1 -0
  31. package/dist/components/sonner.js.map +1 -1
  32. package/dist/components/table.js +24 -7
  33. package/dist/components/table.js.map +1 -1
  34. package/dist/components/toggle-group.js +1 -1
  35. package/dist/components/toggle-group.js.map +1 -1
  36. package/dist/components/top-bar.js +21 -0
  37. package/dist/components/top-bar.js.map +1 -0
  38. package/dist/generated/context-menu.js +1 -1
  39. package/dist/generated/context-menu.js.map +1 -1
  40. package/dist/generated/dropdown-menu.js +1 -1
  41. package/dist/generated/dropdown-menu.js.map +1 -1
  42. package/dist/generated/menubar.js +1 -1
  43. package/dist/generated/menubar.js.map +1 -1
  44. package/dist/generated/table.js +20 -3
  45. package/dist/generated/table.js.map +1 -1
  46. package/dist/generated/toggle.js +2 -2
  47. package/dist/generated/toggle.js.map +1 -1
  48. package/dist/index.d.ts +168 -22
  49. package/dist/index.js +5 -2
  50. package/dist/primitives/dismissable-layer.js +80 -15
  51. package/dist/primitives/dismissable-layer.js.map +1 -1
  52. package/dist/primitives/use-roving-focus.js +13 -7
  53. package/dist/primitives/use-roving-focus.js.map +1 -1
  54. package/package.json +1 -1
  55. package/styles/ev.css +4 -2
  56. package/styles/theme.css +7 -0
  57. package/styles/tokens.css +15 -3
@@ -1,13 +1,77 @@
1
1
  "use client";
2
+ import { jsx } from "react/jsx-runtime";
2
3
  import * as React from "react";
3
- const stack = [];
4
- function isInsideHigherLayer(own, target) {
5
- const index = stack.indexOf(own);
6
- if (index < 0) return false;
7
- return stack.slice(index + 1).some((layer) => layer.current?.contains(target) ?? false);
4
+ const STORE_KEY = /* @__PURE__ */ Symbol.for("@evinvest/uikit/dismissable-layers@1");
5
+ function isStore(value) {
6
+ return typeof value === "object" && value !== null && "stack" in value && Array.isArray(value.stack);
7
+ }
8
+ function sharedStore() {
9
+ const existing = Reflect.get(globalThis, STORE_KEY);
10
+ if (isStore(existing)) return existing;
11
+ const store2 = {
12
+ stack: [],
13
+ info: /* @__PURE__ */ new WeakMap(),
14
+ seen: /* @__PURE__ */ new WeakMap(),
15
+ snapshot: (event) => {
16
+ if (!store2.seen.has(event)) store2.seen.set(event, store2.stack.slice());
17
+ }
18
+ };
19
+ Reflect.set(globalThis, STORE_KEY, store2);
20
+ return store2;
21
+ }
22
+ const store = sharedStore();
23
+ const LAYER_IGNORE_ATTR = "data-dismissable-layer-ignore";
24
+ function layersAt(event) {
25
+ return store.seen.get(event) ?? store.stack;
26
+ }
27
+ function isPointerOutside(own, event) {
28
+ const target = event.target;
29
+ if (!(target instanceof Node)) return false;
30
+ const el = target instanceof Element ? target : target.parentElement;
31
+ if (el?.closest(`[${LAYER_IGNORE_ATTR}]`)) return false;
32
+ const layers = layersAt(event);
33
+ const index = layers.indexOf(own);
34
+ if (index < 0) return true;
35
+ const above = layers.slice(index + 1);
36
+ if (above.some((layer) => store.info.get(layer)?.modal === true)) return false;
37
+ return !above.some((layer) => layer.current?.contains(target) ?? false);
38
+ }
39
+ function ownsEscape(own, event) {
40
+ const layers = layersAt(event);
41
+ return layers[layers.length - 1] === own;
42
+ }
43
+ function isDescendant(layer, ancestor) {
44
+ for (let p = store.info.get(layer)?.parent ?? null; p; p = store.info.get(p)?.parent ?? null) {
45
+ if (p === ancestor) return true;
46
+ }
47
+ return false;
48
+ }
49
+ function push(layer) {
50
+ const { stack } = store;
51
+ if (stack.length === 0) {
52
+ window.addEventListener("keydown", store.snapshot, true);
53
+ window.addEventListener("pointerdown", store.snapshot, true);
54
+ }
55
+ const firstChild = stack.findIndex((other) => isDescendant(other, layer));
56
+ if (firstChild < 0) stack.push(layer);
57
+ else stack.splice(firstChild, 0, layer);
58
+ }
59
+ function pop(layer) {
60
+ const { stack } = store;
61
+ const index = stack.indexOf(layer);
62
+ if (index >= 0) stack.splice(index, 1);
63
+ if (stack.length === 0) {
64
+ window.removeEventListener("keydown", store.snapshot, true);
65
+ window.removeEventListener("pointerdown", store.snapshot, true);
66
+ }
67
+ }
68
+ const ParentLayerContext = React.createContext(null);
69
+ function DismissableLayerScope({ layer, children }) {
70
+ return /* @__PURE__ */ jsx(ParentLayerContext.Provider, { value: layer, children });
8
71
  }
9
72
  function useDismissableLayer(opts) {
10
- const { enabled, onDismiss, exclude } = opts;
73
+ const { enabled, onDismiss, exclude, pointerOutside = true, modal = false } = opts;
74
+ const parent = React.useContext(ParentLayerContext);
11
75
  const ref = React.useRef(null);
12
76
  const onDismissRef = React.useRef(onDismiss);
13
77
  const excludeRef = React.useRef(exclude);
@@ -17,32 +81,33 @@ function useDismissableLayer(opts) {
17
81
  });
18
82
  React.useEffect(() => {
19
83
  if (!enabled) return;
20
- stack.push(ref);
84
+ store.info.set(ref, { parent, modal });
85
+ push(ref);
21
86
  function onPointerDown(event) {
22
87
  const target = event.target;
23
- if (!target) return;
88
+ if (!(target instanceof Node)) return;
24
89
  if (ref.current && ref.current.contains(target)) return;
25
90
  for (const r of excludeRef.current ?? []) {
26
91
  if (r.current && r.current.contains(target)) return;
27
92
  }
28
- if (isInsideHigherLayer(ref, target)) return;
29
- onDismissRef.current(event);
93
+ if (isPointerOutside(ref, event)) onDismissRef.current(event);
30
94
  }
31
95
  function onKeyDown(event) {
32
- if (event.key === "Escape" && stack[stack.length - 1] === ref) onDismissRef.current(event);
96
+ if (event.key === "Escape" && ownsEscape(ref, event)) onDismissRef.current(event);
33
97
  }
34
- document.addEventListener("pointerdown", onPointerDown, true);
98
+ if (pointerOutside) document.addEventListener("pointerdown", onPointerDown, true);
35
99
  document.addEventListener("keydown", onKeyDown);
36
100
  return () => {
37
- const index = stack.indexOf(ref);
38
- if (index >= 0) stack.splice(index, 1);
101
+ pop(ref);
39
102
  document.removeEventListener("pointerdown", onPointerDown, true);
40
103
  document.removeEventListener("keydown", onKeyDown);
41
104
  };
42
- }, [enabled]);
105
+ }, [enabled, pointerOutside, modal, parent]);
43
106
  return ref;
44
107
  }
45
108
  export {
109
+ DismissableLayerScope,
110
+ LAYER_IGNORE_ATTR,
46
111
  useDismissableLayer
47
112
  };
48
113
  //# sourceMappingURL=dismissable-layer.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/primitives/dismissable-layer.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\ntype LayerRef = React.RefObject<HTMLDivElement | null>;\n\n// The open layers, oldest first. An overlay opened inside another (a Select in\n// a Dialog, a Popover in a Drawer) is portaled out of its parent's subtree, so\n// \"outside the parent\" would include it: the stack is what tells a nested\n// layer from the rest of the page.\nconst stack: LayerRef[] = [];\n\nfunction isInsideHigherLayer(own: LayerRef, target: Node): boolean {\n const index = stack.indexOf(own);\n // Not on the stack (a listener outliving its cleanup): nothing is above it.\n if (index < 0) return false;\n return stack.slice(index + 1).some((layer) => layer.current?.contains(target) ?? false);\n}\n\n/** Why a layer is being dismissed: the event itself, for a caller that treats keys and pointers apart. */\nexport type DismissEvent = PointerEvent | KeyboardEvent;\n\n/**\n * Calls `onDismiss` on Escape or on a pointer-down outside the layer — the\n * dep-light core of every overlay's \"click away to close\". Pass refs to any\n * nodes that should NOT count as outside (e.g. the trigger), via `exclude`.\n *\n * Layers stack, in the order they opened: Escape dismisses only the topmost\n * layer on this stack, and a pointer-down inside a layer opened above this one\n * is not outside it — so a Select's list inside a Dialog closes the list, not\n * the Dialog. The stack knows only the overlays built on this hook: a\n * `Drawer` or `CommandDialog` (Escape in their own React `onKeyDown`) is not\n * on it, and a menu that also closes on Escape in its React `onKeyDown` lets\n * the key bubble through React's tree to what holds it. Moving those onto the\n * stack is a follow-up.\n *\n * Mirrors the dismiss behaviour Rust expresses with a full-screen backdrop.\n */\nexport function useDismissableLayer(opts: {\n enabled: boolean;\n onDismiss: (event: DismissEvent) => void;\n exclude?: Array<React.RefObject<Element | null>>;\n}): React.RefObject<HTMLDivElement | null> {\n const { enabled, onDismiss, exclude } = opts;\n const ref = React.useRef<HTMLDivElement | null>(null);\n const onDismissRef = React.useRef(onDismiss);\n // Callers pass `exclude` inline, a new array each render: read through a ref,\n // or the effect would re-run and move this layer to the top of the stack.\n const excludeRef = React.useRef(exclude);\n React.useEffect(() => {\n onDismissRef.current = onDismiss;\n excludeRef.current = exclude;\n });\n\n React.useEffect(() => {\n if (!enabled) return;\n stack.push(ref);\n\n function onPointerDown(event: PointerEvent) {\n const target = event.target as Node | null;\n if (!target) return;\n if (ref.current && ref.current.contains(target)) return;\n for (const r of excludeRef.current ?? []) {\n if (r.current && r.current.contains(target)) return;\n }\n if (isInsideHigherLayer(ref, target)) return;\n onDismissRef.current(event);\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\" && stack[stack.length - 1] === ref) onDismissRef.current(event);\n }\n\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n const index = stack.indexOf(ref);\n if (index >= 0) stack.splice(index, 1);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [enabled]);\n\n return ref;\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAQvB,MAAM,QAAoB,CAAC;AAE3B,SAAS,oBAAoB,KAAe,QAAuB;AACjE,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAE/B,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO,MAAM,MAAM,QAAQ,CAAC,EAAE,KAAK,CAAC,UAAU,MAAM,SAAS,SAAS,MAAM,KAAK,KAAK;AACxF;AAqBO,SAAS,oBAAoB,MAIO;AACzC,QAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;AACxC,QAAM,MAAM,MAAM,OAA8B,IAAI;AACpD,QAAM,eAAe,MAAM,OAAO,SAAS;AAG3C,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,QAAM,UAAU,MAAM;AACpB,iBAAa,UAAU;AACvB,eAAW,UAAU;AAAA,EACvB,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AACd,UAAM,KAAK,GAAG;AAEd,aAAS,cAAc,OAAqB;AAC1C,YAAM,SAAS,MAAM;AACrB,UAAI,CAAC,OAAQ;AACb,UAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,MAAM,EAAG;AACjD,iBAAW,KAAK,WAAW,WAAW,CAAC,GAAG;AACxC,YAAI,EAAE,WAAW,EAAE,QAAQ,SAAS,MAAM,EAAG;AAAA,MAC/C;AACA,UAAI,oBAAoB,KAAK,MAAM,EAAG;AACtC,mBAAa,QAAQ,KAAK;AAAA,IAC5B;AAEA,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,YAAY,MAAM,MAAM,SAAS,CAAC,MAAM,IAAK,cAAa,QAAQ,KAAK;AAAA,IAC3F;AAEA,aAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,YAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,UAAI,SAAS,EAAG,OAAM,OAAO,OAAO,CAAC;AACrC,eAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/primitives/dismissable-layer.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\ntype LayerRef = React.RefObject<HTMLDivElement | null>;\n\ninterface LayerInfo {\n /** The layer this one was rendered inside, from context — not the one below it on the stack. */\n parent: LayerRef | null;\n modal: boolean;\n}\n\ninterface LayerStore {\n // The open layers, bottom first. An overlay opened inside another (a Select\n // in a Dialog, a Popover in a Drawer) is portaled out of its parent's\n // subtree, so \"outside the parent\" would include it: the stack is what tells\n // a nested layer from the rest of the page.\n stack: LayerRef[];\n info: WeakMap<LayerRef, LayerInfo>;\n // The stack as it stood when an event started its trip. By the time the\n // document hears an Escape, a React `onKeyDown` on the way (a Select's, a\n // consumer's) may already have closed the top layer and — a discrete update\n // commits synchronously — popped it, so the layer below would find itself\n // on top and close on the same key; a pointer-down can close an upper modal\n // the same way and lift its barrier mid-flight. Taken in the window's\n // capture phase, which runs before any DOM listener, React's root included.\n seen: WeakMap<Event, LayerRef[]>;\n snapshot: (event: Event) => void;\n}\n\n// One stack per page, not per module copy: a micro-frontend that bundles its\n// own uikit must see the host's layers, and the host its. Versioned, so a copy\n// with a different store shape keeps to itself instead of misreading this one.\nconst STORE_KEY = Symbol.for(\"@evinvest/uikit/dismissable-layers@1\");\n\nfunction isStore(value: unknown): value is LayerStore {\n return typeof value === \"object\" && value !== null && \"stack\" in value && Array.isArray(value.stack);\n}\n\nfunction sharedStore(): LayerStore {\n const existing: unknown = Reflect.get(globalThis, STORE_KEY);\n if (isStore(existing)) return existing;\n const store: LayerStore = {\n stack: [],\n info: new WeakMap(),\n seen: new WeakMap(),\n snapshot: (event) => {\n if (!store.seen.has(event)) store.seen.set(event, store.stack.slice());\n },\n };\n Reflect.set(globalThis, STORE_KEY, store);\n return store;\n}\n\nconst store = sharedStore();\n\n/**\n * Elements carrying this attribute are outside every layer without being a\n * click away from any: a press there dismisses nothing (the toaster).\n */\nexport const LAYER_IGNORE_ATTR = \"data-dismissable-layer-ignore\";\n\nfunction layersAt(event: Event): LayerRef[] {\n // An event dispatched where the window never sees it falls back to the live stack.\n return store.seen.get(event) ?? store.stack;\n}\n\nfunction isPointerOutside(own: LayerRef, event: PointerEvent): boolean {\n const target = event.target;\n if (!(target instanceof Node)) return false;\n const el = target instanceof Element ? target : target.parentElement;\n if (el?.closest(`[${LAYER_IGNORE_ATTR}]`)) return false;\n const layers = layersAt(event);\n const index = layers.indexOf(own);\n // Not on the stack (a listener outliving its cleanup): nothing is above it.\n if (index < 0) return true;\n const above = layers.slice(index + 1);\n // A modal above is a barrier: whatever the press landed on — its scrim, its\n // panel, a toast over it — the modal answers for it, not the layers below.\n if (above.some((layer) => store.info.get(layer)?.modal === true)) return false;\n return !above.some((layer) => layer.current?.contains(target) ?? false);\n}\n\nfunction ownsEscape(own: LayerRef, event: KeyboardEvent): boolean {\n const layers = layersAt(event);\n return layers[layers.length - 1] === own;\n}\n\nfunction isDescendant(layer: LayerRef, ancestor: LayerRef): boolean {\n for (let p = store.info.get(layer)?.parent ?? null; p; p = store.info.get(p)?.parent ?? null) {\n if (p === ancestor) return true;\n }\n return false;\n}\n\nfunction push(layer: LayerRef) {\n const { stack } = store;\n if (stack.length === 0) {\n window.addEventListener(\"keydown\", store.snapshot, true);\n window.addEventListener(\"pointerdown\", store.snapshot, true);\n }\n // Children's effects run before their parent's, so a layer opened in the\n // same commit as one nested in it (`defaultOpen` on both, a shared `open`)\n // arrives second: it goes below its first descendant, not on top.\n const firstChild = stack.findIndex((other) => isDescendant(other, layer));\n if (firstChild < 0) stack.push(layer);\n else stack.splice(firstChild, 0, layer);\n}\n\nfunction pop(layer: LayerRef) {\n const { stack } = store;\n const index = stack.indexOf(layer);\n if (index >= 0) stack.splice(index, 1);\n if (stack.length === 0) {\n window.removeEventListener(\"keydown\", store.snapshot, true);\n window.removeEventListener(\"pointerdown\", store.snapshot, true);\n }\n}\n\nconst ParentLayerContext = React.createContext<LayerRef | null>(null);\n\n/**\n * Marks `children` as rendered inside `layer` (the ref `useDismissableLayer`\n * returned), so a layer among them always stacks above it — even when both\n * open in the same commit. Wrap the overlay's content in it.\n */\nexport function DismissableLayerScope({ layer, children }: { layer: LayerRef; children?: React.ReactNode }) {\n return <ParentLayerContext.Provider value={layer}>{children}</ParentLayerContext.Provider>;\n}\n\n/** Why a layer is being dismissed: the event itself, for a caller that treats keys and pointers apart. */\nexport type DismissEvent = PointerEvent | KeyboardEvent;\n\n/**\n * Calls `onDismiss` on Escape or on a pointer-down outside the layer — the\n * dep-light core of every overlay's \"click away to close\". Pass refs to any\n * nodes that should NOT count as outside (e.g. the trigger), via `exclude`.\n *\n * Layers stack: a layer rendered inside another's `DismissableLayerScope`\n * sits above it, the rest in the order they opened. Escape dismisses only the\n * topmost layer, and a pointer-down inside a layer above this one is not\n * outside it — so a Select's list inside a Dialog closes the list, not the\n * Dialog. The Escape goes to the layer that was on top when the key was\n * pressed, even if a handler on its way closed that layer first.\n *\n * `modal: true` makes the layer a barrier: while it is open, the layers below\n * ignore every pointer-down — a press on its scrim closes it alone. Elements\n * marked with `LAYER_IGNORE_ATTR` (the toaster) are outside no layer.\n *\n * `pointerOutside: false` keeps the layer on the stack (it takes Escape, and\n * the layers below see a pointer-down inside it as inside) but leaves outside\n * clicks to the caller — for an overlay whose own scrim is what \"outside\"\n * means (Drawer, CommandDialog).\n *\n * The stack is shared through `globalThis`, so every copy of this module on\n * the page sees one stack; parent links need one React context, so they hold\n * within a copy only.\n *\n * Mirrors the dismiss behaviour Rust expresses with a full-screen backdrop.\n */\nexport function useDismissableLayer(opts: {\n enabled: boolean;\n onDismiss: (event: DismissEvent) => void;\n exclude?: Array<React.RefObject<Element | null>>;\n /** Dismiss on a pointer-down outside the layer. Default `true`. */\n pointerOutside?: boolean;\n /** Bar the layers below from pointer-downs while open. Default `false`. */\n modal?: boolean;\n}): React.RefObject<HTMLDivElement | null> {\n const { enabled, onDismiss, exclude, pointerOutside = true, modal = false } = opts;\n const parent = React.useContext(ParentLayerContext);\n const ref = React.useRef<HTMLDivElement | null>(null);\n const onDismissRef = React.useRef(onDismiss);\n // Callers pass `exclude` inline, a new array each render: read through a ref,\n // or the effect would re-run and move this layer to the top of the stack.\n const excludeRef = React.useRef(exclude);\n React.useEffect(() => {\n onDismissRef.current = onDismiss;\n excludeRef.current = exclude;\n });\n\n React.useEffect(() => {\n if (!enabled) return;\n store.info.set(ref, { parent, modal });\n push(ref);\n\n function onPointerDown(event: PointerEvent) {\n const target = event.target;\n if (!(target instanceof Node)) return;\n if (ref.current && ref.current.contains(target)) return;\n for (const r of excludeRef.current ?? []) {\n if (r.current && r.current.contains(target)) return;\n }\n if (isPointerOutside(ref, event)) onDismissRef.current(event);\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\" && ownsEscape(ref, event)) onDismissRef.current(event);\n }\n\n if (pointerOutside) document.addEventListener(\"pointerdown\", onPointerDown, true);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n pop(ref);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [enabled, pointerOutside, modal, parent]);\n\n return ref;\n}\n"],"mappings":"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","names":["store"]}
@@ -3,12 +3,17 @@ import * as React from "react";
3
3
  function useRovingFocus(opts) {
4
4
  const { count, orientation = "vertical", loop = true, initial = 0 } = opts;
5
5
  const [activeIndex, setActiveIndex] = React.useState(initial);
6
- const next = (dir) => setActiveIndex((i) => {
7
- let n = i + dir;
8
- if (n < 0) n = loop ? count - 1 : 0;
9
- else if (n >= count) n = loop ? 0 : count - 1;
10
- return n;
11
- });
6
+ const resolveCount = () => typeof count === "function" ? count() : count;
7
+ const next = (dir) => {
8
+ const total = resolveCount();
9
+ if (total <= 0) return;
10
+ setActiveIndex((i) => {
11
+ let n = Math.min(Math.max(i, 0), total - 1) + dir;
12
+ if (n < 0) n = loop ? total - 1 : 0;
13
+ else if (n >= total) n = loop ? 0 : total - 1;
14
+ return n;
15
+ });
16
+ };
12
17
  const onKeyDown = (event) => {
13
18
  const horiz = orientation === "horizontal" || orientation === "both";
14
19
  const vert = orientation === "vertical" || orientation === "both";
@@ -29,7 +34,8 @@ function useRovingFocus(opts) {
29
34
  setActiveIndex(0);
30
35
  } else if (event.key === "End") {
31
36
  event.preventDefault();
32
- setActiveIndex(count - 1);
37
+ const total = resolveCount();
38
+ if (total > 0) setActiveIndex(total - 1);
33
39
  }
34
40
  };
35
41
  return { activeIndex, setActiveIndex, onKeyDown };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/primitives/use-roving-focus.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Arrow-key roving focus over a set of items — the dep-light core of menus,\n * tabs, radio-groups and toolbars (replacing `@radix-ui/react-roving-focus`).\n * Returns an `onKeyDown` to spread on the container and the active index; items\n * should set `tabIndex={index === activeIndex ? 0 : -1}`.\n *\n * Mirrors the keyboard navigation Rust drives with a focused-index signal.\n */\nexport function useRovingFocus(opts: {\n count: number;\n orientation?: \"horizontal\" | \"vertical\" | \"both\";\n loop?: boolean;\n initial?: number;\n}): {\n activeIndex: number;\n setActiveIndex: (i: number) => void;\n onKeyDown: (event: React.KeyboardEvent) => void;\n} {\n const { count, orientation = \"vertical\", loop = true, initial = 0 } = opts;\n const [activeIndex, setActiveIndex] = React.useState(initial);\n\n const next = (dir: 1 | -1) =>\n setActiveIndex((i) => {\n let n = i + dir;\n if (n < 0) n = loop ? count - 1 : 0;\n else if (n >= count) n = loop ? 0 : count - 1;\n return n;\n });\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const horiz = orientation === \"horizontal\" || orientation === \"both\";\n const vert = orientation === \"vertical\" || orientation === \"both\";\n if (vert && event.key === \"ArrowDown\") {\n event.preventDefault();\n next(1);\n } else if (vert && event.key === \"ArrowUp\") {\n event.preventDefault();\n next(-1);\n } else if (horiz && event.key === \"ArrowRight\") {\n event.preventDefault();\n next(1);\n } else if (horiz && event.key === \"ArrowLeft\") {\n event.preventDefault();\n next(-1);\n } else if (event.key === \"Home\") {\n event.preventDefault();\n setActiveIndex(0);\n } else if (event.key === \"End\") {\n event.preventDefault();\n setActiveIndex(count - 1);\n }\n };\n\n return { activeIndex, setActiveIndex, onKeyDown };\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAUhB,SAAS,eAAe,MAS7B;AACA,QAAM,EAAE,OAAO,cAAc,YAAY,OAAO,MAAM,UAAU,EAAE,IAAI;AACtE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,OAAO;AAE5D,QAAM,OAAO,CAAC,QACZ,eAAe,CAAC,MAAM;AACpB,QAAI,IAAI,IAAI;AACZ,QAAI,IAAI,EAAG,KAAI,OAAO,QAAQ,IAAI;AAAA,aACzB,KAAK,MAAO,KAAI,OAAO,IAAI,QAAQ;AAC5C,WAAO;AAAA,EACT,CAAC;AAEH,QAAM,YAAY,CAAC,UAA+B;AAChD,UAAM,QAAQ,gBAAgB,gBAAgB,gBAAgB;AAC9D,UAAM,OAAO,gBAAgB,cAAc,gBAAgB;AAC3D,QAAI,QAAQ,MAAM,QAAQ,aAAa;AACrC,YAAM,eAAe;AACrB,WAAK,CAAC;AAAA,IACR,WAAW,QAAQ,MAAM,QAAQ,WAAW;AAC1C,YAAM,eAAe;AACrB,WAAK,EAAE;AAAA,IACT,WAAW,SAAS,MAAM,QAAQ,cAAc;AAC9C,YAAM,eAAe;AACrB,WAAK,CAAC;AAAA,IACR,WAAW,SAAS,MAAM,QAAQ,aAAa;AAC7C,YAAM,eAAe;AACrB,WAAK,EAAE;AAAA,IACT,WAAW,MAAM,QAAQ,QAAQ;AAC/B,YAAM,eAAe;AACrB,qBAAe,CAAC;AAAA,IAClB,WAAW,MAAM,QAAQ,OAAO;AAC9B,YAAM,eAAe;AACrB,qBAAe,QAAQ,CAAC;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,gBAAgB,UAAU;AAClD;","names":[]}
1
+ {"version":3,"sources":["../../src/primitives/use-roving-focus.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Arrow-key roving focus over a set of items — the dep-light core of menus,\n * tabs, radio-groups and toolbars (replacing `@radix-ui/react-roving-focus`).\n * Returns an `onKeyDown` to spread on the container and the active index; items\n * should set `tabIndex={index === activeIndex ? 0 : -1}`.\n *\n * `count` may be a getter: containers whose items are only known from the DOM\n * (menus with conditionally rendered or disabled items, inline sub-menus) pass\n * one so the bound is read at keydown time — never during render, which keeps\n * the hook safe under SSR.\n *\n * Mirrors the keyboard navigation Rust drives with a focused-index signal.\n */\nexport function useRovingFocus(opts: {\n count: number | (() => number);\n orientation?: \"horizontal\" | \"vertical\" | \"both\";\n loop?: boolean;\n initial?: number;\n}): {\n activeIndex: number;\n setActiveIndex: (i: number) => void;\n onKeyDown: (event: React.KeyboardEvent) => void;\n} {\n const { count, orientation = \"vertical\", loop = true, initial = 0 } = opts;\n const [activeIndex, setActiveIndex] = React.useState(initial);\n\n const resolveCount = () => (typeof count === \"function\" ? count() : count);\n\n const next = (dir: 1 | -1) => {\n const total = resolveCount();\n if (total <= 0) return;\n setActiveIndex((i) => {\n // Clamp first: the item set may have shrunk since the index was set.\n let n = Math.min(Math.max(i, 0), total - 1) + dir;\n if (n < 0) n = loop ? total - 1 : 0;\n else if (n >= total) n = loop ? 0 : total - 1;\n return n;\n });\n };\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const horiz = orientation === \"horizontal\" || orientation === \"both\";\n const vert = orientation === \"vertical\" || orientation === \"both\";\n if (vert && event.key === \"ArrowDown\") {\n event.preventDefault();\n next(1);\n } else if (vert && event.key === \"ArrowUp\") {\n event.preventDefault();\n next(-1);\n } else if (horiz && event.key === \"ArrowRight\") {\n event.preventDefault();\n next(1);\n } else if (horiz && event.key === \"ArrowLeft\") {\n event.preventDefault();\n next(-1);\n } else if (event.key === \"Home\") {\n event.preventDefault();\n setActiveIndex(0);\n } else if (event.key === \"End\") {\n event.preventDefault();\n const total = resolveCount();\n if (total > 0) setActiveIndex(total - 1);\n }\n };\n\n return { activeIndex, setActiveIndex, onKeyDown };\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAehB,SAAS,eAAe,MAS7B;AACA,QAAM,EAAE,OAAO,cAAc,YAAY,OAAO,MAAM,UAAU,EAAE,IAAI;AACtE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,OAAO;AAE5D,QAAM,eAAe,MAAO,OAAO,UAAU,aAAa,MAAM,IAAI;AAEpE,QAAM,OAAO,CAAC,QAAgB;AAC5B,UAAM,QAAQ,aAAa;AAC3B,QAAI,SAAS,EAAG;AAChB,mBAAe,CAAC,MAAM;AAEpB,UAAI,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI;AAC9C,UAAI,IAAI,EAAG,KAAI,OAAO,QAAQ,IAAI;AAAA,eACzB,KAAK,MAAO,KAAI,OAAO,IAAI,QAAQ;AAC5C,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,CAAC,UAA+B;AAChD,UAAM,QAAQ,gBAAgB,gBAAgB,gBAAgB;AAC9D,UAAM,OAAO,gBAAgB,cAAc,gBAAgB;AAC3D,QAAI,QAAQ,MAAM,QAAQ,aAAa;AACrC,YAAM,eAAe;AACrB,WAAK,CAAC;AAAA,IACR,WAAW,QAAQ,MAAM,QAAQ,WAAW;AAC1C,YAAM,eAAe;AACrB,WAAK,EAAE;AAAA,IACT,WAAW,SAAS,MAAM,QAAQ,cAAc;AAC9C,YAAM,eAAe;AACrB,WAAK,CAAC;AAAA,IACR,WAAW,SAAS,MAAM,QAAQ,aAAa;AAC7C,YAAM,eAAe;AACrB,WAAK,EAAE;AAAA,IACT,WAAW,MAAM,QAAQ,QAAQ;AAC/B,YAAM,eAAe;AACrB,qBAAe,CAAC;AAAA,IAClB,WAAW,MAAM,QAAQ,OAAO;AAC9B,YAAM,eAAe;AACrB,YAAM,QAAQ,aAAa;AAC3B,UAAI,QAAQ,EAAG,gBAAe,QAAQ,CAAC;AAAA,IACzC;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,gBAAgB,UAAU;AAClD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/uikit",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {
package/styles/ev.css CHANGED
@@ -60,8 +60,10 @@
60
60
  --accent-warn: #f2c94c;
61
61
  --on-accent-warn: #070d18;
62
62
  /* lifted off the brand red #da251d, which failed AA on a card. This one value
63
- * carries both the fill and the text; the vivid brand red is a gitops
64
- * constant, not a UI token. */
63
+ * is the fill and the text on a bare surface; on its own 10 % tint (a
64
+ * destructive menu row) the text is the derived --accent-error-ink, which
65
+ * EV leaves to the contract: #ec8379, 4.83:1 on that tint over popover. The
66
+ * vivid brand red is a gitops constant, not a UI token. */
65
67
  --accent-error: #ef5b52;
66
68
  --on-accent-error: #070d18;
67
69
 
package/styles/theme.css CHANGED
@@ -538,6 +538,7 @@
538
538
  --color-on-accent-warn: var(--on-accent-warn);
539
539
  --color-accent-error: var(--accent-error);
540
540
  --color-on-accent-error: var(--on-accent-error);
541
+ --color-accent-error-ink: var(--accent-error-ink);
541
542
 
542
543
  /* charts — distinguishable hues carrying no significance */
543
544
  --color-chart-1: var(--chart-1);
@@ -565,6 +566,12 @@
565
566
  * side — ink over white needs ~47% to clear it, dark lands near 4:1. */
566
567
  --input: color-mix(in srgb, var(--ink) 48%, transparent);
567
568
  --ring: var(--primary-ink);
569
+ /* the error role as text on its own 10 % tint — a destructive menu row
570
+ * (lib#166). The fill alone read 3.76:1 there on EV and 2.48:1 on a dark
571
+ * brand; 30 % of the ink pulls it toward whichever polarity the text needs.
572
+ * Derived rather than required so a brand file that predates it still
573
+ * renders; one that misses the floor pins its own (docs/spec/accents.md). */
574
+ --accent-error-ink: color-mix(in srgb, var(--accent-error) 70%, var(--ink));
568
575
  }
569
576
 
570
577
  /* Geometry. A `[data-brand]` scope is a theme root of its own, so it restarts
package/styles/tokens.css CHANGED
@@ -21,6 +21,8 @@
21
21
  * accent-trace · accent-debug · accent-info · accent-warn · accent-error
22
22
  * primary-ink — the primary role as ink on a surface, where the
23
23
  * fill and the readable value are different colours
24
+ * accent-error-ink — the error role as text on its own tint;
25
+ * derived from accent-error and ink, a palette may pin it
24
26
  * SCALARS radius · control-* · display-scale · page-* · shell-* · shadows · fonts
25
27
  * MOTION ev-ease-* · ev-dur-* · ev-rise · ev-stagger(-section)
26
28
  * CHARTS chart-1 … chart-5
@@ -37,7 +39,8 @@
37
39
  *
38
40
  * Accents are ordered by significance, not by meaning; see docs/spec/accents.md.
39
41
  * Their values are read outside CSS (gitops mirrors them into Discord embed
40
- * bars), so they stay plain hex — no color-mix.
42
+ * bars), so they stay plain hex — no color-mix. `accent-error-ink` is not an
43
+ * accent but a reading of one, so it lives with the derived tokens.
41
44
  */
42
45
 
43
46
  /*
@@ -579,6 +582,7 @@
579
582
  --color-on-accent-warn: var(--on-accent-warn);
580
583
  --color-accent-error: var(--accent-error);
581
584
  --color-on-accent-error: var(--on-accent-error);
585
+ --color-accent-error-ink: var(--accent-error-ink);
582
586
 
583
587
  /* charts — distinguishable hues carrying no significance */
584
588
  --color-chart-1: var(--chart-1);
@@ -606,6 +610,12 @@
606
610
  * side — ink over white needs ~47% to clear it, dark lands near 4:1. */
607
611
  --input: color-mix(in srgb, var(--ink) 48%, transparent);
608
612
  --ring: var(--primary-ink);
613
+ /* the error role as text on its own 10 % tint — a destructive menu row
614
+ * (lib#166). The fill alone read 3.76:1 there on EV and 2.48:1 on a dark
615
+ * brand; 30 % of the ink pulls it toward whichever polarity the text needs.
616
+ * Derived rather than required so a brand file that predates it still
617
+ * renders; one that misses the floor pins its own (docs/spec/accents.md). */
618
+ --accent-error-ink: color-mix(in srgb, var(--accent-error) 70%, var(--ink));
609
619
  }
610
620
 
611
621
  /* Geometry. A `[data-brand]` scope is a theme root of its own, so it restarts
@@ -754,8 +764,10 @@
754
764
  --accent-warn: #f2c94c;
755
765
  --on-accent-warn: #070d18;
756
766
  /* lifted off the brand red #da251d, which failed AA on a card. This one value
757
- * carries both the fill and the text; the vivid brand red is a gitops
758
- * constant, not a UI token. */
767
+ * is the fill and the text on a bare surface; on its own 10 % tint (a
768
+ * destructive menu row) the text is the derived --accent-error-ink, which
769
+ * EV leaves to the contract: #ec8379, 4.83:1 on that tint over popover. The
770
+ * vivid brand red is a gitops constant, not a UI token. */
759
771
  --accent-error: #ef5b52;
760
772
  --on-accent-error: #070d18;
761
773