@marktiderman/genesis-ui 2.7.0 → 2.8.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/EXTENDING.md +40 -1
- package/SKILL.md +144 -9
- package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
- package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
- package/dist/chunk-4C4ESW6O.js.map +1 -0
- package/dist/{chunk-IXGAEECT.js → chunk-DWCAC2E7.js} +6 -3
- package/dist/chunk-DWCAC2E7.js.map +1 -0
- package/dist/chunk-FUVWKTPL.js +19 -0
- package/dist/chunk-FUVWKTPL.js.map +1 -0
- package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
- package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
- package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
- package/dist/chunk-OITWCIG2.js.map +1 -0
- package/dist/chunk-PKFDGGZV.js +65 -0
- package/dist/chunk-PKFDGGZV.js.map +1 -0
- package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
- package/dist/chunk-S7NXD37O.js.map +1 -0
- package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
- package/dist/chunk-UYI3GLFJ.js.map +1 -0
- package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
- package/dist/chunk-WGMEVX3Y.js.map +1 -0
- package/dist/chunk-ZDHWNQTR.js +552 -0
- package/dist/chunk-ZDHWNQTR.js.map +1 -0
- package/dist/components/data/index.d.ts +239 -15
- package/dist/components/data/index.js +522 -107
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/form-page.d.ts +1 -1
- package/dist/components/layout/form-page.js +1 -1
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +8 -8
- package/dist/components/patterns/index.d.ts +1 -1
- package/dist/components/patterns/index.js +5 -5
- package/dist/components/patterns/sortable-list.js +1 -1
- package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
- package/dist/components/rich-text/rich-text-editor.js +833 -0
- package/dist/components/rich-text/rich-text-editor.js.map +1 -0
- package/dist/components/rich-text/rich-text.d.ts +51 -0
- package/dist/components/rich-text/rich-text.js +4 -0
- package/dist/components/rich-text/rich-text.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -1
- package/dist/components/ui/alert-dialog.js +1 -1
- package/dist/hooks/index.d.ts +3 -3
- package/dist/hooks/index.js +4 -4
- package/dist/hooks/use-data-filters.d.ts +5 -3
- package/dist/hooks/use-data-filters.js +2 -2
- package/dist/hooks/use-resource-page.d.ts +2 -1
- package/dist/hooks/use-resource-page.js +3 -2
- package/dist/hooks/use-view-preference.d.ts +17 -2
- package/dist/hooks/use-view-preference.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +30 -30
- package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
- package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
- package/dist/router/react-router.d.ts +47 -0
- package/dist/router/react-router.js +53 -0
- package/dist/router/react-router.js.map +1 -0
- package/dist/router/tanstack-router.d.ts +47 -0
- package/dist/router/tanstack-router.js +105 -0
- package/dist/router/tanstack-router.js.map +1 -0
- package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-D4byuwAs.d.ts} +20 -3
- package/package.json +61 -10
- package/dist/chunk-CDYZRZ6U.js.map +0 -1
- package/dist/chunk-FTTNGGLA.js +0 -20
- package/dist/chunk-FTTNGGLA.js.map +0 -1
- package/dist/chunk-IXGAEECT.js.map +0 -1
- package/dist/chunk-JA3ZKULS.js.map +0 -1
- package/dist/chunk-M62CHO3N.js.map +0 -1
- package/dist/chunk-TXOZMKNW.js.map +0 -1
- package/dist/chunk-ZPPLUGLA.js.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useIsMobile } from './chunk-VFO3I7PV.js';
|
|
3
|
+
import { useCallback, useSyncExternalStore, useState } from 'react';
|
|
4
|
+
import { useStorage } from '@marktiderman/genesis-core';
|
|
5
|
+
|
|
6
|
+
var VIEW_MODES = ["grid", "table", "list"];
|
|
7
|
+
function isViewMode(value) {
|
|
8
|
+
return typeof value === "string" && VIEW_MODES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
11
|
+
function subscribe(onChange) {
|
|
12
|
+
listeners.add(onChange);
|
|
13
|
+
window.addEventListener("storage", onChange);
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(onChange);
|
|
16
|
+
window.removeEventListener("storage", onChange);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function getServerSnapshot() {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
var choiceKey = (key) => `genesis-view-choice-${key}`;
|
|
23
|
+
var legacyKey = (key) => `genesis-view-${key}`;
|
|
24
|
+
function useViewPreference(key, defaultMode = "grid") {
|
|
25
|
+
const storage = useStorage();
|
|
26
|
+
const storageKey = choiceKey(key);
|
|
27
|
+
const isMobile = useIsMobile();
|
|
28
|
+
const responsive = typeof defaultMode !== "string";
|
|
29
|
+
const getSnapshot = useCallback(
|
|
30
|
+
() => storage.getItem(storageKey),
|
|
31
|
+
[storage, storageKey]
|
|
32
|
+
);
|
|
33
|
+
const getLegacySnapshot = useCallback(
|
|
34
|
+
() => responsive ? null : storage.getItem(legacyKey(key)),
|
|
35
|
+
[storage, key, responsive]
|
|
36
|
+
);
|
|
37
|
+
const stored = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
38
|
+
const legacy = useSyncExternalStore(
|
|
39
|
+
subscribe,
|
|
40
|
+
getLegacySnapshot,
|
|
41
|
+
getServerSnapshot
|
|
42
|
+
);
|
|
43
|
+
const chosen = isViewMode(stored) ? stored : isViewMode(legacy) ? legacy : void 0;
|
|
44
|
+
const fallback = !responsive ? defaultMode : isMobile ? defaultMode.mobile : defaultMode.desktop;
|
|
45
|
+
const [unsaved, setUnsaved] = useState(void 0);
|
|
46
|
+
const setMode = useCallback(
|
|
47
|
+
(mode) => {
|
|
48
|
+
let saved = null;
|
|
49
|
+
try {
|
|
50
|
+
storage.setItem(storageKey, mode);
|
|
51
|
+
saved = storage.getItem(storageKey);
|
|
52
|
+
} catch {
|
|
53
|
+
saved = null;
|
|
54
|
+
}
|
|
55
|
+
setUnsaved(saved === mode ? void 0 : mode);
|
|
56
|
+
for (const listener of listeners) listener();
|
|
57
|
+
},
|
|
58
|
+
[storage, storageKey]
|
|
59
|
+
);
|
|
60
|
+
return [unsaved ?? chosen ?? fallback, setMode];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { useViewPreference };
|
|
64
|
+
//# sourceMappingURL=chunk-PKFDGGZV.js.map
|
|
65
|
+
//# sourceMappingURL=chunk-PKFDGGZV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-view-preference.ts"],"names":[],"mappings":";;;;AAwBA,IAAM,UAAA,GAAkC,CAAC,MAAA,EAAQ,OAAA,EAAS,MAAM,CAAA;AAEhE,SAAS,WAAW,KAAA,EAAmC;AACrD,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAa,UAAA,CAAiC,SAAS,KAAK,CAAA;AACtF;AAkBA,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAEtC,SAAS,UAAU,QAAA,EAAkC;AACnD,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,QAAQ,CAAA;AAC3C,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AACzB,IAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,QAAQ,CAAA;AAAA,EAChD,CAAA;AACF;AAGA,SAAS,iBAAA,GAA0B;AACjC,EAAA,OAAO,IAAA;AACT;AAWA,IAAM,SAAA,GAAY,CAAC,GAAA,KAAgB,CAAA,oBAAA,EAAuB,GAAG,CAAA,CAAA;AAC7D,IAAM,SAAA,GAAY,CAAC,GAAA,KAAgB,CAAA,aAAA,EAAgB,GAAG,CAAA,CAAA;AAE/C,SAAS,iBAAA,CACd,GAAA,EACA,WAAA,GAA+B,MAAA,EACO;AACtC,EAAA,MAAM,UAAU,UAAA,EAAW;AAC3B,EAAA,MAAM,UAAA,GAAa,UAAU,GAAG,CAAA;AAChC,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,MAAM,UAAA,GAAa,OAAO,WAAA,KAAgB,QAAA;AAO1C,EAAA,MAAM,WAAA,GAAc,WAAA;AAAA,IAClB,MAAM,OAAA,CAAQ,OAAA,CAAQ,UAAU,CAAA;AAAA,IAChC,CAAC,SAAS,UAAU;AAAA,GACtB;AACA,EAAA,MAAM,iBAAA,GAAoB,WAAA;AAAA,IACxB,MAAO,UAAA,GAAa,IAAA,GAAO,QAAQ,OAAA,CAAQ,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IACzD,CAAC,OAAA,EAAS,GAAA,EAAK,UAAU;AAAA,GAC3B;AACA,EAAA,MAAM,MAAA,GAAS,oBAAA,CAAqB,SAAA,EAAW,WAAA,EAAa,iBAAiB,CAAA;AAC7E,EAAA,MAAM,MAAA,GAAS,oBAAA;AAAA,IACb,SAAA;AAAA,IACA,iBAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,MAAA,GAAS,WAAW,MAAM,CAAA,GAC5B,SACA,UAAA,CAAW,MAAM,IACf,MAAA,GACA,MAAA;AAEN,EAAA,MAAM,WAAW,CAAC,UAAA,GACd,cACA,QAAA,GACI,WAAA,CAAY,SACZ,WAAA,CAAY,OAAA;AAKpB,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAA+B,MAAS,CAAA;AAEtE,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,IAAA,KAAmB;AAClB,MAAA,IAAI,KAAA,GAAuB,IAAA;AAC3B,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,OAAA,CAAQ,YAAY,IAAI,CAAA;AAChC,QAAA,KAAA,GAAQ,OAAA,CAAQ,QAAQ,UAAU,CAAA;AAAA,MACpC,CAAA,CAAA,MAAQ;AACN,QAAA,KAAA,GAAQ,IAAA;AAAA,MACV;AACA,MAAA,UAAA,CAAW,KAAA,KAAU,IAAA,GAAO,MAAA,GAAY,IAAI,CAAA;AAC5C,MAAA,KAAA,MAAW,QAAA,IAAY,WAAW,QAAA,EAAS;AAAA,IAC7C,CAAA;AAAA,IACA,CAAC,SAAS,UAAU;AAAA,GACtB;AAEA,EAAA,OAAO,CAAC,OAAA,IAAW,MAAA,IAAU,QAAA,EAAU,OAAO,CAAA;AAChD","file":"chunk-PKFDGGZV.js","sourcesContent":["\"use client\";\n\nimport { useState, useCallback, useSyncExternalStore } from \"react\";\nimport { useStorage } from \"@marktiderman/genesis-core\";\nimport type { ViewMode } from \"../components/patterns/view-toggle\";\nimport { useIsMobile } from \"./use-mobile\";\n\n/**\n * A default view that depends on the viewport: cards on a phone, a table on\n * a desktop. Only the DEFAULT is responsive — once the user picks a view it\n * is theirs on every screen size.\n *\n * @stability Beta\n */\nexport interface ResponsiveViewMode {\n /** Below the mobile breakpoint (768px). */\n mobile: ViewMode;\n /** At or above the mobile breakpoint. */\n desktop: ViewMode;\n}\n\n/** @stability Beta */\nexport type ViewModeDefault = ViewMode | ResponsiveViewMode;\n\nconst VIEW_MODES: readonly ViewMode[] = [\"grid\", \"table\", \"list\"];\n\nfunction isViewMode(value: unknown): value is ViewMode {\n return typeof value === \"string\" && (VIEW_MODES as readonly string[]).includes(value);\n}\n\n/**\n * The user's view mode for a list, persisted under `genesis-view-choice-<key>`.\n *\n * Only an explicit choice is written. The default is never stored: writing\n * it on mount would turn \"cards on phones\" into a stored choice the first\n * time the page rendered on a phone, and the desktop would then open in\n * cards too — the default would stop being a default. Likewise a later\n * change to the default reaches every user who has not chosen.\n *\n * SSR: the server renders the desktop default (it has no viewport), and a\n * phone switches to its mobile default right after hydration — the same\n * contract as `useIsMobile`, so there is no hydration mismatch.\n */\n// Same-tab subscribers: a choice made by one hook instance reaches every\n// other instance reading the same key (two lists of one resource, a header\n// toggle and a page body). Other tabs arrive through the `storage` event.\nconst listeners = new Set<() => void>();\n\nfunction subscribe(onChange: () => void): () => void {\n listeners.add(onChange);\n window.addEventListener(\"storage\", onChange);\n return () => {\n listeners.delete(onChange);\n window.removeEventListener(\"storage\", onChange);\n };\n}\n\n/** The server has no stored preference; hydrate against the default. */\nfunction getServerSnapshot(): null {\n return null;\n}\n\n/**\n * Storage keys. Earlier versions wrote the DEFAULT to `genesis-view-<key>`\n * on mount, so a value there may be a real choice or just a stale default —\n * there is no telling which. Explicit choices now go to their own key. The\n * legacy key is still honoured for a plain default (where a stale default\n * and the default agree, so reading it changes nothing), but not for a\n * responsive one, which it would otherwise pin to whatever the first device\n * stored.\n */\nconst choiceKey = (key: string) => `genesis-view-choice-${key}`;\nconst legacyKey = (key: string) => `genesis-view-${key}`;\n\nexport function useViewPreference(\n key: string,\n defaultMode: ViewModeDefault = \"grid\",\n): [ViewMode, (mode: ViewMode) => void] {\n const storage = useStorage();\n const storageKey = choiceKey(key);\n const isMobile = useIsMobile();\n const responsive = typeof defaultMode !== \"string\";\n\n // `useSyncExternalStore` rather than a `useState` initializer: the server\n // renders with no stored value, so reading storage during the client's\n // first render would mismatch the server's HTML. React hydrates against\n // `getServerSnapshot` and then re-renders with the stored choice; a\n // client-only render reads storage directly, with no flash.\n const getSnapshot = useCallback(\n () => storage.getItem(storageKey),\n [storage, storageKey],\n );\n const getLegacySnapshot = useCallback(\n () => (responsive ? null : storage.getItem(legacyKey(key))),\n [storage, key, responsive],\n );\n const stored = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n const legacy = useSyncExternalStore(\n subscribe,\n getLegacySnapshot,\n getServerSnapshot,\n );\n // `undefined` = the user has not chosen; fall through to the default.\n const chosen = isViewMode(stored)\n ? stored\n : isViewMode(legacy)\n ? legacy\n : undefined;\n\n const fallback = !responsive\n ? defaultMode\n : isMobile\n ? defaultMode.mobile\n : defaultMode.desktop;\n\n // Holds a choice storage did not keep (quota, private mode, a no-op\n // adapter), so it still applies for this hook's lifetime. Adapters\n // swallow write errors, so the write is checked by reading it back.\n const [unsaved, setUnsaved] = useState<ViewMode | undefined>(undefined);\n\n const setMode = useCallback(\n (mode: ViewMode) => {\n let saved: string | null = null;\n try {\n storage.setItem(storageKey, mode);\n saved = storage.getItem(storageKey);\n } catch {\n saved = null;\n }\n setUnsaved(saved === mode ? undefined : mode);\n for (const listener of listeners) listener();\n },\n [storage, storageKey],\n );\n\n return [unsaved ?? chosen ?? fallback, setMode];\n}\n"]}
|
|
@@ -31,9 +31,10 @@ function SortableList({
|
|
|
31
31
|
const [grabbed, setGrabbed] = React.useState(null);
|
|
32
32
|
const [preview, setPreview] = React.useState(null);
|
|
33
33
|
const [pending, setPending] = React.useState(false);
|
|
34
|
+
const pendingRef = React.useRef(false);
|
|
34
35
|
const [announcement, setAnnouncement] = React.useState("");
|
|
35
36
|
const handles = React.useRef(/* @__PURE__ */ new Map());
|
|
36
|
-
const
|
|
37
|
+
const restoreFocus = React.useRef(null);
|
|
37
38
|
const instructionsId = React.useId();
|
|
38
39
|
const byKey = new Map(items.map((item) => [getKey(item), item]));
|
|
39
40
|
const itemKeys = items.map(getKey);
|
|
@@ -44,26 +45,24 @@ function SortableList({
|
|
|
44
45
|
return item === void 0 ? "Item" : getItemLabel(item);
|
|
45
46
|
};
|
|
46
47
|
const announce = (message) => setAnnouncement((previous) => previous === message ? `${message}\xA0` : message);
|
|
47
|
-
const keepFocus = (
|
|
48
|
+
const keepFocus = () => {
|
|
48
49
|
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
49
|
-
|
|
50
|
+
restoreFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
50
51
|
};
|
|
51
|
-
const keysSignature = keys.join("\0");
|
|
52
52
|
useIsomorphicLayoutEffect(() => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (!
|
|
56
|
-
const handle = handles.current.get(key);
|
|
53
|
+
const element = restoreFocus.current;
|
|
54
|
+
restoreFocus.current = null;
|
|
55
|
+
if (!element || !element.isConnected) return;
|
|
57
56
|
const active = document.activeElement;
|
|
58
|
-
if (
|
|
59
|
-
}
|
|
57
|
+
if (active !== element && (active === null || active === document.body)) element.focus();
|
|
58
|
+
});
|
|
60
59
|
React.useEffect(() => {
|
|
61
60
|
if (disabled && (grabbed || armed || dragging)) {
|
|
62
61
|
setGrabbed(null);
|
|
63
62
|
setArmed(null);
|
|
64
63
|
setDragging(null);
|
|
65
64
|
setDropAt(null);
|
|
66
|
-
if (!
|
|
65
|
+
if (!pendingRef.current) setPreview(null);
|
|
67
66
|
return;
|
|
68
67
|
}
|
|
69
68
|
if (dragging && !byKey.has(dragging)) {
|
|
@@ -78,6 +77,7 @@ function SortableList({
|
|
|
78
77
|
});
|
|
79
78
|
const commit = (key, from, to, message) => {
|
|
80
79
|
if (from === to) return;
|
|
80
|
+
pendingRef.current = true;
|
|
81
81
|
setPending(true);
|
|
82
82
|
setPreview(moved(itemKeys, from, to));
|
|
83
83
|
let result;
|
|
@@ -90,21 +90,23 @@ function SortableList({
|
|
|
90
90
|
() => announce(message),
|
|
91
91
|
() => announce(`Couldn\u2019t move ${nameOf(key)}.`)
|
|
92
92
|
).finally(() => {
|
|
93
|
+
keepFocus();
|
|
94
|
+
pendingRef.current = false;
|
|
93
95
|
setPending(false);
|
|
94
96
|
setPreview(null);
|
|
95
97
|
});
|
|
96
98
|
};
|
|
97
99
|
const move = (key, to) => {
|
|
98
|
-
if (disabled ||
|
|
100
|
+
if (disabled || pendingRef.current || grabbed) return;
|
|
99
101
|
if (!Number.isFinite(to)) return;
|
|
100
102
|
const from = itemKeys.indexOf(key);
|
|
101
103
|
const target = Math.max(0, Math.min(total - 1, Math.round(to)));
|
|
102
104
|
if (from < 0 || target === from) return;
|
|
103
|
-
keepFocus(
|
|
105
|
+
keepFocus();
|
|
104
106
|
commit(key, from, target, `${nameOf(key)} moved to position ${target + 1} of ${total}.`);
|
|
105
107
|
};
|
|
106
108
|
const pickUp = (key) => {
|
|
107
|
-
if (disabled ||
|
|
109
|
+
if (disabled || pendingRef.current) return;
|
|
108
110
|
const from = itemKeys.indexOf(key);
|
|
109
111
|
if (from < 0) return;
|
|
110
112
|
setGrabbed({ key, from });
|
|
@@ -123,13 +125,13 @@ function SortableList({
|
|
|
123
125
|
announce(`${nameOf(key)} dropped at position ${to + 1} of ${total}.`);
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
126
|
-
keepFocus(
|
|
128
|
+
keepFocus();
|
|
127
129
|
commit(key, from, to, `${nameOf(key)} dropped at position ${to + 1} of ${total}.`);
|
|
128
130
|
};
|
|
129
131
|
const cancel = (key) => {
|
|
130
132
|
if (!grabbed) return;
|
|
131
133
|
const from = grabbed.from;
|
|
132
|
-
keepFocus(
|
|
134
|
+
keepFocus();
|
|
133
135
|
setGrabbed(null);
|
|
134
136
|
setPreview(null);
|
|
135
137
|
announce(`Move cancelled. ${nameOf(key)} is back at position ${from + 1} of ${total}.`);
|
|
@@ -138,7 +140,7 @@ function SortableList({
|
|
|
138
140
|
const index = keys.indexOf(key);
|
|
139
141
|
const target = Math.max(0, Math.min(total - 1, to));
|
|
140
142
|
if (index < 0 || target === index) return;
|
|
141
|
-
keepFocus(
|
|
143
|
+
keepFocus();
|
|
142
144
|
setPreview(moved(keys, index, target));
|
|
143
145
|
announce(`${nameOf(key)} moved to position ${target + 1} of ${total}.`);
|
|
144
146
|
};
|
|
@@ -156,11 +158,10 @@ function SortableList({
|
|
|
156
158
|
React.useEffect(() => {
|
|
157
159
|
if (!armed || dragging) return;
|
|
158
160
|
const disarm = () => setArmed(null);
|
|
159
|
-
|
|
160
|
-
window.addEventListener(
|
|
161
|
+
const events = ["pointerup", "pointercancel", "contextmenu", "blur"];
|
|
162
|
+
for (const type of events) window.addEventListener(type, disarm, { once: true });
|
|
161
163
|
return () => {
|
|
162
|
-
window.removeEventListener(
|
|
163
|
-
window.removeEventListener("pointercancel", disarm);
|
|
164
|
+
for (const type of events) window.removeEventListener(type, disarm);
|
|
164
165
|
};
|
|
165
166
|
}, [armed, dragging]);
|
|
166
167
|
const onHandleKeyDown = (event, key) => {
|
|
@@ -206,7 +207,7 @@ function SortableList({
|
|
|
206
207
|
const key = event.dataTransfer.getData(DRAG_TYPE) || dragging;
|
|
207
208
|
const target = dropAt;
|
|
208
209
|
endDrag();
|
|
209
|
-
if (!key || !target ||
|
|
210
|
+
if (!key || !target || pendingRef.current || grabbed) return;
|
|
210
211
|
const from = itemKeys.indexOf(key);
|
|
211
212
|
const over = itemKeys.indexOf(target.key);
|
|
212
213
|
if (from < 0 || over < 0) return;
|
|
@@ -237,13 +238,13 @@ function SortableList({
|
|
|
237
238
|
isGrabbed && "bg-accent text-foreground"
|
|
238
239
|
),
|
|
239
240
|
onPointerDown: () => {
|
|
240
|
-
if (!
|
|
241
|
+
if (!pendingRef.current && !grabbed) setArmed(key);
|
|
241
242
|
},
|
|
242
243
|
onPointerCancel: () => setArmed(null),
|
|
243
244
|
onClick: (event) => onHandleClick(event, key),
|
|
244
245
|
onKeyDown: (event) => onHandleKeyDown(event, key),
|
|
245
246
|
onBlur: () => {
|
|
246
|
-
if (isGrabbed &&
|
|
247
|
+
if (isGrabbed && restoreFocus.current !== handles.current.get(key)) cancel(key);
|
|
247
248
|
},
|
|
248
249
|
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4", "aria-hidden": true })
|
|
249
250
|
}
|
|
@@ -316,5 +317,5 @@ function SortableList({
|
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
export { SortableList };
|
|
319
|
-
//# sourceMappingURL=chunk-
|
|
320
|
-
//# sourceMappingURL=chunk-
|
|
320
|
+
//# sourceMappingURL=chunk-S7NXD37O.js.map
|
|
321
|
+
//# sourceMappingURL=chunk-S7NXD37O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/patterns/sortable-list.tsx"],"names":[],"mappings":"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","file":"chunk-S7NXD37O.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { GripVertical, MoreVertical } from \"lucide-react\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"../ui/dropdown-menu\";\nimport { cn } from \"../../utils\";\n\nexport interface SortableListItemContext {\n /** The item's position as shown, from 0. */\n index: number;\n /**\n * The drag handle: drag it with a pointer, or focus it and press Space or\n * Enter to pick the item up, the arrow keys to move it and Space or Enter\n * again to drop it. Render it somewhere in the row.\n */\n handle: React.ReactNode;\n /** True while this item is being dragged with a pointer. */\n isDragging: boolean;\n /** True while this item is picked up from the keyboard. */\n isGrabbed: boolean;\n /** Move this item without dragging, for a row menu of your own. */\n moveUp: () => void;\n moveDown: () => void;\n moveTo: (index: number) => void;\n}\n\nexport interface SortableListReorder {\n /** Key of the item that moved. */\n key: string;\n}\n\nexport interface SortableListProps<T> {\n items: readonly T[];\n /** A stable key per item. Required: dragging finds the item again by key. */\n getKey: (item: T) => string;\n /** A name for an item, for the handle's label and announcements (\"Week 1\"). */\n getItemLabel: (item: T) => string;\n renderItem: (item: T, context: SortableListItemContext) => React.ReactNode;\n /**\n * Move the item at `from` to `to` (both from 0, `to` counted after the item\n * is taken out). Each finished move calls this once: a keyboard pick-up\n * that wanders and comes back calls nothing.\n *\n * The list shows the new order straight away and keeps showing it until the\n * returned promise settles; by then `items` must be in the new order. A\n * synchronous handler must update `items` in the same tick. A rejected\n * promise puts the old order back and announces that the move failed. Other\n * moves wait until this one settles.\n */\n onReorder: (from: number, to: number, move: SortableListReorder) => void | Promise<void>;\n /** Accessible name of the list (\"Course steps\"). */\n label: string;\n /** Turns off dragging and every kind of move. */\n disabled?: boolean | undefined;\n /**\n * A \"Move\" menu on each row (up, down, to top, to bottom), on by default so\n * touch and screen-reader users always have a way to move an item. Turn it\n * off only when the row offers `moveUp` / `moveDown` / `moveTo` itself.\n */\n moveMenu?: boolean | undefined;\n className?: string | undefined;\n itemClassName?: string | undefined;\n}\n\ntype DropAt = { key: string; edge: \"before\" | \"after\" };\n\nconst DRAG_TYPE = \"application/x-genesis-sortable-item\";\n\n// useLayoutEffect warns during server rendering; nothing needs refocusing there.\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nfunction moved(keys: readonly string[], from: number, to: number) {\n const next = keys.slice();\n const [key] = next.splice(from, 1);\n next.splice(to, 0, key!);\n return next;\n}\n\n/**\n * A vertical list the user can rearrange: course steps, lesson blocks, form\n * fields.\n *\n * Every way to move an item is equivalent, so dragging is never the only one\n * (the same stance as KanbanBoard's \"Move to…\" menu):\n * - drag the handle with a pointer;\n * - focus the handle, Space or Enter to pick up, ↑/↓ and Home/End to move,\n * Space or Enter to drop, Escape to put it back (tabbing away also puts it\n * back); Alt+↑/↓ moves one step without picking up;\n * - the row's \"Move\" menu, or `moveUp` / `moveDown` / `moveTo` from the render\n * context.\n *\n * Moves are announced in a polite live region once they succeed. Native drag\n * and drop needs no dependency and Radix has none to offer (docs/FRAMEWORK.md\n * rule 3).\n *\n * @stability Beta\n */\nexport function SortableList<T>({\n items,\n getKey,\n getItemLabel,\n renderItem,\n onReorder,\n label,\n disabled,\n moveMenu = true,\n className,\n itemClassName,\n}: SortableListProps<T>) {\n const [armed, setArmed] = React.useState<string | null>(null);\n const [dragging, setDragging] = React.useState<string | null>(null);\n const [dropAt, setDropAt] = React.useState<DropAt | null>(null);\n // While a key is grabbed, `preview` is the order on screen; nothing is\n // written until the drop.\n const [grabbed, setGrabbed] = React.useState<{ key: string; from: number } | null>(null);\n // The order to show instead of `items`: during a keyboard pick-up, and from\n // a committed move until its promise settles.\n const [preview, setPreview] = React.useState<string[] | null>(null);\n const [pending, setPending] = React.useState(false);\n // The same, readable within one tick: two moves fired back to back must\n // not both start from the old order.\n const pendingRef = React.useRef(false);\n const [announcement, setAnnouncement] = React.useState(\"\");\n const handles = React.useRef(new Map<string, HTMLButtonElement>());\n // The element that had focus when a move started, to put focus back on if\n // the DOM move drops it to the body. Cleared after every commit.\n const restoreFocus = React.useRef<HTMLElement | null>(null);\n const instructionsId = React.useId();\n\n const byKey = new Map(items.map((item) => [getKey(item), item] as const));\n const itemKeys = items.map(getKey);\n const keys = preview && preview.length === itemKeys.length && preview.every((k) => byKey.has(k)) ? preview : itemKeys;\n const total = keys.length;\n const nameOf = (key: string) => {\n const item = byKey.get(key);\n return item === undefined ? \"Item\" : getItemLabel(item);\n };\n\n // A live region only speaks when its text changes, so the same message\n // twice in a row is told apart by a trailing no-break space.\n const announce = (message: string) =>\n setAnnouncement((previous) => (previous === message ? `${message} ` : message));\n\n // Moving a focused element in the DOM drops focus to the body in real\n // browsers. Remember what had focus, the handle or a row's own button, and\n // put it back after the commit.\n const keepFocus = () => {\n const active = typeof document === \"undefined\" ? null : document.activeElement;\n restoreFocus.current = active instanceof HTMLElement && active !== document.body ? active : null;\n };\n useIsomorphicLayoutEffect(() => {\n const element = restoreFocus.current;\n restoreFocus.current = null;\n if (!element || !element.isConnected) return;\n const active = document.activeElement;\n if (active !== element && (active === null || active === document.body)) element.focus();\n });\n\n // An item that disappears mid-gesture, or the list turning disabled, ends\n // the gesture.\n React.useEffect(() => {\n if (disabled && (grabbed || armed || dragging)) {\n setGrabbed(null);\n setArmed(null);\n setDragging(null);\n setDropAt(null);\n if (!pendingRef.current) setPreview(null);\n return;\n }\n if (dragging && !byKey.has(dragging)) {\n setDragging(null);\n setDropAt(null);\n setArmed(null);\n }\n if (grabbed && !byKey.has(grabbed.key)) {\n setGrabbed(null);\n setPreview(null);\n }\n });\n\n const commit = (key: string, from: number, to: number, message: string) => {\n if (from === to) return;\n pendingRef.current = true;\n setPending(true);\n setPreview(moved(itemKeys, from, to));\n // Called synchronously, so a synchronous handler updates `items` in the\n // same tick as the preview.\n let result: void | Promise<void>;\n try {\n result = onReorder(from, to, { key });\n } catch (error) {\n result = Promise.reject(error);\n }\n Promise.resolve(result)\n .then(\n () => announce(message),\n () => announce(`Couldn’t move ${nameOf(key)}.`),\n )\n .finally(() => {\n // A rejected move reverts the order, which moves rows again.\n keepFocus();\n pendingRef.current = false;\n setPending(false);\n setPreview(null);\n });\n };\n\n const move = (key: string, to: number) => {\n if (disabled || pendingRef.current || grabbed) return;\n if (!Number.isFinite(to)) return;\n const from = itemKeys.indexOf(key);\n const target = Math.max(0, Math.min(total - 1, Math.round(to)));\n if (from < 0 || target === from) return;\n keepFocus();\n commit(key, from, target, `${nameOf(key)} moved to position ${target + 1} of ${total}.`);\n };\n\n const pickUp = (key: string) => {\n if (disabled || pendingRef.current) return;\n const from = itemKeys.indexOf(key);\n if (from < 0) return;\n setGrabbed({ key, from });\n setPreview(itemKeys);\n announce(\n `${nameOf(key)} picked up, position ${from + 1} of ${total}. Use the up and down arrow keys to move, Space to drop, Escape to cancel.`,\n );\n };\n\n const drop = (key: string) => {\n if (!grabbed) return;\n const to = keys.indexOf(key);\n const from = grabbed.from;\n setGrabbed(null);\n if (to === from) {\n setPreview(null);\n announce(`${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n return;\n }\n keepFocus();\n commit(key, from, to, `${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n };\n\n const cancel = (key: string) => {\n if (!grabbed) return;\n const from = grabbed.from;\n keepFocus();\n setGrabbed(null);\n setPreview(null);\n announce(`Move cancelled. ${nameOf(key)} is back at position ${from + 1} of ${total}.`);\n };\n\n const step = (key: string, to: number) => {\n const index = keys.indexOf(key);\n const target = Math.max(0, Math.min(total - 1, to));\n if (index < 0 || target === index) return;\n keepFocus();\n setPreview(moved(keys, index, target));\n announce(`${nameOf(key)} moved to position ${target + 1} of ${total}.`);\n };\n\n // Escape while an item is picked up must not also close a surrounding\n // Dialog or Sheet, which listen before the handle does.\n React.useEffect(() => {\n if (!grabbed) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n event.preventDefault();\n event.stopPropagation();\n cancel(grabbed.key);\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n });\n\n // A press on the handle arms its row for native dragging; any release or\n // cancel, wherever it lands, disarms it.\n React.useEffect(() => {\n if (!armed || dragging) return;\n const disarm = () => setArmed(null);\n // A context menu or a release outside the window never sends pointerup.\n const events = [\"pointerup\", \"pointercancel\", \"contextmenu\", \"blur\"] as const;\n for (const type of events) window.addEventListener(type, disarm, { once: true });\n return () => {\n for (const type of events) window.removeEventListener(type, disarm);\n };\n }, [armed, dragging]);\n\n const onHandleKeyDown = (event: React.KeyboardEvent, key: string) => {\n if (disabled) return;\n const isGrabbed = grabbed?.key === key;\n const index = keys.indexOf(key);\n const delta = event.key === \"ArrowUp\" ? -1 : event.key === \"ArrowDown\" ? 1 : 0;\n if (isGrabbed) {\n if (delta) {\n event.preventDefault();\n step(key, index + delta);\n } else if (event.key === \"Home\" || event.key === \"End\") {\n event.preventDefault();\n step(key, event.key === \"Home\" ? 0 : total - 1);\n }\n return;\n }\n if (delta && event.altKey) {\n event.preventDefault();\n move(key, index + delta);\n }\n };\n\n // Space and Enter reach the handle as a click with `detail === 0`, as do\n // screen readers' activations; a pointer click is a press for dragging.\n const onHandleClick = (event: React.MouseEvent, key: string) => {\n if (disabled || event.detail !== 0) return;\n if (grabbed?.key === key) drop(key);\n else if (!grabbed) pickUp(key);\n };\n\n const onDragOver = (event: React.DragEvent<HTMLLIElement>, key: string) => {\n if (!dragging) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = \"move\";\n const rect = event.currentTarget.getBoundingClientRect();\n const edge = event.clientY < rect.top + rect.height / 2 ? \"before\" : \"after\";\n if (dropAt?.key !== key || dropAt.edge !== edge) setDropAt({ key, edge });\n };\n\n const endDrag = () => {\n setDragging(null);\n setDropAt(null);\n setArmed(null);\n };\n\n const onDrop = (event: React.DragEvent) => {\n event.preventDefault();\n const key = event.dataTransfer.getData(DRAG_TYPE) || dragging;\n const target = dropAt;\n endDrag();\n if (!key || !target || pendingRef.current || grabbed) return;\n const from = itemKeys.indexOf(key);\n const over = itemKeys.indexOf(target.key);\n if (from < 0 || over < 0) return;\n // `to` counts positions after the dragged item is taken out.\n let to = target.edge === \"before\" ? over : over + 1;\n if (from < to) to -= 1;\n commit(key, from, to, `${nameOf(key)} dropped at position ${to + 1} of ${total}.`);\n };\n\n return (\n <>\n <ul aria-label={label} aria-busy={pending || undefined} className={cn(\"flex flex-col gap-1\", className)}>\n {keys.map((key, index) => {\n const item = byKey.get(key)!;\n const itemLabel = getItemLabel(item);\n const isGrabbed = grabbed?.key === key;\n const isDragging = dragging === key;\n const handle = disabled ? null : (\n <button\n type=\"button\"\n ref={(node) => {\n if (node) handles.current.set(key, node);\n else handles.current.delete(key);\n }}\n aria-label={`Reorder ${itemLabel}`}\n aria-describedby={instructionsId}\n data-grabbed={isGrabbed || undefined}\n className={cn(\n \"inline-flex h-7 w-5 shrink-0 cursor-grab items-center justify-center rounded text-muted-foreground\",\n \"hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isGrabbed && \"bg-accent text-foreground\",\n )}\n onPointerDown={() => {\n if (!pendingRef.current && !grabbed) setArmed(key);\n }}\n onPointerCancel={() => setArmed(null)}\n onClick={(event) => onHandleClick(event, key)}\n onKeyDown={(event) => onHandleKeyDown(event, key)}\n onBlur={() => {\n // A DOM move during the pick-up blurs the handle for a moment;\n // that is not the user leaving.\n if (isGrabbed && restoreFocus.current !== handles.current.get(key)) cancel(key);\n }}\n >\n <GripVertical className=\"h-4 w-4\" aria-hidden />\n </button>\n );\n return (\n <li\n key={key}\n data-sortable-key={key}\n draggable={(!disabled && armed === key) || undefined}\n className={cn(\n \"relative rounded-md\",\n moveMenu && !disabled && \"pr-8\",\n isDragging && \"opacity-50\",\n isGrabbed && \"ring-2 ring-primary/50\",\n itemClassName,\n )}\n onDragStart={(event) => {\n if (disabled || armed !== key) return;\n event.dataTransfer.setData(DRAG_TYPE, key);\n event.dataTransfer.effectAllowed = \"move\";\n setDragging(key);\n }}\n onDragEnd={endDrag}\n onDragOver={(event) => onDragOver(event, key)}\n onDrop={onDrop}\n >\n {dropAt?.key === key && dragging !== key && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute inset-x-0 h-0.5 rounded bg-primary\",\n dropAt.edge === \"before\" ? \"-top-0.5\" : \"-bottom-0.5\",\n )}\n />\n )}\n {renderItem(item, {\n index,\n handle,\n isDragging,\n isGrabbed,\n moveUp: () => move(key, index - 1),\n moveDown: () => move(key, index + 1),\n moveTo: (to) => move(key, to),\n })}\n {moveMenu && !disabled && total > 1 && (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label={`Move ${itemLabel}`}\n className=\"absolute right-1 top-1 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MoreVertical className=\"h-4 w-4\" aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem disabled={index === 0} onSelect={() => move(key, index - 1)}>\n Move up\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === total - 1} onSelect={() => move(key, index + 1)}>\n Move down\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === 0} onSelect={() => move(key, 0)}>\n Move to top\n </DropdownMenuItem>\n <DropdownMenuItem disabled={index === total - 1} onSelect={() => move(key, total - 1)}>\n Move to bottom\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </li>\n );\n })}\n </ul>\n <p id={instructionsId} hidden>\n Space or Enter picks the item up. Up and down arrows, Home and End move it. Space or Enter drops it, Escape puts\n it back. Alt with an arrow moves it one step.\n </p>\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {announcement}\n </div>\n </>\n );\n}\n"]}
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { ViewToggle, ViewSettings } from './chunk-46TZGMFW.js';
|
|
3
3
|
import { Collapsible, CollapsibleContent } from './chunk-B2GIBUQD.js';
|
|
4
4
|
import { Badge } from './chunk-NPNADZHM.js';
|
|
5
|
-
import { Popover, PopoverTrigger, PopoverContent } from './chunk-22XB3Q6H.js';
|
|
6
5
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './chunk-QJ6P2DW6.js';
|
|
7
6
|
import { Switch } from './chunk-27ELYTGE.js';
|
|
7
|
+
import { Popover, PopoverTrigger, PopoverContent } from './chunk-22XB3Q6H.js';
|
|
8
8
|
import { Button } from './chunk-NWSCYLPT.js';
|
|
9
9
|
import { Label } from './chunk-F47U5KNW.js';
|
|
10
10
|
import { cn } from './chunk-NZ5WBA4R.js';
|
|
11
|
-
import { DefaultLink, isPathActive } from './chunk-
|
|
11
|
+
import { resolveNavigation, DefaultLink, isPathActive } from './chunk-WGMEVX3Y.js';
|
|
12
12
|
import { useId, useState, useEffect, useMemo, useCallback } from 'react';
|
|
13
|
-
import { ListFilter, MoreHorizontal, SlidersHorizontal, Menu, X, PanelLeft, PanelLeftClose, Sun, Moon, ChevronDown } from 'lucide-react';
|
|
13
|
+
import { ListFilter, MoreHorizontal, SlidersHorizontal, Menu, ArrowLeft, X, PanelLeft, PanelLeftClose, Sun, Moon, ChevronDown } from 'lucide-react';
|
|
14
14
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
15
15
|
|
|
16
16
|
// src/option-storage.ts
|
|
@@ -38,6 +38,7 @@ function pluralizeEntity(entity, count) {
|
|
|
38
38
|
return entity.endsWith("s") ? entity : `${entity}s`;
|
|
39
39
|
}
|
|
40
40
|
var OPTION_DEFAULTS = {
|
|
41
|
+
showTitle: true,
|
|
41
42
|
showIcon: true,
|
|
42
43
|
showSubtitle: true,
|
|
43
44
|
showCount: true,
|
|
@@ -343,7 +344,14 @@ function PageHeader({
|
|
|
343
344
|
] })
|
|
344
345
|
] }) : null
|
|
345
346
|
] });
|
|
346
|
-
const titleRow = /* @__PURE__ */
|
|
347
|
+
const titleRow = resolved.showTitle === false ? /* @__PURE__ */ jsx(
|
|
348
|
+
"div",
|
|
349
|
+
{
|
|
350
|
+
"data-testid": "page-header-toolbar",
|
|
351
|
+
className: "flex min-w-0 flex-wrap items-center justify-end gap-2",
|
|
352
|
+
children: actionsCluster
|
|
353
|
+
}
|
|
354
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
|
|
347
355
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
|
|
348
356
|
Icon && resolved.showIcon !== false ? /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-primary/10", children: /* @__PURE__ */ jsx(Icon, { className: "h-5 w-5 text-primary" }) }) : null,
|
|
349
357
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
@@ -416,6 +424,34 @@ function writeStoredCollapsed(key, collapsed) {
|
|
|
416
424
|
} catch {
|
|
417
425
|
}
|
|
418
426
|
}
|
|
427
|
+
var PARAM = /^\$(.+)$/;
|
|
428
|
+
function fillParams(to, params) {
|
|
429
|
+
const cut = to.search(/[?#]/);
|
|
430
|
+
const path = cut === -1 ? to : to.slice(0, cut);
|
|
431
|
+
const suffix = cut === -1 ? "" : to.slice(cut);
|
|
432
|
+
let missing = false;
|
|
433
|
+
const filled = path.split("/").map((part) => {
|
|
434
|
+
const name = PARAM.exec(part)?.[1];
|
|
435
|
+
if (name == null) return part;
|
|
436
|
+
const value = params[name];
|
|
437
|
+
if (value == null) missing = true;
|
|
438
|
+
return value == null ? part : encodeURIComponent(value);
|
|
439
|
+
}).join("/");
|
|
440
|
+
return missing ? void 0 : filled + suffix;
|
|
441
|
+
}
|
|
442
|
+
function fillItems(items, params) {
|
|
443
|
+
return items.flatMap((item) => {
|
|
444
|
+
const to = fillParams(item.to, params);
|
|
445
|
+
if (to == null) return [];
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
...item,
|
|
449
|
+
to,
|
|
450
|
+
children: item.children ? fillItems(item.children, params) : void 0
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
});
|
|
454
|
+
}
|
|
419
455
|
function ThemeToggle({ testId }) {
|
|
420
456
|
const [dark, setDark] = useState(false);
|
|
421
457
|
useEffect(() => {
|
|
@@ -464,7 +500,8 @@ function NavLinkRow({
|
|
|
464
500
|
onNavClick,
|
|
465
501
|
linkComponent: LinkComp,
|
|
466
502
|
activePath,
|
|
467
|
-
isActive
|
|
503
|
+
isActive,
|
|
504
|
+
notCurrent = false
|
|
468
505
|
}) {
|
|
469
506
|
const active = itemIsActive(item, activePath, isActive);
|
|
470
507
|
const pad = density === "compact" ? "px-2.5 py-1.5" : "px-3 py-2";
|
|
@@ -493,7 +530,7 @@ function NavLinkRow({
|
|
|
493
530
|
onClick: onNavClick,
|
|
494
531
|
title: collapsed ? item.label : void 0,
|
|
495
532
|
"aria-label": collapsed ? item.label : void 0,
|
|
496
|
-
"aria-current": active ? "page" : void 0,
|
|
533
|
+
"aria-current": active && !notCurrent ? "page" : void 0,
|
|
497
534
|
className: cn(
|
|
498
535
|
"flex items-center gap-3 rounded-lg text-sm font-medium transition-colors",
|
|
499
536
|
pad,
|
|
@@ -517,7 +554,8 @@ function NavGroup({
|
|
|
517
554
|
onNavClick,
|
|
518
555
|
linkComponent,
|
|
519
556
|
activePath,
|
|
520
|
-
isActive
|
|
557
|
+
isActive,
|
|
558
|
+
notCurrent = false
|
|
521
559
|
}) {
|
|
522
560
|
const children = item.children ?? [];
|
|
523
561
|
const childActive = children.some(
|
|
@@ -537,7 +575,8 @@ function NavGroup({
|
|
|
537
575
|
onNavClick,
|
|
538
576
|
linkComponent,
|
|
539
577
|
activePath,
|
|
540
|
-
isActive
|
|
578
|
+
isActive,
|
|
579
|
+
notCurrent
|
|
541
580
|
}
|
|
542
581
|
);
|
|
543
582
|
}
|
|
@@ -552,7 +591,8 @@ function NavGroup({
|
|
|
552
591
|
onNavClick,
|
|
553
592
|
linkComponent,
|
|
554
593
|
activePath,
|
|
555
|
-
isActive
|
|
594
|
+
isActive,
|
|
595
|
+
notCurrent
|
|
556
596
|
}
|
|
557
597
|
);
|
|
558
598
|
}
|
|
@@ -575,7 +615,7 @@ function NavGroup({
|
|
|
575
615
|
{
|
|
576
616
|
href: item.to,
|
|
577
617
|
onClick: onNavClick,
|
|
578
|
-
"aria-current": selfExact ? "page" : void 0,
|
|
618
|
+
"aria-current": selfExact && !notCurrent ? "page" : void 0,
|
|
579
619
|
className: parentClass,
|
|
580
620
|
children: [
|
|
581
621
|
/* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
|
|
@@ -633,7 +673,8 @@ function NavGroup({
|
|
|
633
673
|
onNavClick,
|
|
634
674
|
linkComponent,
|
|
635
675
|
activePath,
|
|
636
|
-
isActive
|
|
676
|
+
isActive,
|
|
677
|
+
notCurrent
|
|
637
678
|
},
|
|
638
679
|
child.to + child.label
|
|
639
680
|
)) }) })
|
|
@@ -641,6 +682,8 @@ function NavGroup({
|
|
|
641
682
|
}
|
|
642
683
|
function SidebarNav({
|
|
643
684
|
navItems,
|
|
685
|
+
contextualNavItems,
|
|
686
|
+
contextualNavLabel,
|
|
644
687
|
secondaryNavItems,
|
|
645
688
|
secondaryNavLabel,
|
|
646
689
|
showSecondary,
|
|
@@ -651,6 +694,10 @@ function SidebarNav({
|
|
|
651
694
|
activePath,
|
|
652
695
|
isActive
|
|
653
696
|
}) {
|
|
697
|
+
const contextualLabelId = useId();
|
|
698
|
+
const contextualActive = Boolean(
|
|
699
|
+
contextualNavItems?.some((item) => itemIsActive(item, activePath, isActive))
|
|
700
|
+
);
|
|
654
701
|
return /* @__PURE__ */ jsxs(
|
|
655
702
|
"nav",
|
|
656
703
|
{
|
|
@@ -669,10 +716,47 @@ function SidebarNav({
|
|
|
669
716
|
onNavClick,
|
|
670
717
|
linkComponent,
|
|
671
718
|
activePath,
|
|
672
|
-
isActive
|
|
719
|
+
isActive,
|
|
720
|
+
notCurrent: contextualActive
|
|
673
721
|
},
|
|
674
722
|
item.to + item.label
|
|
675
723
|
)),
|
|
724
|
+
contextualNavItems && contextualNavItems.length > 0 ? /* @__PURE__ */ jsxs(
|
|
725
|
+
"div",
|
|
726
|
+
{
|
|
727
|
+
className: cn(
|
|
728
|
+
"mt-4 space-y-1 border-t border-sidebar-border pt-4",
|
|
729
|
+
collapsed && "mt-2 pt-2"
|
|
730
|
+
),
|
|
731
|
+
"data-testid": "app-shell-contextual-nav",
|
|
732
|
+
role: "group",
|
|
733
|
+
"aria-labelledby": !collapsed && contextualNavLabel ? contextualLabelId : void 0,
|
|
734
|
+
"aria-label": collapsed ? contextualNavLabel : void 0,
|
|
735
|
+
children: [
|
|
736
|
+
!collapsed && contextualNavLabel ? /* @__PURE__ */ jsx(
|
|
737
|
+
"p",
|
|
738
|
+
{
|
|
739
|
+
id: contextualLabelId,
|
|
740
|
+
className: "mb-2 truncate px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/40",
|
|
741
|
+
children: contextualNavLabel
|
|
742
|
+
}
|
|
743
|
+
) : null,
|
|
744
|
+
contextualNavItems.map((item) => /* @__PURE__ */ jsx(
|
|
745
|
+
NavGroup,
|
|
746
|
+
{
|
|
747
|
+
item,
|
|
748
|
+
collapsed,
|
|
749
|
+
density,
|
|
750
|
+
onNavClick,
|
|
751
|
+
linkComponent,
|
|
752
|
+
activePath,
|
|
753
|
+
isActive
|
|
754
|
+
},
|
|
755
|
+
item.to + item.label
|
|
756
|
+
))
|
|
757
|
+
]
|
|
758
|
+
}
|
|
759
|
+
) : null,
|
|
676
760
|
showSecondary && secondaryNavItems && secondaryNavItems.length > 0 ? /* @__PURE__ */ jsxs(
|
|
677
761
|
"div",
|
|
678
762
|
{
|
|
@@ -729,7 +813,9 @@ function AppShell({
|
|
|
729
813
|
appName,
|
|
730
814
|
appLogo,
|
|
731
815
|
logo,
|
|
732
|
-
navItems,
|
|
816
|
+
navItems: navItemsProp,
|
|
817
|
+
navigation,
|
|
818
|
+
parentNavigation: parentNavigationProp,
|
|
733
819
|
secondaryNavItems,
|
|
734
820
|
secondaryNavLabel = "More",
|
|
735
821
|
mobileNavItems,
|
|
@@ -748,6 +834,18 @@ function AppShell({
|
|
|
748
834
|
isActive = isPathActive
|
|
749
835
|
}) {
|
|
750
836
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
837
|
+
const derived = useMemo(() => {
|
|
838
|
+
if (!navigation) return void 0;
|
|
839
|
+
const nav = resolveNavigation(navigation, activePath ?? "/");
|
|
840
|
+
const atRoot = nav.trail.length === 1;
|
|
841
|
+
const globalItems = fillItems(navigation.items, nav.params);
|
|
842
|
+
const contextualItems = atRoot ? [] : fillItems(nav.items, nav.params);
|
|
843
|
+
const parent = nav.parent && nav.trail.length > 2 && !nav.parent.path.split("/").some((p) => PARAM.test(p)) ? { href: nav.parent.path, label: nav.parent.label } : void 0;
|
|
844
|
+
return { current: nav.current, globalItems, contextualItems, parent };
|
|
845
|
+
}, [navigation, activePath]);
|
|
846
|
+
const navItems = navItemsProp ?? derived?.globalItems ?? [];
|
|
847
|
+
const contextualNavItems = derived?.contextualItems ?? [];
|
|
848
|
+
const parentNavigation = parentNavigationProp === void 0 ? derived?.parent : parentNavigationProp ?? void 0;
|
|
751
849
|
const [userOverrides, setUserOverrides] = useState(
|
|
752
850
|
{}
|
|
753
851
|
);
|
|
@@ -827,11 +925,15 @@ function AppShell({
|
|
|
827
925
|
}
|
|
828
926
|
onCollapsedChange?.(next);
|
|
829
927
|
};
|
|
928
|
+
const ParentLink = linkComponent;
|
|
830
929
|
const density = resolved.density ?? "comfortable";
|
|
831
930
|
const scrollWindow = resolved.scrollRoot === "window";
|
|
832
931
|
const railExpanded = resolved.railWidth === "wide" ? "md:w-64" : "md:w-52";
|
|
833
932
|
const railClass = collapsed ? "md:w-14" : railExpanded;
|
|
834
|
-
const
|
|
933
|
+
const leaves = (items) => items.filter((i) => !i.children || i.children.length === 0);
|
|
934
|
+
const contextualLeaves = leaves(contextualNavItems);
|
|
935
|
+
const bottomItems = (mobileNavItems ?? (contextualLeaves.length > 0 ? contextualLeaves : leaves(navItems))).filter((i) => !i.disabled);
|
|
936
|
+
const bottomNotCurrent = !mobileNavItems && contextualLeaves.length === 0 && contextualNavItems.some((i) => itemIsActive(i, activePath, isActive));
|
|
835
937
|
const toggles = [
|
|
836
938
|
{ key: "showLogo", label: "Logo", available: hasLogo },
|
|
837
939
|
{ key: "showAppName", label: "App name", available: true },
|
|
@@ -981,6 +1083,8 @@ function AppShell({
|
|
|
981
1083
|
SidebarNav,
|
|
982
1084
|
{
|
|
983
1085
|
navItems,
|
|
1086
|
+
contextualNavItems,
|
|
1087
|
+
contextualNavLabel: derived?.current.label,
|
|
984
1088
|
secondaryNavItems,
|
|
985
1089
|
secondaryNavLabel,
|
|
986
1090
|
showSecondary,
|
|
@@ -1100,7 +1204,7 @@ function AppShell({
|
|
|
1100
1204
|
]
|
|
1101
1205
|
}
|
|
1102
1206
|
),
|
|
1103
|
-
/* @__PURE__ */
|
|
1207
|
+
/* @__PURE__ */ jsxs(
|
|
1104
1208
|
"main",
|
|
1105
1209
|
{
|
|
1106
1210
|
className: cn(
|
|
@@ -1108,7 +1212,46 @@ function AppShell({
|
|
|
1108
1212
|
!scrollWindow && "overflow-y-auto",
|
|
1109
1213
|
showMobileBottom ? "pb-16 md:pb-0" : "pb-0"
|
|
1110
1214
|
),
|
|
1111
|
-
children:
|
|
1215
|
+
children: [
|
|
1216
|
+
parentNavigation ? /* @__PURE__ */ jsx(
|
|
1217
|
+
"div",
|
|
1218
|
+
{
|
|
1219
|
+
className: "flex min-w-0 items-center px-4 pt-4 sm:px-6 sm:pt-6 lg:px-8 lg:pt-8",
|
|
1220
|
+
"data-testid": "app-shell-parent-nav",
|
|
1221
|
+
children: /* @__PURE__ */ jsx(
|
|
1222
|
+
Button,
|
|
1223
|
+
{
|
|
1224
|
+
asChild: true,
|
|
1225
|
+
variant: "ghost",
|
|
1226
|
+
size: "sm",
|
|
1227
|
+
className: "-ml-3 min-w-0 max-w-full",
|
|
1228
|
+
testID: "app-shell-parent-link",
|
|
1229
|
+
children: /* @__PURE__ */ jsxs(
|
|
1230
|
+
ParentLink,
|
|
1231
|
+
{
|
|
1232
|
+
href: parentNavigation.href,
|
|
1233
|
+
"aria-label": `Back to ${parentNavigation.label}`,
|
|
1234
|
+
children: [
|
|
1235
|
+
/* @__PURE__ */ jsx(ArrowLeft, { "aria-hidden": true }),
|
|
1236
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: parentNavigation.label })
|
|
1237
|
+
]
|
|
1238
|
+
}
|
|
1239
|
+
)
|
|
1240
|
+
}
|
|
1241
|
+
)
|
|
1242
|
+
}
|
|
1243
|
+
) : null,
|
|
1244
|
+
/* @__PURE__ */ jsx(
|
|
1245
|
+
"div",
|
|
1246
|
+
{
|
|
1247
|
+
className: cn(
|
|
1248
|
+
"min-w-0 p-4 sm:p-6 lg:p-8",
|
|
1249
|
+
parentNavigation && "pt-2 sm:pt-3 lg:pt-4"
|
|
1250
|
+
),
|
|
1251
|
+
children
|
|
1252
|
+
}
|
|
1253
|
+
)
|
|
1254
|
+
]
|
|
1112
1255
|
}
|
|
1113
1256
|
),
|
|
1114
1257
|
showMobileBottom ? /* @__PURE__ */ jsx(
|
|
@@ -1125,7 +1268,7 @@ function AppShell({
|
|
|
1125
1268
|
href: item.to,
|
|
1126
1269
|
title: item.label,
|
|
1127
1270
|
"aria-label": item.label,
|
|
1128
|
-
"aria-current": active ? "page" : void 0,
|
|
1271
|
+
"aria-current": active && !bottomNotCurrent ? "page" : void 0,
|
|
1129
1272
|
className: cn(
|
|
1130
1273
|
"flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors",
|
|
1131
1274
|
active ? "text-primary" : "text-muted-foreground"
|
|
@@ -1149,5 +1292,5 @@ function AppShell({
|
|
|
1149
1292
|
}
|
|
1150
1293
|
|
|
1151
1294
|
export { AppShell, PageHeader };
|
|
1152
|
-
//# sourceMappingURL=chunk-
|
|
1153
|
-
//# sourceMappingURL=chunk-
|
|
1295
|
+
//# sourceMappingURL=chunk-UYI3GLFJ.js.map
|
|
1296
|
+
//# sourceMappingURL=chunk-UYI3GLFJ.js.map
|