@fayz-ai/ui 0.6.6 → 0.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.
Files changed (72) hide show
  1. package/README.md +2 -0
  2. package/dist/chunk-75EFDLST.js +274 -0
  3. package/dist/chunk-75EFDLST.js.map +1 -0
  4. package/dist/{chunk-YB5ZJKNG.cjs → chunk-7PTRAWHO.cjs} +263 -225
  5. package/dist/chunk-7PTRAWHO.cjs.map +1 -0
  6. package/dist/{chunk-LO5QZO7F.js → chunk-COC5DDFG.js} +125 -20
  7. package/dist/chunk-COC5DDFG.js.map +1 -0
  8. package/dist/{chunk-MYZT2OG7.js → chunk-FY7OZNKU.js} +24 -68
  9. package/dist/chunk-FY7OZNKU.js.map +1 -0
  10. package/dist/chunk-H6OULCHF.cjs +81 -0
  11. package/dist/chunk-H6OULCHF.cjs.map +1 -0
  12. package/dist/{chunk-DJHS2XRW.cjs → chunk-HG5RH3B3.cjs} +161 -54
  13. package/dist/chunk-HG5RH3B3.cjs.map +1 -0
  14. package/dist/{chunk-5QFB6AIQ.cjs → chunk-NWPMNPGR.cjs} +40 -40
  15. package/dist/{chunk-5QFB6AIQ.cjs.map → chunk-NWPMNPGR.cjs.map} +1 -1
  16. package/dist/{chunk-D6SXPF4F.cjs → chunk-ODY3GZ3Y.cjs} +41 -87
  17. package/dist/chunk-ODY3GZ3Y.cjs.map +1 -0
  18. package/dist/{chunk-22J2RTMN.js → chunk-OTUH4K4B.js} +196 -150
  19. package/dist/chunk-OTUH4K4B.js.map +1 -0
  20. package/dist/chunk-OW73RZOU.cjs +313 -0
  21. package/dist/chunk-OW73RZOU.cjs.map +1 -0
  22. package/dist/{chunk-XLBGB7IW.js → chunk-W5O2X277.js} +4 -4
  23. package/dist/{chunk-XLBGB7IW.js.map → chunk-W5O2X277.js.map} +1 -1
  24. package/dist/chunk-YRVE4FOD.js +57 -0
  25. package/dist/chunk-YRVE4FOD.js.map +1 -0
  26. package/dist/dashboard/index.cjs +27 -27
  27. package/dist/dashboard/index.js +3 -3
  28. package/dist/index.cjs +195 -175
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +7 -7
  32. package/dist/layout/ContentSplit.d.ts +32 -0
  33. package/dist/layout/ContentSplit.d.ts.map +1 -0
  34. package/dist/layout/Sidebar.d.ts.map +1 -1
  35. package/dist/layout/Topbar.d.ts.map +1 -1
  36. package/dist/layout/index.cjs +31 -23
  37. package/dist/layout/index.d.ts +1 -0
  38. package/dist/layout/index.d.ts.map +1 -1
  39. package/dist/layout/index.js +3 -3
  40. package/dist/primitives/index.cjs +126 -114
  41. package/dist/primitives/index.d.ts +2 -0
  42. package/dist/primitives/index.d.ts.map +1 -1
  43. package/dist/primitives/index.js +4 -4
  44. package/dist/primitives/markdown-editor.d.ts +23 -0
  45. package/dist/primitives/markdown-editor.d.ts.map +1 -0
  46. package/dist/primitives/markdown.d.ts +9 -0
  47. package/dist/primitives/markdown.d.ts.map +1 -0
  48. package/package.json +9 -3
  49. package/src/index.ts +9 -0
  50. package/src/layout/ContentSplit.tsx +153 -0
  51. package/src/layout/Sidebar.tsx +39 -14
  52. package/src/layout/Topbar.tsx +2 -1
  53. package/src/layout/index.ts +6 -0
  54. package/src/primitives/dropdown.tsx +4 -4
  55. package/src/primitives/index.ts +2 -0
  56. package/src/primitives/markdown-editor.tsx +75 -0
  57. package/src/primitives/markdown.tsx +169 -0
  58. package/src/primitives/select.tsx +1 -1
  59. package/dist/chunk-22J2RTMN.js.map +0 -1
  60. package/dist/chunk-D6SXPF4F.cjs.map +0 -1
  61. package/dist/chunk-DJHS2XRW.cjs.map +0 -1
  62. package/dist/chunk-DNYVEOAX.js +0 -11
  63. package/dist/chunk-DNYVEOAX.js.map +0 -1
  64. package/dist/chunk-E526FHZ4.js +0 -169
  65. package/dist/chunk-E526FHZ4.js.map +0 -1
  66. package/dist/chunk-LO5QZO7F.js.map +0 -1
  67. package/dist/chunk-MYZT2OG7.js.map +0 -1
  68. package/dist/chunk-VVVZO2LE.cjs +0 -13
  69. package/dist/chunk-VVVZO2LE.cjs.map +0 -1
  70. package/dist/chunk-W4XXXJRW.cjs +0 -196
  71. package/dist/chunk-W4XXXJRW.cjs.map +0 -1
  72. package/dist/chunk-YB5ZJKNG.cjs.map +0 -1
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![npm](https://img.shields.io/npm/v/@fayz-ai/ui.svg)](https://www.npmjs.com/package/@fayz-ai/ui)
6
6
  [![license](https://img.shields.io/npm/l/@fayz-ai/ui.svg)](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
7
7
 
8
+ **Status:** beta — published to npm and used across Fayz dogfood apps. Pre-1.0: minor APIs may change before 1.0.
9
+
8
10
  A composed Fayz app needs to look like one product, not a pile of plugins. `@fayz-ai/ui` is the design layer that makes that true: accessible Radix-backed primitives, a data table, layout shells (app shell, sidebar, module pages), a themeable token system, and a dashboard widget kit that plugins contribute KPIs, charts, and tables into. Salon, restaurant, clinic — same shell, same widgets, different brand.
9
11
 
10
12
  The philosophy: plugins describe behavior, the UI renders it consistently. Define a KPI widget once and it lands in the dashboard grid with layout, ranges, and customization already wired. Drop in `AppShell` and `Sidebar` and the navigation a plugin declared just appears.
@@ -0,0 +1,274 @@
1
+ import { cn } from './chunk-YRVE4FOD.js';
2
+ import * as React2 from 'react';
3
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
4
+ import { X, Loader2 } from 'lucide-react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { useTranslation, useNavReferrer, routeModule, navHistoryBack } from '@fayz-ai/core';
7
+
8
+ var STYLE_ID = "saas-sheet-styles";
9
+ if (typeof document !== "undefined" && !document.getElementById(STYLE_ID)) {
10
+ const style = document.createElement("style");
11
+ style.id = STYLE_ID;
12
+ style.textContent = `
13
+ @keyframes saas-sheet-fade-in { from { opacity: 0 } to { opacity: 1 } }
14
+ @keyframes saas-sheet-fade-out { from { opacity: 1 } to { opacity: 0 } }
15
+ @keyframes saas-sheet-slide-in { from { transform: translateX(100%) } to { transform: translateX(0) } }
16
+ @keyframes saas-sheet-slide-out { from { transform: translateX(0) } to { transform: translateX(100%) } }
17
+ .saas-sheet-overlay[data-state="open"] { animation: saas-sheet-fade-in 200ms ease-out }
18
+ .saas-sheet-overlay[data-state="closed"] { animation: saas-sheet-fade-out 150ms ease-in }
19
+ .saas-sheet-content[data-state="open"] { animation: saas-sheet-slide-in 280ms cubic-bezier(0.16, 1, 0.3, 1) }
20
+ .saas-sheet-content[data-state="closed"] { animation: saas-sheet-slide-out 200ms ease-in }
21
+ `;
22
+ document.head.appendChild(style);
23
+ }
24
+ var Sheet = DialogPrimitive.Root;
25
+ var SheetTrigger = DialogPrimitive.Trigger;
26
+ var SheetClose = DialogPrimitive.Close;
27
+ var SheetPortal = DialogPrimitive.Portal;
28
+ var SheetOverlay = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
29
+ DialogPrimitive.Overlay,
30
+ {
31
+ ref,
32
+ className: cn(
33
+ "saas-sheet-overlay fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]",
34
+ className
35
+ ),
36
+ ...props
37
+ }
38
+ ));
39
+ SheetOverlay.displayName = "SheetOverlay";
40
+ var SheetContent = React2.forwardRef(({ className, children, width, hideClose, overlay = "blur", ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [
41
+ /* @__PURE__ */ jsx(SheetOverlay, { className: overlay === "none" ? "bg-transparent backdrop-blur-0" : overlay === "dim" ? "backdrop-blur-0" : void 0 }),
42
+ /* @__PURE__ */ jsxs(
43
+ DialogPrimitive.Content,
44
+ {
45
+ ref,
46
+ className: cn(
47
+ "fayz-glass-surface saas-sheet-content fixed inset-y-0 right-0 z-50 flex flex-col border-l bg-card shadow-2xl outline-none",
48
+ "w-full",
49
+ width ?? "max-w-md",
50
+ className
51
+ ),
52
+ ...props,
53
+ children: [
54
+ children,
55
+ !hideClose && /* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring", children: [
56
+ /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
57
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
58
+ ] })
59
+ ]
60
+ }
61
+ )
62
+ ] }));
63
+ SheetContent.displayName = "SheetContent";
64
+ function SheetHeader({ className, ...props }) {
65
+ return /* @__PURE__ */ jsx(
66
+ "div",
67
+ {
68
+ className: cn("flex flex-col gap-1 px-5 py-4 border-b shrink-0", className),
69
+ ...props
70
+ }
71
+ );
72
+ }
73
+ SheetHeader.displayName = "SheetHeader";
74
+ function SheetFooter({ className, ...props }) {
75
+ return /* @__PURE__ */ jsx(
76
+ "div",
77
+ {
78
+ className: cn("flex items-center gap-2 px-5 py-3 border-t shrink-0", className),
79
+ ...props
80
+ }
81
+ );
82
+ }
83
+ SheetFooter.displayName = "SheetFooter";
84
+ var SheetTitle = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
85
+ DialogPrimitive.Title,
86
+ {
87
+ ref,
88
+ className: cn("text-sm font-semibold leading-none tracking-tight", className),
89
+ ...props
90
+ }
91
+ ));
92
+ SheetTitle.displayName = "SheetTitle";
93
+ var SheetDescription = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
94
+ DialogPrimitive.Description,
95
+ {
96
+ ref,
97
+ className: cn("text-xs text-muted-foreground", className),
98
+ ...props
99
+ }
100
+ ));
101
+ SheetDescription.displayName = "SheetDescription";
102
+ function SheetBody({ className, ...props }) {
103
+ return /* @__PURE__ */ jsx(
104
+ "div",
105
+ {
106
+ className: cn("flex-1 overflow-y-auto px-5 py-5", className),
107
+ ...props
108
+ }
109
+ );
110
+ }
111
+ SheetBody.displayName = "SheetBody";
112
+ var SaveBarApiContext = React2.createContext(null);
113
+ var SaveBarStateContext = React2.createContext({ visible: false, saving: false });
114
+ var _saveBarVisible = false;
115
+ var _saveBarSubs = /* @__PURE__ */ new Set();
116
+ function setSaveBarVisibleSignal(v) {
117
+ if (v === _saveBarVisible) return;
118
+ _saveBarVisible = v;
119
+ _saveBarSubs.forEach((fn) => fn());
120
+ }
121
+ function useSaveBarVisible() {
122
+ return React2.useSyncExternalStore(
123
+ (cb) => {
124
+ _saveBarSubs.add(cb);
125
+ return () => {
126
+ _saveBarSubs.delete(cb);
127
+ };
128
+ },
129
+ () => _saveBarVisible,
130
+ () => false
131
+ );
132
+ }
133
+ function SaveBarProvider({ children }) {
134
+ const [state, setState] = React2.useState({ visible: false, saving: false });
135
+ const handlers = React2.useRef({});
136
+ const api = React2.useMemo(() => ({ setState, handlers }), []);
137
+ const backStack = React2.useRef([]);
138
+ const backApi = React2.useMemo(() => ({ stack: backStack }), []);
139
+ React2.useEffect(() => {
140
+ setSaveBarVisibleSignal(state.visible);
141
+ }, [state.visible]);
142
+ React2.useEffect(() => {
143
+ function onKey(e) {
144
+ if (state.visible && !state.saving) {
145
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
146
+ e.preventDefault();
147
+ handlers.current.onSave?.();
148
+ } else if (e.key === "Escape") {
149
+ e.preventDefault();
150
+ handlers.current.onDiscard?.();
151
+ }
152
+ return;
153
+ }
154
+ if (e.key === "Escape" && !e.defaultPrevented && !isEditableTarget(e.target)) {
155
+ for (let i = backStack.current.length - 1; i >= 0; i--) {
156
+ const back = backStack.current[i].current;
157
+ if (back) {
158
+ e.preventDefault();
159
+ back();
160
+ return;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ window.addEventListener("keydown", onKey);
166
+ return () => window.removeEventListener("keydown", onKey);
167
+ }, [state.visible, state.saving]);
168
+ return /* @__PURE__ */ jsx(SaveBarApiContext.Provider, { value: api, children: /* @__PURE__ */ jsx(BackNavApiContext.Provider, { value: backApi, children: /* @__PURE__ */ jsx(SaveBarStateContext.Provider, { value: state, children }) }) });
169
+ }
170
+ var BackNavApiContext = React2.createContext(null);
171
+ function isEditableTarget(target) {
172
+ const el = target;
173
+ if (!el || !el.tagName) return false;
174
+ const tag = el.tagName.toLowerCase();
175
+ return tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable;
176
+ }
177
+ function useBackHandler(onBack) {
178
+ const api = React2.useContext(BackNavApiContext);
179
+ const ref = React2.useRef(onBack);
180
+ React2.useEffect(() => {
181
+ ref.current = onBack;
182
+ });
183
+ React2.useEffect(() => {
184
+ if (!api) return;
185
+ const stack = api.stack.current;
186
+ stack.push(ref);
187
+ return () => {
188
+ const i = stack.indexOf(ref);
189
+ if (i !== -1) stack.splice(i, 1);
190
+ };
191
+ }, [api]);
192
+ }
193
+ function useSaveBar({ dirty, saving = false, onSave, onDiscard, message, saveLabel, discardLabel }) {
194
+ const api = React2.useContext(SaveBarApiContext);
195
+ React2.useEffect(() => {
196
+ if (api) api.handlers.current = { onSave, onDiscard };
197
+ });
198
+ React2.useEffect(() => {
199
+ if (!api) return;
200
+ api.setState({ visible: dirty, saving, message, saveLabel, discardLabel });
201
+ return () => api.setState((s) => ({ ...s, visible: false }));
202
+ }, [api, dirty, saving, message, saveLabel, discardLabel]);
203
+ }
204
+ function SaveBar({ className }) {
205
+ const state = React2.useContext(SaveBarStateContext);
206
+ const api = React2.useContext(SaveBarApiContext);
207
+ const t = useTranslation();
208
+ const [rendered, setRendered] = React2.useState(state.visible);
209
+ const [closing, setClosing] = React2.useState(false);
210
+ React2.useEffect(() => {
211
+ if (state.visible) {
212
+ setRendered(true);
213
+ setClosing(false);
214
+ return;
215
+ }
216
+ if (!rendered) return;
217
+ setClosing(true);
218
+ const id = setTimeout(() => {
219
+ setRendered(false);
220
+ setClosing(false);
221
+ }, 200);
222
+ return () => clearTimeout(id);
223
+ }, [state.visible, rendered]);
224
+ if (!api || !rendered) return null;
225
+ const { saving, message, saveLabel, discardLabel } = state;
226
+ return /* @__PURE__ */ jsx("div", { className: cn("pointer-events-none fixed inset-x-0 bottom-5 z-40 flex justify-center px-4", className), children: /* @__PURE__ */ jsxs("div", { className: cn(
227
+ "pointer-events-auto flex items-center gap-5 rounded-full bg-foreground py-2 pl-5 pr-2 text-background shadow-xl ring-1 ring-black/5 duration-200",
228
+ closing ? "animate-out fade-out slide-out-to-bottom-2" : "animate-in fade-in slide-in-from-bottom-2"
229
+ ), children: [
230
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm font-medium", children: [
231
+ /* @__PURE__ */ jsx("span", { className: "h-2 w-2 shrink-0 rounded-full bg-amber-400" }),
232
+ message ?? t("saveBar.unsaved")
233
+ ] }),
234
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
235
+ /* @__PURE__ */ jsx(
236
+ "button",
237
+ {
238
+ type: "button",
239
+ disabled: saving,
240
+ onClick: () => api.handlers.current.onDiscard?.(),
241
+ className: "rounded-full px-3 py-1.5 text-sm font-medium text-background/70 transition-colors hover:bg-background/10 hover:text-background disabled:opacity-50",
242
+ children: discardLabel ?? t("common.discard")
243
+ }
244
+ ),
245
+ /* @__PURE__ */ jsxs(
246
+ "button",
247
+ {
248
+ type: "button",
249
+ disabled: saving,
250
+ onClick: () => api.handlers.current.onSave?.(),
251
+ className: "inline-flex items-center rounded-full bg-background px-4 py-1.5 text-sm font-semibold text-foreground transition-colors hover:bg-background/90 disabled:opacity-60",
252
+ children: [
253
+ saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }),
254
+ saveLabel ?? t("common.save")
255
+ ]
256
+ }
257
+ )
258
+ ] })
259
+ ] }) });
260
+ }
261
+ function useCrossModuleBack(onBack, parentLabel) {
262
+ const currentPath = typeof window !== "undefined" ? (window.location.hash ? window.location.hash.slice(1) : window.location.pathname) || "/" : "/";
263
+ const referrer = useNavReferrer(currentPath);
264
+ const cross = referrer && referrer.label && routeModule(referrer.path) !== routeModule(currentPath) ? referrer : null;
265
+ return {
266
+ onBack: cross ? () => navHistoryBack(onBack) : onBack,
267
+ parentLabel: cross ? cross.label : parentLabel,
268
+ isCrossModule: !!cross
269
+ };
270
+ }
271
+
272
+ export { SaveBar, SaveBarProvider, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, useBackHandler, useCrossModuleBack, useSaveBar, useSaveBarVisible };
273
+ //# sourceMappingURL=chunk-75EFDLST.js.map
274
+ //# sourceMappingURL=chunk-75EFDLST.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/primitives/sheet.tsx","../src/layout/SaveBar.tsx","../src/layout/useCrossModuleBack.ts"],"names":["React","jsx","jsxs"],"mappings":"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","file":"chunk-75EFDLST.js","sourcesContent":["import * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { X } from 'lucide-react'\n\nimport { cn } from '../utils/cn'\n\n// ---------------------------------------------------------------------------\n// Inject sheet animations once (module-level side effect)\n// ---------------------------------------------------------------------------\n\nconst STYLE_ID = 'saas-sheet-styles'\nif (typeof document !== 'undefined' && !document.getElementById(STYLE_ID)) {\n const style = document.createElement('style')\n style.id = STYLE_ID\n style.textContent = `\n @keyframes saas-sheet-fade-in { from { opacity: 0 } to { opacity: 1 } }\n @keyframes saas-sheet-fade-out { from { opacity: 1 } to { opacity: 0 } }\n @keyframes saas-sheet-slide-in { from { transform: translateX(100%) } to { transform: translateX(0) } }\n @keyframes saas-sheet-slide-out { from { transform: translateX(0) } to { transform: translateX(100%) } }\n .saas-sheet-overlay[data-state=\"open\"] { animation: saas-sheet-fade-in 200ms ease-out }\n .saas-sheet-overlay[data-state=\"closed\"] { animation: saas-sheet-fade-out 150ms ease-in }\n .saas-sheet-content[data-state=\"open\"] { animation: saas-sheet-slide-in 280ms cubic-bezier(0.16, 1, 0.3, 1) }\n .saas-sheet-content[data-state=\"closed\"] { animation: saas-sheet-slide-out 200ms ease-in }\n `\n document.head.appendChild(style)\n}\n\n// ---------------------------------------------------------------------------\n// Root, Trigger, Close — re-exported from Radix\n// ---------------------------------------------------------------------------\n\nconst Sheet = DialogPrimitive.Root\nconst SheetTrigger = DialogPrimitive.Trigger\nconst SheetClose = DialogPrimitive.Close\nconst SheetPortal = DialogPrimitive.Portal\n\n// ---------------------------------------------------------------------------\n// Overlay\n// ---------------------------------------------------------------------------\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n 'saas-sheet-overlay fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]',\n className\n )}\n {...props}\n />\n))\nSheetOverlay.displayName = 'SheetOverlay'\n\n// ---------------------------------------------------------------------------\n// Content — slides in from the right\n// ---------------------------------------------------------------------------\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {\n /** Width class override — defaults to max-w-md */\n width?: string\n /** Hide the built-in close button */\n hideClose?: boolean\n /** Overlay style: \"blur\" (default), \"dim\" (no blur), \"none\" (transparent) */\n overlay?: 'blur' | 'dim' | 'none'\n }\n>(({ className, children, width, hideClose, overlay = 'blur', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay className={overlay === 'none' ? 'bg-transparent backdrop-blur-0' : overlay === 'dim' ? 'backdrop-blur-0' : undefined} />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n 'fayz-glass-surface saas-sheet-content fixed inset-y-0 right-0 z-50 flex flex-col border-l bg-card shadow-2xl outline-none',\n 'w-full',\n width ?? 'max-w-md',\n className\n )}\n {...props}\n >\n {children}\n {!hideClose && (\n <DialogPrimitive.Close className=\"absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring\">\n <X className=\"h-4 w-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = 'SheetContent'\n\n// ---------------------------------------------------------------------------\n// Header — top section with title + description\n// ---------------------------------------------------------------------------\n\nfunction SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn('flex flex-col gap-1 px-5 py-4 border-b shrink-0', className)}\n {...props}\n />\n )\n}\nSheetHeader.displayName = 'SheetHeader'\n\n// ---------------------------------------------------------------------------\n// Footer — bottom section for actions\n// ---------------------------------------------------------------------------\n\nfunction SheetFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn('flex items-center gap-2 px-5 py-3 border-t shrink-0', className)}\n {...props}\n />\n )\n}\nSheetFooter.displayName = 'SheetFooter'\n\n// ---------------------------------------------------------------------------\n// Title\n// ---------------------------------------------------------------------------\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-sm font-semibold leading-none tracking-tight', className)}\n {...props}\n />\n))\nSheetTitle.displayName = 'SheetTitle'\n\n// ---------------------------------------------------------------------------\n// Description\n// ---------------------------------------------------------------------------\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-xs text-muted-foreground', className)}\n {...props}\n />\n))\nSheetDescription.displayName = 'SheetDescription'\n\n// ---------------------------------------------------------------------------\n// Body — scrollable content area\n// ---------------------------------------------------------------------------\n\nfunction SheetBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn('flex-1 overflow-y-auto px-5 py-5', className)}\n {...props}\n />\n )\n}\nSheetBody.displayName = 'SheetBody'\n\nexport {\n Sheet,\n SheetPortal,\n SheetOverlay,\n SheetTrigger,\n SheetClose,\n SheetContent,\n SheetHeader,\n SheetBody,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n}\n","import * as React from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { useTranslation } from '@fayz-ai/core'\nimport { cn } from '../utils/cn'\n\n// ---------------------------------------------------------------------------\n// SaveBar — the single, app-wide \"Unsaved changes\" floating island. A form\n// declares its state via useSaveBar({ dirty, saving, onSave, onDiscard }); the\n// shell renders ONE <SaveBar/> (centered, floating above the content area) that\n// appears whenever the active form is dirty. Mirrors the BackStyleProvider /\n// PageHeaderActions idiom: decide-once at the shell, hook-in at the page.\n//\n// Handlers live in a ref (not state) so per-render closure changes never cause\n// re-render loops; only the primitive `dirty`/`saving` flags drive visibility.\n// ---------------------------------------------------------------------------\n\nexport interface SaveBarRegistration {\n /** Show the bar when true (form has unsaved changes). */\n dirty: boolean\n /** Save in progress — disables buttons and shows a spinner. */\n saving?: boolean\n onSave: () => void\n onDiscard?: () => void\n /** Override the \"Unsaved changes\" message. */\n message?: string\n /** Override the Save / Discard button labels. */\n saveLabel?: string\n discardLabel?: string\n}\n\ninterface SaveBarState {\n visible: boolean\n saving: boolean\n message?: string\n saveLabel?: string\n discardLabel?: string\n}\n\ninterface SaveBarApi {\n setState: React.Dispatch<React.SetStateAction<SaveBarState>>\n handlers: React.MutableRefObject<{ onSave?: () => void; onDiscard?: () => void }>\n}\n\n// Two contexts on purpose: the API context is STABLE (so useSaveBar's effect\n// deps don't change every render → no loop), the state context changes to\n// re-render only <SaveBar/>.\nconst SaveBarApiContext = React.createContext<SaveBarApi | null>(null)\nconst SaveBarStateContext = React.createContext<SaveBarState>({ visible: false, saving: false })\n\n// Module-level \"is the SaveBar showing\" signal so sibling UI that isn't a\n// descendant of SaveBarProvider (e.g. the ToastProvider) can react to it —\n// used to lift toasts above the bar so they stack instead of overlapping.\nlet _saveBarVisible = false\nconst _saveBarSubs = new Set<() => void>()\nfunction setSaveBarVisibleSignal(v: boolean) {\n if (v === _saveBarVisible) return\n _saveBarVisible = v\n _saveBarSubs.forEach((fn) => fn())\n}\n\n/** Subscribe to whether the floating SaveBar is currently on screen. */\nexport function useSaveBarVisible(): boolean {\n return React.useSyncExternalStore(\n (cb) => { _saveBarSubs.add(cb); return () => { _saveBarSubs.delete(cb) } },\n () => _saveBarVisible,\n () => false,\n )\n}\n\nexport function SaveBarProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = React.useState<SaveBarState>({ visible: false, saving: false })\n const handlers = React.useRef<{ onSave?: () => void; onDiscard?: () => void }>({})\n const api = React.useMemo<SaveBarApi>(() => ({ setState, handlers }), [])\n\n // Stack of registered back-navigation handlers (latest-mounted wins) so the\n // app-wide Escape key can navigate \"up\" to the parent route. Lives here, next\n // to the SaveBar handlers, so ONE keydown listener arbitrates both — Escape\n // discards a dirty form first, and only navigates back when none is open.\n const backStack = React.useRef<React.MutableRefObject<(() => void) | undefined>[]>([])\n const backApi = React.useMemo<BackNavApi>(() => ({ stack: backStack }), [])\n\n // Broadcast visibility to the module-level signal (for the ToastProvider).\n React.useEffect(() => { setSaveBarVisibleSignal(state.visible) }, [state.visible])\n\n // App-wide keyboard shortcuts, arbitrated in one place:\n // • Cmd/Ctrl+Enter → save the active (dirty) form\n // • Escape (form dirty) → discard the form\n // • Escape (no dirty form) → navigate back to the parent route\n React.useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (state.visible && !state.saving) {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n handlers.current.onSave?.()\n } else if (e.key === 'Escape') {\n e.preventDefault()\n handlers.current.onDiscard?.()\n }\n return\n }\n // No open/dirty form — Escape walks \"up\" via the topmost back handler.\n if (e.key === 'Escape' && !e.defaultPrevented && !isEditableTarget(e.target)) {\n for (let i = backStack.current.length - 1; i >= 0; i--) {\n const back = backStack.current[i].current\n if (back) { e.preventDefault(); back(); return }\n }\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [state.visible, state.saving])\n\n return (\n <SaveBarApiContext.Provider value={api}>\n <BackNavApiContext.Provider value={backApi}>\n <SaveBarStateContext.Provider value={state}>\n {children}\n </SaveBarStateContext.Provider>\n </BackNavApiContext.Provider>\n </SaveBarApiContext.Provider>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Back-navigation registry — lets any subpage register its \"go to parent\" action\n// so the shell's single Escape handler can drive it (see SaveBarProvider above).\n// SubpageHeader already receives onBack, so wiring it there gives every plugin\n// detail page Escape-to-parent for free, with zero per-plugin code.\n// ---------------------------------------------------------------------------\n\ninterface BackNavApi {\n stack: React.MutableRefObject<React.MutableRefObject<(() => void) | undefined>[]>\n}\nconst BackNavApiContext = React.createContext<BackNavApi | null>(null)\n\n/** True when the keyboard event originates from a field where Escape has its\n * own meaning (clear/close), so we shouldn't hijack it for back-navigation. */\nfunction isEditableTarget(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el || !el.tagName) return false\n const tag = el.tagName.toLowerCase()\n return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable\n}\n\n/**\n * Register a back-navigation handler with the app-wide Escape key. Call it in a\n * subpage (or pass through SubpageHeader's onBack); while mounted, pressing\n * Escape with no dirty form open invokes the most-recently-registered handler.\n * Pass `undefined` to opt out (e.g. a top-level page with nothing to go back to).\n */\nexport function useBackHandler(onBack?: () => void): void {\n const api = React.useContext(BackNavApiContext)\n // Hold the latest closure in a ref so re-renders don't churn the stack.\n const ref = React.useRef(onBack)\n React.useEffect(() => { ref.current = onBack })\n React.useEffect(() => {\n if (!api) return\n const stack = api.stack.current\n stack.push(ref)\n return () => {\n const i = stack.indexOf(ref)\n if (i !== -1) stack.splice(i, 1)\n }\n }, [api])\n}\n\n/**\n * Register the current form with the app-wide SaveBar. Call it unconditionally\n * in a form view; it shows the island while `dirty`, keeps handlers current, and\n * hides the island automatically when the form unmounts (e.g. on navigation).\n */\nexport function useSaveBar({ dirty, saving = false, onSave, onDiscard, message, saveLabel, discardLabel }: SaveBarRegistration): void {\n const api = React.useContext(SaveBarApiContext)\n\n // Keep latest handlers in the shared ref (no state churn from new closures).\n React.useEffect(() => {\n if (api) api.handlers.current = { onSave, onDiscard }\n })\n\n // Drive visibility from primitives only; `api` is stable → no render loops.\n React.useEffect(() => {\n if (!api) return\n api.setState({ visible: dirty, saving, message, saveLabel, discardLabel })\n return () => api.setState((s) => ({ ...s, visible: false }))\n }, [api, dirty, saving, message, saveLabel, discardLabel])\n}\n\n/** The floating island. Rendered once by the shell inside the content column. */\nexport function SaveBar({ className }: { className?: string }) {\n const state = React.useContext(SaveBarStateContext)\n const api = React.useContext(SaveBarApiContext)\n const t = useTranslation()\n\n // Keep the island mounted through its exit animation: `rendered` lags behind\n // `visible` so we can play fade-out before unmounting.\n const [rendered, setRendered] = React.useState(state.visible)\n const [closing, setClosing] = React.useState(false)\n\n React.useEffect(() => {\n if (state.visible) {\n setRendered(true)\n setClosing(false)\n return\n }\n if (!rendered) return\n setClosing(true)\n const id = setTimeout(() => { setRendered(false); setClosing(false) }, 200)\n return () => clearTimeout(id)\n }, [state.visible, rendered])\n\n if (!api || !rendered) return null\n const { saving, message, saveLabel, discardLabel } = state\n\n return (\n <div className={cn('pointer-events-none fixed inset-x-0 bottom-5 z-40 flex justify-center px-4', className)}>\n {/* Dark island for strong contrast against the light page (auto-inverts in dark mode). */}\n <div className={cn(\n 'pointer-events-auto flex items-center gap-5 rounded-full bg-foreground py-2 pl-5 pr-2 text-background shadow-xl ring-1 ring-black/5 duration-200',\n closing ? 'animate-out fade-out slide-out-to-bottom-2' : 'animate-in fade-in slide-in-from-bottom-2',\n )}>\n <span className=\"flex items-center gap-2 text-sm font-medium\">\n <span className=\"h-2 w-2 shrink-0 rounded-full bg-amber-400\" />\n {message ?? t('saveBar.unsaved')}\n </span>\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n disabled={saving}\n onClick={() => api.handlers.current.onDiscard?.()}\n className=\"rounded-full px-3 py-1.5 text-sm font-medium text-background/70 transition-colors hover:bg-background/10 hover:text-background disabled:opacity-50\"\n >\n {discardLabel ?? t('common.discard')}\n </button>\n <button\n type=\"button\"\n disabled={saving}\n onClick={() => api.handlers.current.onSave?.()}\n className=\"inline-flex items-center rounded-full bg-background px-4 py-1.5 text-sm font-semibold text-foreground transition-colors hover:bg-background/90 disabled:opacity-60\"\n >\n {saving && <Loader2 className=\"mr-1.5 h-3.5 w-3.5 animate-spin\" />}\n {saveLabel ?? t('common.save')}\n </button>\n </div>\n </div>\n </div>\n )\n}\n","import { useNavReferrer, navHistoryBack, routeModule } from '@fayz-ai/core'\n\n/**\n * Context-aware back target shared by all header/breadcrumb components.\n *\n * When the user arrived from a *different* module (tracked in the global\n * navigation history), a page's own onBack/parentLabel point at its local\n * parent — wrong for the referrer. In that case we return the referrer's label\n * and a back action that pops the browser history to the actual previous page.\n * Within the same module the page's own (more specific) values pass through\n * unchanged, so existing behavior is preserved.\n */\nexport function useCrossModuleBack(onBack?: () => void, parentLabel?: string): {\n onBack?: () => void\n parentLabel?: string\n isCrossModule: boolean\n} {\n const currentPath = typeof window !== 'undefined'\n ? ((window.location.hash ? window.location.hash.slice(1) : window.location.pathname) || '/')\n : '/'\n const referrer = useNavReferrer(currentPath)\n const cross = referrer && referrer.label && routeModule(referrer.path) !== routeModule(currentPath)\n ? referrer\n : null\n return {\n onBack: cross ? () => navHistoryBack(onBack) : onBack,\n parentLabel: cross ? cross.label : parentLabel,\n isCrossModule: !!cross,\n }\n}\n"]}