@evinvest/uikit 0.29.4 → 0.31.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 +294 -90
- package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
- package/dist/chunk-35SPIU4U.js.map +1 -0
- package/dist/components/account-list.js +128 -0
- package/dist/components/account-list.js.map +1 -0
- package/dist/components/account-menu-profile.js +114 -0
- package/dist/components/account-menu-profile.js.map +1 -0
- package/dist/components/account-menu.js +50 -23
- package/dist/components/account-menu.js.map +1 -1
- package/dist/components/account-settings-dialog.js +84 -0
- package/dist/components/account-settings-dialog.js.map +1 -0
- package/dist/components/alert-dialog.js +3 -2
- package/dist/components/alert-dialog.js.map +1 -1
- package/dist/components/animated-number-columns.js +105 -0
- package/dist/components/animated-number-columns.js.map +1 -0
- package/dist/components/animated-number.js +157 -0
- package/dist/components/animated-number.js.map +1 -0
- package/dist/components/app-shell.js +9 -0
- package/dist/components/app-shell.js.map +1 -1
- package/dist/components/aside.js +140 -0
- package/dist/components/aside.js.map +1 -0
- package/dist/components/badge.js +1 -1
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.js +3 -11
- package/dist/components/button.js.map +1 -1
- package/dist/components/chat-composer.js +141 -0
- package/dist/components/chat-composer.js.map +1 -0
- package/dist/components/chat-message.js +195 -0
- package/dist/components/chat-message.js.map +1 -0
- package/dist/components/chat-panel.js +137 -0
- package/dist/components/chat-panel.js.map +1 -0
- package/dist/components/chat-stream-text.js +69 -0
- package/dist/components/chat-stream-text.js.map +1 -0
- package/dist/components/chat-typing.js +26 -0
- package/dist/components/chat-typing.js.map +1 -0
- package/dist/components/command-palette.js +119 -0
- package/dist/components/command-palette.js.map +1 -0
- package/dist/components/command.js +3 -1
- package/dist/components/command.js.map +1 -1
- package/dist/components/confirm-dialog.js +123 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/context-menu.js +53 -5
- package/dist/components/context-menu.js.map +1 -1
- package/dist/components/dialog.js +3 -2
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/drawer.js +3 -1
- package/dist/components/drawer.js.map +1 -1
- package/dist/components/dropdown-menu.js +64 -5
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/form-row.js +51 -0
- package/dist/components/form-row.js.map +1 -0
- package/dist/components/header-controls.js +149 -0
- package/dist/components/header-controls.js.map +1 -0
- package/dist/components/icon-button.js +35 -0
- package/dist/components/icon-button.js.map +1 -0
- package/dist/components/key-value.js +68 -0
- package/dist/components/key-value.js.map +1 -0
- package/dist/components/menubar.js +39 -5
- package/dist/components/menubar.js.map +1 -1
- package/dist/components/native-select.js +3 -4
- package/dist/components/native-select.js.map +1 -1
- package/dist/components/nav-collapse.js +142 -0
- package/dist/components/nav-collapse.js.map +1 -0
- package/dist/components/nav-model.js.map +1 -1
- package/dist/components/navigation-menu.js +4 -5
- package/dist/components/navigation-menu.js.map +1 -1
- package/dist/components/page-frame.js +2 -5
- package/dist/components/page-frame.js.map +1 -1
- package/dist/components/popover-form.js +60 -0
- package/dist/components/popover-form.js.map +1 -0
- package/dist/components/resource-error.js +1 -1
- package/dist/components/resource-error.js.map +1 -1
- package/dist/components/section-nav.js +73 -20
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/select.js +16 -6
- package/dist/components/select.js.map +1 -1
- package/dist/components/settings.js +94 -0
- package/dist/components/settings.js.map +1 -0
- package/dist/components/sheet.js +3 -2
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/shell-nav.js +80 -38
- package/dist/components/shell-nav.js.map +1 -1
- package/dist/components/smart-header.js +53 -0
- package/dist/components/smart-header.js.map +1 -0
- package/dist/components/status-item.js +42 -0
- package/dist/components/status-item.js.map +1 -0
- package/dist/components/system-banner.js +22 -12
- package/dist/components/system-banner.js.map +1 -1
- package/dist/components/top-bar.js +4 -3
- package/dist/components/top-bar.js.map +1 -1
- package/dist/components/ui-icons.js +77 -0
- package/dist/components/ui-icons.js.map +1 -0
- package/dist/components/use-count-tween.js +42 -0
- package/dist/components/use-count-tween.js.map +1 -0
- package/dist/components/workspace-tab.js +140 -0
- package/dist/components/workspace-tab.js.map +1 -0
- package/dist/generated/accent.js +2 -2
- package/dist/generated/accent.js.map +1 -1
- package/dist/generated/account-list.js +35 -0
- package/dist/generated/account-list.js.map +1 -0
- package/dist/generated/account-settings-dialog.js +27 -0
- package/dist/generated/account-settings-dialog.js.map +1 -0
- package/dist/generated/alert-dialog.js +1 -1
- package/dist/generated/alert-dialog.js.map +1 -1
- package/dist/generated/alert.js +2 -2
- package/dist/generated/alert.js.map +1 -1
- package/dist/generated/aside.js +36 -0
- package/dist/generated/aside.js.map +1 -0
- package/dist/generated/badge.js +7 -6
- package/dist/generated/badge.js.map +1 -1
- package/dist/generated/band.js +1 -1
- package/dist/generated/band.js.map +1 -1
- package/dist/generated/breadcrumb.js +2 -2
- package/dist/generated/breadcrumb.js.map +1 -1
- package/dist/generated/button-group.js +2 -2
- package/dist/generated/button-group.js.map +1 -1
- package/dist/generated/button.js +4 -4
- package/dist/generated/button.js.map +1 -1
- package/dist/generated/card.js +6 -6
- package/dist/generated/card.js.map +1 -1
- package/dist/generated/chat-composer.js +21 -0
- package/dist/generated/chat-composer.js.map +1 -0
- package/dist/generated/chat-message.js +86 -0
- package/dist/generated/chat-message.js.map +1 -0
- package/dist/generated/chat-typing.js +9 -0
- package/dist/generated/chat-typing.js.map +1 -0
- package/dist/generated/command-palette.js +17 -0
- package/dist/generated/command-palette.js.map +1 -0
- package/dist/generated/command.js +3 -3
- package/dist/generated/command.js.map +1 -1
- package/dist/generated/confirm-dialog.js +28 -0
- package/dist/generated/confirm-dialog.js.map +1 -0
- package/dist/generated/context-menu.js +1 -1
- package/dist/generated/context-menu.js.map +1 -1
- package/dist/generated/date-time-picker.js +1 -1
- package/dist/generated/date-time-picker.js.map +1 -1
- package/dist/generated/dialog.js +1 -1
- package/dist/generated/dialog.js.map +1 -1
- package/dist/generated/drawer.js +1 -1
- package/dist/generated/drawer.js.map +1 -1
- package/dist/generated/dropdown-menu.js +1 -1
- package/dist/generated/dropdown-menu.js.map +1 -1
- package/dist/generated/form-row.js +17 -0
- package/dist/generated/form-row.js.map +1 -0
- package/dist/generated/header-controls.js +56 -0
- package/dist/generated/header-controls.js.map +1 -0
- package/dist/generated/icon-button.js +13 -0
- package/dist/generated/icon-button.js.map +1 -0
- package/dist/generated/key-value.js +15 -0
- package/dist/generated/key-value.js.map +1 -0
- package/dist/generated/menubar.js +1 -1
- package/dist/generated/menubar.js.map +1 -1
- package/dist/generated/navigation-menu.js +1 -1
- package/dist/generated/navigation-menu.js.map +1 -1
- package/dist/generated/popover-form.js +13 -0
- package/dist/generated/popover-form.js.map +1 -0
- package/dist/generated/section-label.js +10 -0
- package/dist/generated/section-label.js.map +1 -0
- package/dist/generated/select.js +15 -3
- package/dist/generated/select.js.map +1 -1
- package/dist/generated/settings.js +36 -0
- package/dist/generated/settings.js.map +1 -0
- package/dist/generated/sheet.js +1 -1
- package/dist/generated/sheet.js.map +1 -1
- package/dist/generated/slider.js +1 -1
- package/dist/generated/slider.js.map +1 -1
- package/dist/generated/smart-header.js +11 -0
- package/dist/generated/smart-header.js.map +1 -0
- package/dist/generated/sonner.js +4 -4
- package/dist/generated/sonner.js.map +1 -1
- package/dist/generated/status-item.js +17 -0
- package/dist/generated/status-item.js.map +1 -0
- package/dist/generated/system-banner.js +33 -0
- package/dist/generated/system-banner.js.map +1 -0
- package/dist/generated/top-bar.js +9 -0
- package/dist/generated/top-bar.js.map +1 -0
- package/dist/generated/workspace-tab.js +17 -0
- package/dist/generated/workspace-tab.js.map +1 -0
- package/dist/index.d.ts +852 -156
- package/dist/index.js +21 -1
- package/dist/lib/cn.js +8 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/number-parts.js +90 -0
- package/dist/lib/number-parts.js.map +1 -0
- package/dist/palette-cli.js +1 -1
- package/dist/palette.d.ts +6 -4
- package/dist/palette.js +3 -1
- package/dist/primitives/dismissable-layer.js +76 -4
- package/dist/primitives/dismissable-layer.js.map +1 -1
- package/dist/primitives/focus-scope.js +20 -5
- package/dist/primitives/focus-scope.js.map +1 -1
- package/dist/primitives/use-floating.js +16 -1
- package/dist/primitives/use-floating.js.map +1 -1
- package/dist/primitives/use-reduced-motion.js +20 -0
- package/dist/primitives/use-reduced-motion.js.map +1 -0
- package/package.json +1 -1
- package/styles/ev.css +281 -49
- package/styles/theme.css +335 -39
- package/styles/tokens.css +643 -103
- package/dist/chunk-3IPB4QQJ.js.map +0 -1
- package/dist/components/terminal.js +0 -206
- package/dist/components/terminal.js.map +0 -1
- package/dist/generated/terminal.js +0 -70
- package/dist/generated/terminal.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -70,7 +70,6 @@ export { Calendar, } from "./components/calendar.js";
|
|
|
70
70
|
export { DateTimePicker, } from "./components/date-time-picker.js";
|
|
71
71
|
export { Toaster, toast, } from "./components/sonner.js";
|
|
72
72
|
export { Form, FormControl, FormDescription, FormItem, FormLabel, FormMessage, } from "./components/form.js";
|
|
73
|
-
export { OpenOrdersEmpty, OrderBook, OrderBookHead, OrderBookRow, OrderBookSpread, OrderForm, OrderFormRow, OrderFormSubmit, Terminal, TerminalChart, TerminalPane, TerminalPaneBody, TerminalPaneHeader, TerminalTicker, TickerStat, TradesTapeRow, } from "./components/terminal.js";
|
|
74
73
|
export { AppShell } from "./components/app-shell.js";
|
|
75
74
|
export { isNavItemActive, isPlainLeftClick, navItemMatch, resolveActiveNavItem, } from "./components/nav-model.js";
|
|
76
75
|
export { usePendingNavigation } from "./components/use-shell-nav.js";
|
|
@@ -85,3 +84,24 @@ export { SectionNav } from "./components/section-nav.js";
|
|
|
85
84
|
export { ResourceError } from "./components/resource-error.js";
|
|
86
85
|
export { SystemBanner } from "./components/system-banner.js";
|
|
87
86
|
export { Settled } from "./components/settled.js";
|
|
87
|
+
export { AnimatedNumber } from "./components/animated-number.js";
|
|
88
|
+
export { useCountTween } from "./components/use-count-tween.js";
|
|
89
|
+
export { IconButton } from "./components/icon-button.js";
|
|
90
|
+
export { StatusItem, StatusDot } from "./components/status-item.js";
|
|
91
|
+
export { WorkspaceTabs, WorkspaceTab } from "./components/workspace-tab.js";
|
|
92
|
+
export { KeyValue } from "./components/key-value.js";
|
|
93
|
+
export { FormRow } from "./components/form-row.js";
|
|
94
|
+
export { SettingsLayout, SettingsSection, SettingsRow } from "./components/settings.js";
|
|
95
|
+
export { AccountSettingsDialog } from "./components/account-settings-dialog.js";
|
|
96
|
+
export { ConfirmDialog } from "./components/confirm-dialog.js";
|
|
97
|
+
export { AccountList, AccountListItem } from "./components/account-list.js";
|
|
98
|
+
export { PopoverForm } from "./components/popover-form.js";
|
|
99
|
+
export { CommandPalette, useCommandPaletteShortcut } from "./components/command-palette.js";
|
|
100
|
+
export { HeaderBalance, HeaderSearch, NotificationBell } from "./components/header-controls.js";
|
|
101
|
+
export { SmartHeader } from "./components/smart-header.js";
|
|
102
|
+
export { AsideHeader, AsidePanel, AsideRail } from "./components/aside.js";
|
|
103
|
+
export { ChatAttachment, ChatBubble, ChatMarker, ChatMessage } from "./components/chat-message.js";
|
|
104
|
+
export { ChatStreamText } from "./components/chat-stream-text.js";
|
|
105
|
+
export { ChatTyping } from "./components/chat-typing.js";
|
|
106
|
+
export { ChatComposer, ModelChip } from "./components/chat-composer.js";
|
|
107
|
+
export { ChatDock, ChatPanel } from "./components/chat-panel.js";
|
package/dist/lib/cn.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { clsx } from "clsx";
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
const twMerge = extendTailwindMerge({
|
|
4
|
+
extend: {
|
|
5
|
+
classGroups: {
|
|
6
|
+
"font-size": [{ text: ["caption", "body-s", "body", "body-l", "h3", "h2", "h1", "figure-m", "figure-l"] }]
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
});
|
|
3
10
|
function cn(...inputs) {
|
|
4
11
|
if (inputs.length === 2 && typeof inputs[0] === "string" && !inputs[1]) {
|
|
5
12
|
return inputs[0];
|
package/dist/lib/cn.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n// The kit's own type scale (theme.css `--text-*`). tailwind-merge only knows\n// t-shirt sizes as font sizes, so `text-body` would be filed under text colour\n// and dropped by a later `text-ink`. The Rust mirror is `fuse` in ev_lib_classes.\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n \"font-size\": [{ text: [\"caption\", \"body-s\", \"body\", \"body-l\", \"h3\", \"h2\", \"h1\", \"figure-m\", \"figure-l\"] }],\n },\n },\n});\n\n/**\n * Fuses class fragments into one string with real Tailwind conflict resolution:\n * `clsx` drops falsy fragments and joins the rest, `tailwind-merge` resolves\n * conflicting utilities so the rightmost wins (`cn(\"p-4\", \"p-2\") === \"p-2\"`). A\n * caller's `className` override, passed last, therefore beats the base classes.\n *\n * `twMerge` parses every utility on every render, and the overwhelmingly common\n * call is one kit constant plus a `className` override that is absent — so that\n * call returns the constant untouched.\n *\n * The Rust mirror is the `cn!` macro in `ev_lib::uikit` (`tailwind_fuse::tw_merge!`).\n */\nexport function cn(...inputs: ClassValue[]): string {\n if (inputs.length === 2 && typeof inputs[0] === \"string\" && !inputs[1]) {\n return inputs[0];\n }\n return twMerge(clsx(inputs));\n}\n"],"mappings":"AAAA,SAAS,YAA6B;AACtC,SAAS,2BAA2B;AAKpC,MAAM,UAAU,oBAAoB;AAAA,EAClC,QAAQ;AAAA,IACN,aAAa;AAAA,MACX,aAAa,CAAC,EAAE,MAAM,CAAC,WAAW,UAAU,QAAQ,UAAU,MAAM,MAAM,MAAM,YAAY,UAAU,EAAE,CAAC;AAAA,IAC3G;AAAA,EACF;AACF,CAAC;AAcM,SAAS,MAAM,QAA8B;AAClD,MAAI,OAAO,WAAW,KAAK,OAAO,OAAO,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,GAAG;AACtE,WAAO,OAAO,CAAC;AAAA,EACjB;AACA,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function digitGlyphs(formatter) {
|
|
2
|
+
const { numberingSystem } = formatter.resolvedOptions();
|
|
3
|
+
const plain = new Intl.NumberFormat("en", { useGrouping: false, numberingSystem });
|
|
4
|
+
return Array.from({ length: 10 }, (_, d) => plain.format(d));
|
|
5
|
+
}
|
|
6
|
+
function splitNumber(formatter, value, glyphs) {
|
|
7
|
+
const values = new Map(glyphs.map((glyph, digit) => [glyph, digit]));
|
|
8
|
+
const raw = formatter.formatToParts(value);
|
|
9
|
+
const integerDigits = raw.filter((p) => p.type === "integer").reduce((n, p) => n + Array.from(p.value).length, 0);
|
|
10
|
+
const parts = [];
|
|
11
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12
|
+
const textKey = (type, side2) => {
|
|
13
|
+
const id = `${type}:${side2}`;
|
|
14
|
+
const n = seen.get(id) ?? 0;
|
|
15
|
+
seen.set(id, n + 1);
|
|
16
|
+
return `${id}:${n}`;
|
|
17
|
+
};
|
|
18
|
+
let side = "pre";
|
|
19
|
+
let placed = 0;
|
|
20
|
+
let fractionAt = 0;
|
|
21
|
+
for (const { type, value: text } of raw) {
|
|
22
|
+
if (type === "integer") {
|
|
23
|
+
side = "post";
|
|
24
|
+
for (const char of Array.from(text)) {
|
|
25
|
+
const digit = values.get(char);
|
|
26
|
+
const place = integerDigits - 1 - placed;
|
|
27
|
+
placed += 1;
|
|
28
|
+
parts.push(
|
|
29
|
+
digit === void 0 ? { kind: "text", key: textKey("integer", side), text: char } : { kind: "digit", key: `int:${place}`, digit }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
} else if (type === "fraction") {
|
|
33
|
+
for (const char of Array.from(text)) {
|
|
34
|
+
const digit = values.get(char);
|
|
35
|
+
const at = fractionAt;
|
|
36
|
+
fractionAt += 1;
|
|
37
|
+
parts.push(
|
|
38
|
+
digit === void 0 ? { kind: "text", key: textKey("fraction", side), text: char } : { kind: "digit", key: `frac:${at}`, digit }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} else if (type === "group") {
|
|
42
|
+
parts.push({ kind: "text", key: `group:${integerDigits - 1 - placed}`, text });
|
|
43
|
+
} else if (type === "decimal") {
|
|
44
|
+
side = "post";
|
|
45
|
+
parts.push({ kind: "text", key: "decimal", text });
|
|
46
|
+
} else {
|
|
47
|
+
parts.push({ kind: "text", key: textKey(type, side), text });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts;
|
|
51
|
+
}
|
|
52
|
+
function rollStep(from, to, direction) {
|
|
53
|
+
const up = ((to - from) % 10 + 10) % 10;
|
|
54
|
+
const down = (10 - up) % 10;
|
|
55
|
+
if (direction === 1) return up;
|
|
56
|
+
if (direction === -1) return -down;
|
|
57
|
+
return up <= down ? up : -down;
|
|
58
|
+
}
|
|
59
|
+
function resolveDirection(trend, from, to) {
|
|
60
|
+
if (trend !== void 0) return trend;
|
|
61
|
+
return to > from ? 1 : to < from ? -1 : 0;
|
|
62
|
+
}
|
|
63
|
+
function mergeItems(previous, next, animate) {
|
|
64
|
+
const known = new Set(previous.map((item) => item.part.key));
|
|
65
|
+
const result = next.map((part) => ({
|
|
66
|
+
part,
|
|
67
|
+
exiting: false,
|
|
68
|
+
fresh: animate && !known.has(part.key)
|
|
69
|
+
}));
|
|
70
|
+
if (!animate) return result;
|
|
71
|
+
const nextKeys = new Set(next.map((part) => part.key));
|
|
72
|
+
let anchor = -1;
|
|
73
|
+
for (const item of previous) {
|
|
74
|
+
if (nextKeys.has(item.part.key)) {
|
|
75
|
+
anchor = result.findIndex((r) => r.part.key === item.part.key);
|
|
76
|
+
} else {
|
|
77
|
+
anchor += 1;
|
|
78
|
+
result.splice(anchor, 0, { part: item.part, exiting: true, fresh: false });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
digitGlyphs,
|
|
85
|
+
mergeItems,
|
|
86
|
+
resolveDirection,
|
|
87
|
+
rollStep,
|
|
88
|
+
splitNumber
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=number-parts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/number-parts.ts"],"sourcesContent":["/** A digit column or a fixed piece of a formatted number, with a stable identity across values. */\nexport type NumberPart =\n | { kind: \"digit\"; key: string; digit: number }\n | { kind: \"text\"; key: string; text: string };\n\nexport type RollDirection = 1 | -1 | 0;\n\n/** The ten digit glyphs of the formatter's numbering system (\"٠\"..\"٩\" in Arabic locales). */\nexport function digitGlyphs(formatter: Intl.NumberFormat): readonly string[] {\n const { numberingSystem } = formatter.resolvedOptions();\n const plain = new Intl.NumberFormat(\"en\", { useGrouping: false, numberingSystem });\n return Array.from({ length: 10 }, (_, d) => plain.format(d));\n}\n\n/**\n * Splits a formatted number into parts keyed by what they are, not where they\n * sit in the string. Integer digits are keyed by place value counted from the\n * right, fraction digits by position after the point: adding a leading digit\n * (9 -> 10) leaves every existing column's key alone, so React keeps those\n * nodes and only the new place enters. Everything else is keyed by type, the\n * side of the integer it sits on, and its order among same-type parts there.\n */\nexport function splitNumber(\n formatter: Intl.NumberFormat,\n value: number,\n glyphs: readonly string[],\n): NumberPart[] {\n const values = new Map(glyphs.map((glyph, digit) => [glyph, digit] as const));\n const raw = formatter.formatToParts(value);\n const integerDigits = raw\n .filter((p) => p.type === \"integer\")\n .reduce((n, p) => n + Array.from(p.value).length, 0);\n\n const parts: NumberPart[] = [];\n const seen = new Map<string, number>();\n const textKey = (type: string, side: string): string => {\n const id = `${type}:${side}`;\n const n = seen.get(id) ?? 0;\n seen.set(id, n + 1);\n return `${id}:${n}`;\n };\n let side = \"pre\";\n let placed = 0;\n let fractionAt = 0;\n\n for (const { type, value: text } of raw) {\n if (type === \"integer\") {\n side = \"post\";\n for (const char of Array.from(text)) {\n const digit = values.get(char);\n const place = integerDigits - 1 - placed;\n placed += 1;\n parts.push(\n digit === undefined\n ? { kind: \"text\", key: textKey(\"integer\", side), text: char }\n : { kind: \"digit\", key: `int:${place}`, digit },\n );\n }\n } else if (type === \"fraction\") {\n for (const char of Array.from(text)) {\n const digit = values.get(char);\n const at = fractionAt;\n fractionAt += 1;\n parts.push(\n digit === undefined\n ? { kind: \"text\", key: textKey(\"fraction\", side), text: char }\n : { kind: \"digit\", key: `frac:${at}`, digit },\n );\n }\n } else if (type === \"group\") {\n parts.push({ kind: \"text\", key: `group:${integerDigits - 1 - placed}`, text });\n } else if (type === \"decimal\") {\n side = \"post\";\n parts.push({ kind: \"text\", key: \"decimal\", text });\n } else {\n parts.push({ kind: \"text\", key: textKey(type, side), text });\n }\n }\n return parts;\n}\n\n/**\n * Signed number of steps a digit column travels. `1` always goes up (9 -> 0\n * wraps forward), `-1` always down, `0` takes the shorter way (a tie goes up).\n */\nexport function rollStep(from: number, to: number, direction: RollDirection): number {\n const up = (((to - from) % 10) + 10) % 10;\n const down = (10 - up) % 10;\n if (direction === 1) return up;\n if (direction === -1) return -down;\n return up <= down ? up : -down;\n}\n\n/** Which way the whole number moved: `trend` when given, else the sign of the change. */\nexport function resolveDirection(\n trend: RollDirection | undefined,\n from: number,\n to: number,\n): RollDirection {\n if (trend !== undefined) return trend;\n return to > from ? 1 : to < from ? -1 : 0;\n}\n\nexport interface NumberItem {\n part: NumberPart;\n /** Still on screen only to finish its exit; no longer in the value. */\n exiting: boolean;\n /** Enters from zero width; false for the first render and for reduced motion. */\n fresh: boolean;\n}\n\n/**\n * Carries the previous items into the next value. A key that is gone stays as\n * an exiting item in its old position (after the nearest surviving neighbour\n * to its left), so it can collapse instead of vanishing. A key that comes back\n * while exiting is reused, not remounted.\n */\nexport function mergeItems(\n previous: readonly NumberItem[],\n next: readonly NumberPart[],\n animate: boolean,\n): NumberItem[] {\n const known = new Set(previous.map((item) => item.part.key));\n const result: NumberItem[] = next.map((part) => ({\n part,\n exiting: false,\n fresh: animate && !known.has(part.key),\n }));\n if (!animate) return result;\n\n const nextKeys = new Set(next.map((part) => part.key));\n let anchor = -1;\n for (const item of previous) {\n if (nextKeys.has(item.part.key)) {\n anchor = result.findIndex((r) => r.part.key === item.part.key);\n } else {\n anchor += 1;\n result.splice(anchor, 0, { part: item.part, exiting: true, fresh: false });\n }\n }\n return result;\n}\n"],"mappings":"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","names":["side"]}
|
package/dist/palette-cli.js
CHANGED
package/dist/palette.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ interface Contract {
|
|
|
13
13
|
derivedFrom: Readonly<Record<string, readonly string[]>>;
|
|
14
14
|
}
|
|
15
15
|
/** The rule the derived tokens live on — see `theme.css`, the contract half of `tokens.css`. */
|
|
16
|
-
declare const DERIVED_SCOPE = ":where(:root, .dark, .light, [data-brand])";
|
|
16
|
+
declare const DERIVED_SCOPE = ":where(:root, .dark, .light, [data-brand], [data-theme])";
|
|
17
|
+
/** The same scopes in another order: the button faces and chips, derived the same way. */
|
|
18
|
+
declare const FACES_SCOPE = ":where([data-theme], [data-brand], .light, .dark, :root)";
|
|
17
19
|
interface CssRule {
|
|
18
20
|
selector: string;
|
|
19
21
|
/** Custom properties declared directly in the rule, in source order. */
|
|
@@ -38,7 +40,7 @@ interface BrandConfig {
|
|
|
38
40
|
/** `--brand-font-<role>`: a family name per role; unset roles keep the kit's. */
|
|
39
41
|
fonts?: Partial<Record<FontRole, string>>;
|
|
40
42
|
}
|
|
41
|
-
type FontRole = "sans" | "
|
|
43
|
+
type FontRole = "sans" | "serif" | "mono";
|
|
42
44
|
declare const FONT_ROLES: readonly FontRole[];
|
|
43
45
|
/** Every problem with a brand file at once, so one run fixes them all. */
|
|
44
46
|
declare class PaletteError extends Error {
|
|
@@ -48,7 +50,7 @@ declare class PaletteError extends Error {
|
|
|
48
50
|
/**
|
|
49
51
|
* Reads the aquafix `brand.toml` shape: `[colors.light]` and `[colors.dark]`
|
|
50
52
|
* keyed by contract name, optional top-level `mark` / `mark-aspect`, and the
|
|
51
|
-
* family names in `[fonts]` (`sans` or `text`, `
|
|
53
|
+
* family names in `[fonts]` (`sans` or `text`, `serif`, `mono`). The rest of
|
|
52
54
|
* `[fonts]` and every other table (`[print]`, …) belong to other tools and are
|
|
53
55
|
* ignored — loading the font files is the site's job, not the palette's.
|
|
54
56
|
*/
|
|
@@ -77,4 +79,4 @@ interface TomlTable {
|
|
|
77
79
|
}
|
|
78
80
|
declare function parseToml(text: string): TomlTable;
|
|
79
81
|
|
|
80
|
-
export { type BrandConfig, type Contract, type CssRule, DERIVED_SCOPE, FONT_ROLES, type FontRole, POLARITIES, PaletteError, type PolarityName, type TomlTable, type TomlValue, brandFromToml, parseToml, readContract, readRules, renderPalette, validateBrand };
|
|
82
|
+
export { type BrandConfig, type Contract, type CssRule, DERIVED_SCOPE, FACES_SCOPE, FONT_ROLES, type FontRole, POLARITIES, PaletteError, type PolarityName, type TomlTable, type TomlValue, brandFromToml, parseToml, readContract, readRules, renderPalette, validateBrand };
|
package/dist/palette.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DERIVED_SCOPE,
|
|
3
|
+
FACES_SCOPE,
|
|
3
4
|
FONT_ROLES,
|
|
4
5
|
POLARITIES,
|
|
5
6
|
PaletteError,
|
|
@@ -9,9 +10,10 @@ import {
|
|
|
9
10
|
readRules,
|
|
10
11
|
renderPalette,
|
|
11
12
|
validateBrand
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-35SPIU4U.js";
|
|
13
14
|
export {
|
|
14
15
|
DERIVED_SCOPE,
|
|
16
|
+
FACES_SCOPE,
|
|
15
17
|
FONT_ROLES,
|
|
16
18
|
POLARITIES,
|
|
17
19
|
PaletteError,
|
|
@@ -7,11 +7,15 @@ function isStore(value) {
|
|
|
7
7
|
}
|
|
8
8
|
function sharedStore() {
|
|
9
9
|
const existing = Reflect.get(globalThis, STORE_KEY);
|
|
10
|
-
if (isStore(existing))
|
|
10
|
+
if (isStore(existing)) {
|
|
11
|
+
existing.listeners ??= /* @__PURE__ */ new Set();
|
|
12
|
+
return existing;
|
|
13
|
+
}
|
|
11
14
|
const store2 = {
|
|
12
15
|
stack: [],
|
|
13
16
|
info: /* @__PURE__ */ new WeakMap(),
|
|
14
17
|
seen: /* @__PURE__ */ new WeakMap(),
|
|
18
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
15
19
|
snapshot: (event) => {
|
|
16
20
|
if (!store2.seen.has(event)) store2.seen.set(event, store2.stack.slice());
|
|
17
21
|
}
|
|
@@ -55,6 +59,10 @@ function push(layer) {
|
|
|
55
59
|
const firstChild = stack.findIndex((other) => isDescendant(other, layer));
|
|
56
60
|
if (firstChild < 0) stack.push(layer);
|
|
57
61
|
else stack.splice(firstChild, 0, layer);
|
|
62
|
+
notify();
|
|
63
|
+
}
|
|
64
|
+
function notify() {
|
|
65
|
+
for (const listener of store.listeners) listener();
|
|
58
66
|
}
|
|
59
67
|
function pop(layer) {
|
|
60
68
|
const { stack } = store;
|
|
@@ -64,21 +72,84 @@ function pop(layer) {
|
|
|
64
72
|
window.removeEventListener("keydown", store.snapshot, true);
|
|
65
73
|
window.removeEventListener("pointerdown", store.snapshot, true);
|
|
66
74
|
}
|
|
75
|
+
notify();
|
|
67
76
|
}
|
|
68
77
|
const ParentLayerContext = React.createContext(null);
|
|
78
|
+
const renderedModal = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
const waitingForParent = /* @__PURE__ */ new WeakMap();
|
|
80
|
+
function liftAbove(child, parent) {
|
|
81
|
+
const node = child.current;
|
|
82
|
+
const parentNode = parent.current;
|
|
83
|
+
if (!node || !parentNode) return false;
|
|
84
|
+
if (!(parentNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING)) return true;
|
|
85
|
+
const view = node.ownerDocument.defaultView;
|
|
86
|
+
if (!view) return true;
|
|
87
|
+
const parentZ = Number.parseInt(view.getComputedStyle(parentNode).zIndex, 10);
|
|
88
|
+
const ownZ = Number.parseInt(view.getComputedStyle(node).zIndex, 10);
|
|
89
|
+
if (Number.isNaN(parentZ) || Number.isNaN(ownZ) || ownZ > parentZ) return true;
|
|
90
|
+
node.style.zIndex = String(parentZ + 2);
|
|
91
|
+
const scrim = node.previousElementSibling;
|
|
92
|
+
if (renderedModal.get(child) && scrim instanceof HTMLElement && scrim.dataset["slot"]?.endsWith("overlay")) {
|
|
93
|
+
scrim.style.zIndex = String(parentZ + 1);
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
function modalBelow(layer) {
|
|
98
|
+
const { stack } = store;
|
|
99
|
+
const index = stack.indexOf(layer);
|
|
100
|
+
const below = index < 0 ? stack : stack.slice(0, index);
|
|
101
|
+
return below.some((other) => other !== layer && store.info.get(other)?.modal === true);
|
|
102
|
+
}
|
|
103
|
+
function subscribe(listener) {
|
|
104
|
+
store.listeners.add(listener);
|
|
105
|
+
return () => store.listeners.delete(listener);
|
|
106
|
+
}
|
|
107
|
+
const CLEAR_SCRIM = Object.freeze({ backgroundColor: "transparent" });
|
|
108
|
+
function useModalScrimStyle(layer) {
|
|
109
|
+
const parent = React.useContext(ParentLayerContext);
|
|
110
|
+
const below = React.useSyncExternalStore(
|
|
111
|
+
subscribe,
|
|
112
|
+
() => modalBelow(layer),
|
|
113
|
+
() => false
|
|
114
|
+
);
|
|
115
|
+
return below || parent?.modalAbove === true ? CLEAR_SCRIM : void 0;
|
|
116
|
+
}
|
|
69
117
|
function DismissableLayerScope({ layer, children }) {
|
|
70
|
-
|
|
118
|
+
const outer = React.useContext(ParentLayerContext);
|
|
119
|
+
const outerModal = outer?.modalAbove ?? false;
|
|
120
|
+
const value = React.useMemo(
|
|
121
|
+
() => ({ layer, modalAbove: outerModal || renderedModal.get(layer) === true }),
|
|
122
|
+
[layer, outerModal]
|
|
123
|
+
);
|
|
124
|
+
return /* @__PURE__ */ jsx(ParentLayerContext.Provider, { value, children });
|
|
71
125
|
}
|
|
72
126
|
function useDismissableLayer(opts) {
|
|
73
127
|
const { enabled, onDismiss, exclude, pointerOutside = true, modal = false } = opts;
|
|
74
|
-
const parent = React.useContext(ParentLayerContext);
|
|
128
|
+
const parent = React.useContext(ParentLayerContext)?.layer ?? null;
|
|
75
129
|
const ref = React.useRef(null);
|
|
130
|
+
renderedModal.set(ref, modal);
|
|
76
131
|
const onDismissRef = React.useRef(onDismiss);
|
|
77
132
|
const excludeRef = React.useRef(exclude);
|
|
78
133
|
React.useEffect(() => {
|
|
79
134
|
onDismissRef.current = onDismiss;
|
|
80
135
|
excludeRef.current = exclude;
|
|
81
136
|
});
|
|
137
|
+
React.useLayoutEffect(() => {
|
|
138
|
+
if (!enabled) return;
|
|
139
|
+
const waiting = waitingForParent.get(ref);
|
|
140
|
+
if (waiting) {
|
|
141
|
+
waitingForParent.delete(ref);
|
|
142
|
+
for (const child of waiting) liftAbove(child, ref);
|
|
143
|
+
}
|
|
144
|
+
if (parent && !liftAbove(ref, parent)) {
|
|
145
|
+
const siblings = waitingForParent.get(parent) ?? /* @__PURE__ */ new Set();
|
|
146
|
+
siblings.add(ref);
|
|
147
|
+
waitingForParent.set(parent, siblings);
|
|
148
|
+
}
|
|
149
|
+
return () => {
|
|
150
|
+
if (parent) waitingForParent.get(parent)?.delete(ref);
|
|
151
|
+
};
|
|
152
|
+
}, [enabled, parent]);
|
|
82
153
|
React.useEffect(() => {
|
|
83
154
|
if (!enabled) return;
|
|
84
155
|
store.info.set(ref, { parent, modal });
|
|
@@ -108,6 +179,7 @@ function useDismissableLayer(opts) {
|
|
|
108
179
|
export {
|
|
109
180
|
DismissableLayerScope,
|
|
110
181
|
LAYER_IGNORE_ATTR,
|
|
111
|
-
useDismissableLayer
|
|
182
|
+
useDismissableLayer,
|
|
183
|
+
useModalScrimStyle
|
|
112
184
|
};
|
|
113
185
|
//# 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\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":";AA+HS;AA7HT,YAAY,WAAW;AA+BvB,MAAM,YAAY,uBAAO,IAAI,sCAAsC;AAEnE,SAAS,QAAQ,OAAqC;AACpD,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,WAAW,SAAS,MAAM,QAAQ,MAAM,KAAK;AACrG;AAEA,SAAS,cAA0B;AACjC,QAAM,WAAoB,QAAQ,IAAI,YAAY,SAAS;AAC3D,MAAI,QAAQ,QAAQ,EAAG,QAAO;AAC9B,QAAMA,SAAoB;AAAA,IACxB,OAAO,CAAC;AAAA,IACR,MAAM,oBAAI,QAAQ;AAAA,IAClB,MAAM,oBAAI,QAAQ;AAAA,IAClB,UAAU,CAAC,UAAU;AACnB,UAAI,CAACA,OAAM,KAAK,IAAI,KAAK,EAAG,CAAAA,OAAM,KAAK,IAAI,OAAOA,OAAM,MAAM,MAAM,CAAC;AAAA,IACvE;AAAA,EACF;AACA,UAAQ,IAAI,YAAY,WAAWA,MAAK;AACxC,SAAOA;AACT;AAEA,MAAM,QAAQ,YAAY;AAMnB,MAAM,oBAAoB;AAEjC,SAAS,SAAS,OAA0B;AAE1C,SAAO,MAAM,KAAK,IAAI,KAAK,KAAK,MAAM;AACxC;AAEA,SAAS,iBAAiB,KAAe,OAA8B;AACrE,QAAM,SAAS,MAAM;AACrB,MAAI,EAAE,kBAAkB,MAAO,QAAO;AACtC,QAAM,KAAK,kBAAkB,UAAU,SAAS,OAAO;AACvD,MAAI,IAAI,QAAQ,IAAI,iBAAiB,GAAG,EAAG,QAAO;AAClD,QAAM,SAAS,SAAS,KAAK;AAC7B,QAAM,QAAQ,OAAO,QAAQ,GAAG;AAEhC,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,QAAQ,OAAO,MAAM,QAAQ,CAAC;AAGpC,MAAI,MAAM,KAAK,CAAC,UAAU,MAAM,KAAK,IAAI,KAAK,GAAG,UAAU,IAAI,EAAG,QAAO;AACzE,SAAO,CAAC,MAAM,KAAK,CAAC,UAAU,MAAM,SAAS,SAAS,MAAM,KAAK,KAAK;AACxE;AAEA,SAAS,WAAW,KAAe,OAA+B;AAChE,QAAM,SAAS,SAAS,KAAK;AAC7B,SAAO,OAAO,OAAO,SAAS,CAAC,MAAM;AACvC;AAEA,SAAS,aAAa,OAAiB,UAA6B;AAClE,WAAS,IAAI,MAAM,KAAK,IAAI,KAAK,GAAG,UAAU,MAAM,GAAG,IAAI,MAAM,KAAK,IAAI,CAAC,GAAG,UAAU,MAAM;AAC5F,QAAI,MAAM,SAAU,QAAO;AAAA,EAC7B;AACA,SAAO;AACT;AAEA,SAAS,KAAK,OAAiB;AAC7B,QAAM,EAAE,MAAM,IAAI;AAClB,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,iBAAiB,WAAW,MAAM,UAAU,IAAI;AACvD,WAAO,iBAAiB,eAAe,MAAM,UAAU,IAAI;AAAA,EAC7D;AAIA,QAAM,aAAa,MAAM,UAAU,CAAC,UAAU,aAAa,OAAO,KAAK,CAAC;AACxE,MAAI,aAAa,EAAG,OAAM,KAAK,KAAK;AAAA,MAC/B,OAAM,OAAO,YAAY,GAAG,KAAK;AACxC;AAEA,SAAS,IAAI,OAAiB;AAC5B,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,MAAI,SAAS,EAAG,OAAM,OAAO,OAAO,CAAC;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,oBAAoB,WAAW,MAAM,UAAU,IAAI;AAC1D,WAAO,oBAAoB,eAAe,MAAM,UAAU,IAAI;AAAA,EAChE;AACF;AAEA,MAAM,qBAAqB,MAAM,cAA+B,IAAI;AAO7D,SAAS,sBAAsB,EAAE,OAAO,SAAS,GAAoD;AAC1G,SAAO,oBAAC,mBAAmB,UAAnB,EAA4B,OAAO,OAAQ,UAAS;AAC9D;AAgCO,SAAS,oBAAoB,MAQO;AACzC,QAAM,EAAE,SAAS,WAAW,SAAS,iBAAiB,MAAM,QAAQ,MAAM,IAAI;AAC9E,QAAM,SAAS,MAAM,WAAW,kBAAkB;AAClD,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,IAAI,KAAK,EAAE,QAAQ,MAAM,CAAC;AACrC,SAAK,GAAG;AAER,aAAS,cAAc,OAAqB;AAC1C,YAAM,SAAS,MAAM;AACrB,UAAI,EAAE,kBAAkB,MAAO;AAC/B,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,iBAAiB,KAAK,KAAK,EAAG,cAAa,QAAQ,KAAK;AAAA,IAC9D;AAEA,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,YAAY,WAAW,KAAK,KAAK,EAAG,cAAa,QAAQ,KAAK;AAAA,IAClF;AAEA,QAAI,eAAgB,UAAS,iBAAiB,eAAe,eAAe,IAAI;AAChF,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,UAAI,GAAG;AACP,eAAS,oBAAoB,eAAe,eAAe,IAAI;AAC/D,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,SAAS,gBAAgB,OAAO,MAAM,CAAC;AAE3C,SAAO;AACT;","names":["store"]}
|
|
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 // Who to tell when the stack changes (a modal asks whether another modal is\n // below it, to decide whether it draws the scrim).\n listeners: Set<() => 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 an incompatible 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)) {\n // an older copy created the store: it never notifies, so only this copy's listeners hear changes\n existing.listeners ??= new Set();\n return existing;\n }\n const store: LayerStore = {\n stack: [],\n info: new WeakMap(),\n seen: new WeakMap(),\n listeners: new Set(),\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 notify();\n}\n\nfunction notify() {\n for (const listener of store.listeners) listener();\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 notify();\n}\n\ninterface ParentLayer {\n layer: LayerRef;\n /** This layer or one above it in the React tree is a modal. */\n modalAbove: boolean;\n}\n\nconst ParentLayerContext = React.createContext<ParentLayer | null>(null);\n\n// Whether each layer asked for `modal`, known from its first render. The stack\n// learns it only in an effect, too late for the scope below a layer opened in\n// the same commit to tell its children that a modal is under them.\nconst renderedModal = new WeakMap<LayerRef, boolean>();\n\n// Layers waiting for their parent's node: a child's layout effect runs before\n// the parent's ref is attached when both open in one commit.\nconst waitingForParent = new WeakMap<LayerRef, Set<LayerRef>>();\n\n/**\n * Children's portals mount before their parent's when both open in one\n * commit, so with equal `z-index` the parent paints over the child and its\n * scrim. Lifts `child` (and the scrim rendered just before it) over `parent`\n * only in that case, so overlays that do not share a tree keep the stacking\n * their classes give them. Returns false while a node is still missing.\n */\nfunction liftAbove(child: LayerRef, parent: LayerRef): boolean {\n const node = child.current;\n const parentNode = parent.current;\n if (!node || !parentNode) return false;\n if (!(parentNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING)) return true;\n const view = node.ownerDocument.defaultView;\n if (!view) return true;\n const parentZ = Number.parseInt(view.getComputedStyle(parentNode).zIndex, 10);\n const ownZ = Number.parseInt(view.getComputedStyle(node).zIndex, 10);\n if (Number.isNaN(parentZ) || Number.isNaN(ownZ) || ownZ > parentZ) return true;\n node.style.zIndex = String(parentZ + 2);\n const scrim = node.previousElementSibling;\n // Only a modal renders its own scrim just before its panel; for anything\n // else that sibling belongs to someone else.\n if (renderedModal.get(child) && scrim instanceof HTMLElement && scrim.dataset[\"slot\"]?.endsWith(\"overlay\")) {\n scrim.style.zIndex = String(parentZ + 1);\n }\n return true;\n}\n\nfunction modalBelow(layer: LayerRef): boolean {\n const { stack } = store;\n const index = stack.indexOf(layer);\n // Not on the stack yet (first render) or no longer (exiting): whatever modals\n // are open are, or were, under it.\n const below = index < 0 ? stack : stack.slice(0, index);\n return below.some((other) => other !== layer && store.info.get(other)?.modal === true);\n}\n\nfunction subscribe(listener: () => void): () => void {\n store.listeners.add(listener);\n return () => store.listeners.delete(listener);\n}\n\nconst CLEAR_SCRIM: React.CSSProperties = Object.freeze({ backgroundColor: \"transparent\" });\n\n/**\n * The inline style for a modal's scrim. One scrim darkens the page however\n * many modals are stacked: the lowest draws it, the ones above get a clear\n * one (it still sits under their panel and catches presses for their layer),\n * so the panel below stays readable between the scrim and the top modal.\n * Pass the layer `useDismissableLayer` returned.\n */\nexport function useModalScrimStyle(layer: LayerRef): React.CSSProperties | undefined {\n const parent = React.useContext(ParentLayerContext);\n const below = React.useSyncExternalStore(\n subscribe,\n () => modalBelow(layer),\n () => false,\n );\n return below || parent?.modalAbove === true ? CLEAR_SCRIM : undefined;\n}\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 const outer = React.useContext(ParentLayerContext);\n const outerModal = outer?.modalAbove ?? false;\n const value = React.useMemo<ParentLayer>(\n () => ({ layer, modalAbove: outerModal || renderedModal.get(layer) === true }),\n [layer, outerModal],\n );\n return <ParentLayerContext.Provider value={value}>{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)?.layer ?? null;\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Idempotent per ref, so safe in render; see `renderedModal`.\n renderedModal.set(ref, modal);\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.useLayoutEffect(() => {\n if (!enabled) return;\n // Late children first (they ran before this layer's ref existed), then this\n // layer against its own parent.\n const waiting = waitingForParent.get(ref);\n if (waiting) {\n waitingForParent.delete(ref);\n for (const child of waiting) liftAbove(child, ref);\n }\n if (parent && !liftAbove(ref, parent)) {\n const siblings = waitingForParent.get(parent) ?? new Set<LayerRef>();\n siblings.add(ref);\n waitingForParent.set(parent, siblings);\n }\n return () => {\n if (parent) waitingForParent.get(parent)?.delete(ref);\n };\n }, [enabled, parent]);\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"]}
|
|
@@ -16,6 +16,11 @@ function focusable(root) {
|
|
|
16
16
|
(el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
|
+
let claimed = [];
|
|
20
|
+
function claim(root, saved) {
|
|
21
|
+
if (claimed.length === 0) queueMicrotask(() => claimed = []);
|
|
22
|
+
claimed.push({ root, saved });
|
|
23
|
+
}
|
|
19
24
|
function usable(el) {
|
|
20
25
|
return !!el && el !== document.body && el.isConnected;
|
|
21
26
|
}
|
|
@@ -31,10 +36,18 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
|
|
|
31
36
|
if (!enabled) return;
|
|
32
37
|
const root = ref.current;
|
|
33
38
|
if (!root) return;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));
|
|
40
|
+
const saved = {
|
|
41
|
+
el: inner ? inner.saved.el : document.activeElement,
|
|
42
|
+
behind: false
|
|
43
|
+
};
|
|
44
|
+
if (inner) inner.saved.behind = true;
|
|
45
|
+
else {
|
|
46
|
+
const first = initialFocus === "first" ? focusable(root)[0] : void 0;
|
|
47
|
+
if (first) first.focus();
|
|
48
|
+
else focusRoot(root);
|
|
49
|
+
}
|
|
50
|
+
claim(root, saved);
|
|
38
51
|
function onKeyDown(event) {
|
|
39
52
|
if (event.key !== "Tab" || !root) return;
|
|
40
53
|
const items = focusable(root);
|
|
@@ -59,9 +72,11 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
|
|
|
59
72
|
root.addEventListener("keydown", onKeyDown);
|
|
60
73
|
return () => {
|
|
61
74
|
root.removeEventListener("keydown", onKeyDown);
|
|
75
|
+
claimed = claimed.filter((c) => c.root !== root);
|
|
62
76
|
const active = document.activeElement;
|
|
63
77
|
if (active && active !== document.body && !root.contains(active)) return;
|
|
64
|
-
const
|
|
78
|
+
const anchor = fallbackRef.current?.current;
|
|
79
|
+
const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;
|
|
65
80
|
if (usable(target)) target.focus({ preventScroll: true });
|
|
66
81
|
};
|
|
67
82
|
}, [enabled, initialFocus]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n const
|
|
1
|
+
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\ninterface Claim {\n root: HTMLElement;\n // Boxed so an outer scope opened in the same commit can mark it `behind`.\n saved: { el: HTMLElement | null; behind: boolean };\n}\n\n// Scopes that took focus in the current task. Cleared in a microtask, so only\n// scopes opened in the same commit count, never one that has been open a while.\nlet claimed: Claim[] = [];\n\nfunction claim(root: HTMLElement, saved: Claim[\"saved\"]) {\n if (claimed.length === 0) queueMicrotask(() => (claimed = []));\n claimed.push({ root, saved });\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n // A scope opened in the same commit (a popover inside this dialog) has\n // already run, since children's effects come first, and taken focus. This\n // one must not pull focus out of it. Both saved the element focused before\n // either opened; the inner scope sits inside this one, so on close it goes\n // back to its own trigger first and to that element only as a fallback.\n const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));\n const saved: Claim[\"saved\"] = {\n el: inner ? inner.saved.el : (document.activeElement as HTMLElement | null),\n behind: false,\n };\n if (inner) inner.saved.behind = true;\n else {\n const first = initialFocus === \"first\" ? focusable(root)[0] : undefined;\n if (first) first.focus();\n else focusRoot(root);\n }\n claim(root, saved);\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key !== \"Tab\" || !root) return;\n const items = focusable(root);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const firstEl = items[0]!;\n const lastEl = items[items.length - 1]!;\n const active = document.activeElement;\n // From the root itself Shift+Tab would leave the scope for whatever\n // precedes the portal, so both directions are steered explicitly.\n if (active === root) {\n event.preventDefault();\n (event.shiftKey ? lastEl : firstEl).focus();\n } else if (event.shiftKey && active === firstEl) {\n event.preventDefault();\n lastEl.focus();\n } else if (!event.shiftKey && active === lastEl) {\n event.preventDefault();\n firstEl.focus();\n }\n }\n\n root.addEventListener(\"keydown\", onKeyDown);\n return () => {\n root.removeEventListener(\"keydown\", onKeyDown);\n claimed = claimed.filter((c) => c.root !== root);\n // Focus that already left the scope (a click outside landed on a field)\n // is the user's; only focus still inside, or dropped to <body>, returns.\n const active = document.activeElement;\n if (active && active !== document.body && !root.contains(active)) return;\n const anchor = fallbackRef.current?.current;\n const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;\n if (usable(target)) target.focus({ preventScroll: true });\n };\n }, [enabled, initialFocus]);\n\n return ref;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -21,6 +21,12 @@ function toContainingBlock(floating, x, y) {
|
|
|
21
21
|
function layoutSize(el) {
|
|
22
22
|
return { width: el.offsetWidth, height: el.offsetHeight };
|
|
23
23
|
}
|
|
24
|
+
function hasRunningAnimationAbove(el) {
|
|
25
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
26
|
+
if (node.getAnimations?.().some((a) => a.playState === "running")) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
24
30
|
function viewport() {
|
|
25
31
|
const el = document.documentElement;
|
|
26
32
|
return {
|
|
@@ -105,7 +111,15 @@ function useFloating(opts) {
|
|
|
105
111
|
const { top, left } = ideal(a, f, placed);
|
|
106
112
|
write(floating, f, top + shiftY, left + shiftX, true);
|
|
107
113
|
}
|
|
108
|
-
|
|
114
|
+
let frame = 0;
|
|
115
|
+
function follow() {
|
|
116
|
+
frame = 0;
|
|
117
|
+
place();
|
|
118
|
+
if (anchorRef.current && hasRunningAnimationAbove(anchorRef.current)) {
|
|
119
|
+
frame = requestAnimationFrame(follow);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
follow();
|
|
109
123
|
window.addEventListener("scroll", sync, true);
|
|
110
124
|
window.addEventListener("resize", place);
|
|
111
125
|
let observer;
|
|
@@ -115,6 +129,7 @@ function useFloating(opts) {
|
|
|
115
129
|
if (floatingRef.current) observer.observe(floatingRef.current);
|
|
116
130
|
}
|
|
117
131
|
return () => {
|
|
132
|
+
if (frame) cancelAnimationFrame(frame);
|
|
118
133
|
window.removeEventListener("scroll", sync, true);
|
|
119
134
|
window.removeEventListener("resize", place);
|
|
120
135
|
observer?.disconnect();
|