gridsmith-ui 0.31.1 → 0.33.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/CHANGELOG.md +78 -1
- package/README.md +4 -2
- package/dist/charts.d.ts +111 -31
- package/dist/charts.js +288 -148
- package/dist/charts.js.map +1 -1
- package/dist/chat.js +6 -6
- package/dist/{chunk-EZ3ZGFGA.js → chunk-2724SJO2.js} +12 -4
- package/dist/chunk-2724SJO2.js.map +1 -0
- package/dist/{chunk-I6JJ7KKP.js → chunk-6NRLTG5W.js} +16 -11
- package/dist/chunk-6NRLTG5W.js.map +1 -0
- package/dist/{chunk-BHTAVJIG.js → chunk-6XCWKLJZ.js} +41 -6
- package/dist/chunk-6XCWKLJZ.js.map +1 -0
- package/dist/{chunk-TH272DIC.js → chunk-AB4UQ4G5.js} +125 -235
- package/dist/chunk-AB4UQ4G5.js.map +1 -0
- package/dist/{chunk-IOHGE4EW.js → chunk-AVSP6NDX.js} +8 -8
- package/dist/chunk-AVSP6NDX.js.map +1 -0
- package/dist/chunk-F2XMZOUP.js +81 -0
- package/dist/chunk-F2XMZOUP.js.map +1 -0
- package/dist/{chunk-HQFTP7Y5.js → chunk-FF57N3P7.js} +10 -8
- package/dist/chunk-FF57N3P7.js.map +1 -0
- package/dist/chunk-GIA4BMGI.js +18 -0
- package/dist/chunk-GIA4BMGI.js.map +1 -0
- package/dist/{chunk-DR6WEI3A.js → chunk-HCEHTJ5E.js} +3 -3
- package/dist/{chunk-DR6WEI3A.js.map → chunk-HCEHTJ5E.js.map} +1 -1
- package/dist/{chunk-IZW5XHKA.js → chunk-OWGF362U.js} +10 -8
- package/dist/chunk-OWGF362U.js.map +1 -0
- package/dist/chunk-P3NUUIL6.js +9 -0
- package/dist/chunk-P3NUUIL6.js.map +1 -0
- package/dist/{chunk-W3GY5ROH.js → chunk-R2IAK3J3.js} +3 -3
- package/dist/{chunk-W3GY5ROH.js.map → chunk-R2IAK3J3.js.map} +1 -1
- package/dist/chunk-R7FG4ZQ5.js +16 -0
- package/dist/chunk-R7FG4ZQ5.js.map +1 -0
- package/dist/{chunk-V73LHNRL.js → chunk-RS624TD5.js} +29 -10
- package/dist/chunk-RS624TD5.js.map +1 -0
- package/dist/{chunk-DBHVJ7Z3.js → chunk-SS3C24IJ.js} +3 -2
- package/dist/chunk-SS3C24IJ.js.map +1 -0
- package/dist/{chunk-K3NAFQ4S.js → chunk-TMPPBXX3.js} +3 -3
- package/dist/{chunk-K3NAFQ4S.js.map → chunk-TMPPBXX3.js.map} +1 -1
- package/dist/{chunk-NDL2JGQ7.js → chunk-UBLBVCQN.js} +5 -5
- package/dist/{chunk-NDL2JGQ7.js.map → chunk-UBLBVCQN.js.map} +1 -1
- package/dist/{chunk-2TCCSKL3.js → chunk-WD6T57ZJ.js} +5 -5
- package/dist/{chunk-2TCCSKL3.js.map → chunk-WD6T57ZJ.js.map} +1 -1
- package/dist/{chunk-SLGGZ3MU.js → chunk-WSKPESAX.js} +12 -10
- package/dist/chunk-WSKPESAX.js.map +1 -0
- package/dist/chunk-ZQV26NND.js +213 -0
- package/dist/chunk-ZQV26NND.js.map +1 -0
- package/dist/dashboard.d.ts +207 -0
- package/dist/dashboard.js +1210 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/data-grid.js +11 -10
- package/dist/data-grid.js.map +1 -1
- package/dist/editor-mentions.js +2 -2
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +94 -12
- package/dist/index.js +601 -316
- package/dist/index.js.map +1 -1
- package/dist/kanban.js +9 -9
- package/dist/resizable.d.ts +11 -2
- package/dist/resizable.js +97 -22
- package/dist/resizable.js.map +1 -1
- package/dist/roots.css +2 -2
- package/dist/tokens.css +6 -6
- package/dist/utils.d.ts +51 -1
- package/dist/utils.js +55 -4
- package/dist/utils.js.map +1 -1
- package/dist/virtualized.js +9 -8
- package/dist/virtualized.js.map +1 -1
- package/dist-static/tokens.css +6 -6
- package/package.json +6 -1
- package/dist/chunk-BHTAVJIG.js.map +0 -1
- package/dist/chunk-DBHVJ7Z3.js.map +0 -1
- package/dist/chunk-EZ3ZGFGA.js.map +0 -1
- package/dist/chunk-HQFTP7Y5.js.map +0 -1
- package/dist/chunk-I6JJ7KKP.js.map +0 -1
- package/dist/chunk-IOHGE4EW.js.map +0 -1
- package/dist/chunk-IZW5XHKA.js.map +0 -1
- package/dist/chunk-KSJTMSYA.js +0 -71
- package/dist/chunk-KSJTMSYA.js.map +0 -1
- package/dist/chunk-LJYSVZMI.js +0 -25
- package/dist/chunk-LJYSVZMI.js.map +0 -1
- package/dist/chunk-SLGGZ3MU.js.map +0 -1
- package/dist/chunk-TH272DIC.js.map +0 -1
- package/dist/chunk-V73LHNRL.js.map +0 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FormChangeCtx } from './chunk-72SDAX54.js';
|
|
3
|
+
import { Button } from './chunk-FF57N3P7.js';
|
|
4
|
+
import { readBreakpointPx } from './chunk-ZHUVLBCC.js';
|
|
5
|
+
import { createContext, useRef, useState, useEffect, useMemo, useCallback, useLayoutEffect } from 'react';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var OVERLAY_ROOT_ATTR = "data-overlay-root";
|
|
9
|
+
function controlsOf(root) {
|
|
10
|
+
if (!root) return "";
|
|
11
|
+
const controls = root.querySelectorAll("input, select, textarea, [role='combobox'], [role='radiogroup'], [role='checkbox'], [role='switch'], [role='slider'], [role='spinbutton'], [contenteditable='true']");
|
|
12
|
+
return Array.from(controls).map((el) => el.id || el.getAttribute("name") || el.getAttribute("role") || el.tagName).join("|");
|
|
13
|
+
}
|
|
14
|
+
var OverlayMarkCtx = createContext(null);
|
|
15
|
+
function OverlayMarkScope({ mark, children }) {
|
|
16
|
+
return /* @__PURE__ */ jsx(OverlayMarkCtx, { value: mark, children: /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children }) });
|
|
17
|
+
}
|
|
18
|
+
function useOverlayMark(open) {
|
|
19
|
+
const forms = useRef(/* @__PURE__ */ new Map());
|
|
20
|
+
const count = useRef(0);
|
|
21
|
+
const round = useRef(0);
|
|
22
|
+
const [changed, setChanged] = useState(false);
|
|
23
|
+
const [holds, setHolds] = useState(0);
|
|
24
|
+
const [pends, setPends] = useState(0);
|
|
25
|
+
const [answers, setAnswers] = useState([]);
|
|
26
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
27
|
+
if (open !== wasOpen) {
|
|
28
|
+
setWasOpen(open);
|
|
29
|
+
if (!open) {
|
|
30
|
+
setChanged(false);
|
|
31
|
+
setAnswers([]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (open) return;
|
|
36
|
+
round.current++;
|
|
37
|
+
for (const [id, f] of forms.current) {
|
|
38
|
+
if (f.mounted) f.stamp = 0;
|
|
39
|
+
else forms.current.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}, [open]);
|
|
42
|
+
const mark = useMemo(() => ({
|
|
43
|
+
report: (form) => {
|
|
44
|
+
const f = forms.current.get(form) ?? { stamp: 0, mounted: true };
|
|
45
|
+
f.stamp = ++count.current;
|
|
46
|
+
forms.current.set(form, f);
|
|
47
|
+
setChanged(true);
|
|
48
|
+
},
|
|
49
|
+
attach: (form) => {
|
|
50
|
+
const f = forms.current.get(form) ?? { stamp: 0, mounted: true };
|
|
51
|
+
f.mounted = true;
|
|
52
|
+
forms.current.set(form, f);
|
|
53
|
+
return () => {
|
|
54
|
+
const gone = forms.current.get(form);
|
|
55
|
+
if (!gone) return;
|
|
56
|
+
if (gone.stamp > 0) gone.mounted = false;
|
|
57
|
+
else forms.current.delete(form);
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
submit: (form, from) => {
|
|
61
|
+
const root = from.element()?.closest(`[${OVERLAY_ROOT_ATTR}]`) ?? null;
|
|
62
|
+
const answer = { form, since: count.current, root, controls: controlsOf(root), ...from };
|
|
63
|
+
const at = round.current;
|
|
64
|
+
let settled = false;
|
|
65
|
+
return {
|
|
66
|
+
settle: (how) => {
|
|
67
|
+
if (settled || at !== round.current) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
setAnswers((list) => [...list, { ...answer, how }]);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
hold: () => {
|
|
74
|
+
setHolds((n) => n + 1);
|
|
75
|
+
let released = false;
|
|
76
|
+
return () => {
|
|
77
|
+
if (released) return;
|
|
78
|
+
released = true;
|
|
79
|
+
setHolds((n) => n - 1);
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
pend: () => {
|
|
83
|
+
setPends((n) => n + 1);
|
|
84
|
+
let released = false;
|
|
85
|
+
return () => {
|
|
86
|
+
if (released) return;
|
|
87
|
+
released = true;
|
|
88
|
+
setPends((n) => n - 1);
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
open
|
|
92
|
+
}), [open]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!answers.length) return;
|
|
95
|
+
for (const a of answers) {
|
|
96
|
+
const form = a.element();
|
|
97
|
+
if (form?.isConnected && (form.querySelector("[aria-invalid='true']") || a.failing())) continue;
|
|
98
|
+
if (a.how === "returned") {
|
|
99
|
+
const now = a.root?.isConnected ? controlsOf(a.root) : "";
|
|
100
|
+
if (now !== "" && now !== a.controls) continue;
|
|
101
|
+
}
|
|
102
|
+
for (const [id, f] of forms.current) {
|
|
103
|
+
if (f.stamp === 0 || f.stamp > a.since || id !== a.form && f.mounted) continue;
|
|
104
|
+
if (f.mounted) forms.current.set(id, { ...f, stamp: 0 });
|
|
105
|
+
else forms.current.delete(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
setAnswers([]);
|
|
109
|
+
setChanged([...forms.current.values()].some((f) => f.stamp > 0));
|
|
110
|
+
}, [answers]);
|
|
111
|
+
return { marked: changed || holds > 0, pending: pends > 0, mark };
|
|
112
|
+
}
|
|
113
|
+
var OverlayCloseCtx = createContext(null);
|
|
114
|
+
function useDirtyGuard(dirty, onClose, onDiscard) {
|
|
115
|
+
const [wantsConfirm, setWantsConfirm] = useState(false);
|
|
116
|
+
const confirming = wantsConfirm && !!dirty;
|
|
117
|
+
const pending = useRef(void 0);
|
|
118
|
+
const runAfter = useCallback((then) => {
|
|
119
|
+
if (typeof then === "function" && then !== onClose) then();
|
|
120
|
+
}, [onClose]);
|
|
121
|
+
const requestClose = useCallback((then) => {
|
|
122
|
+
if (!dirty) {
|
|
123
|
+
onClose();
|
|
124
|
+
runAfter(then);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
pending.current = then;
|
|
128
|
+
setWantsConfirm((c) => !c);
|
|
129
|
+
}, [dirty, onClose, runAfter]);
|
|
130
|
+
const keep = useCallback(() => {
|
|
131
|
+
pending.current = void 0;
|
|
132
|
+
setWantsConfirm(false);
|
|
133
|
+
}, []);
|
|
134
|
+
const discard = useCallback(() => {
|
|
135
|
+
setWantsConfirm(false);
|
|
136
|
+
onDiscard?.();
|
|
137
|
+
onClose();
|
|
138
|
+
const then = pending.current;
|
|
139
|
+
pending.current = void 0;
|
|
140
|
+
runAfter(then);
|
|
141
|
+
}, [onClose, onDiscard, runAfter]);
|
|
142
|
+
return { confirming, requestClose, keep, discard };
|
|
143
|
+
}
|
|
144
|
+
function DiscardConfirm({ open, onKeep, onDiscard }) {
|
|
145
|
+
const keepRef = useRef(null);
|
|
146
|
+
const returnTo = useRef(null);
|
|
147
|
+
const overlay = useRef(null);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!open) return;
|
|
150
|
+
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
151
|
+
overlay.current = keepRef.current?.closest("[role='dialog'], [role='alertdialog']") ?? null;
|
|
152
|
+
returnTo.current = active && active !== document.body && overlay.current?.contains(active) ? active : null;
|
|
153
|
+
keepRef.current?.focus();
|
|
154
|
+
return () => {
|
|
155
|
+
if (document.activeElement !== document.body) return;
|
|
156
|
+
const back = returnTo.current;
|
|
157
|
+
if (back && back.isConnected) {
|
|
158
|
+
back.focus();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const first = overlay.current?.querySelector("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
|
|
162
|
+
(first ?? overlay.current)?.focus();
|
|
163
|
+
};
|
|
164
|
+
}, [open]);
|
|
165
|
+
if (!open) return null;
|
|
166
|
+
return /* @__PURE__ */ jsxs(
|
|
167
|
+
"div",
|
|
168
|
+
{
|
|
169
|
+
role: "group",
|
|
170
|
+
"aria-label": "Unsaved changes",
|
|
171
|
+
"data-discard-confirm": "",
|
|
172
|
+
className: "shrink-0 flex flex-wrap items-center justify-between gap-ds-3 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] bg-[var(--ds-status-warning-bg)]",
|
|
173
|
+
style: { padding: "var(--ds-space-3) var(--ds-page-gutter)" },
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--ds-text-primary)]", children: "You have unsaved changes." }),
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(Button, { type: "button", ref: keepRef, variant: "secondary", size: "sm", onClick: onKeep, children: "Keep editing" }),
|
|
178
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: onDiscard, children: "Discard changes" })
|
|
179
|
+
] })
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function useCompact(ref, breakpoint, band = 0) {
|
|
185
|
+
const px = typeof breakpoint === "number" ? breakpoint : readBreakpointPx(breakpoint);
|
|
186
|
+
const width = useElementWidth(ref);
|
|
187
|
+
return useBelow(width, px, band);
|
|
188
|
+
}
|
|
189
|
+
function useBelow(width, threshold, band = 0) {
|
|
190
|
+
const [below, setBelow] = useState(false);
|
|
191
|
+
const next = width > 0 && width < (below ? threshold + band : threshold);
|
|
192
|
+
if (next !== below) setBelow(next);
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
function useElementWidth(ref) {
|
|
196
|
+
const [width, setWidth] = useState(0);
|
|
197
|
+
const check = useCallback(() => {
|
|
198
|
+
const next = ref.current?.offsetWidth ?? 0;
|
|
199
|
+
if (next > 0) setWidth(next);
|
|
200
|
+
}, [ref]);
|
|
201
|
+
useLayoutEffect(() => {
|
|
202
|
+
check();
|
|
203
|
+
if (!ref.current || typeof ResizeObserver === "undefined") return;
|
|
204
|
+
const observer = new ResizeObserver(check);
|
|
205
|
+
observer.observe(ref.current);
|
|
206
|
+
return () => observer.disconnect();
|
|
207
|
+
}, [check, ref]);
|
|
208
|
+
return width;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export { DiscardConfirm, OVERLAY_ROOT_ATTR, OverlayCloseCtx, OverlayMarkCtx, OverlayMarkScope, useBelow, useCompact, useDirtyGuard, useElementWidth, useOverlayMark };
|
|
212
|
+
//# sourceMappingURL=chunk-ZQV26NND.js.map
|
|
213
|
+
//# sourceMappingURL=chunk-ZQV26NND.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../showcase/src/components/DiscardConfirm.tsx","../../../showcase/src/lib/use-compact.ts"],"names":["useState","useCallback"],"mappings":"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","file":"chunk-ZQV26NND.js","sourcesContent":["import { createContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { Button } from \"./Button\";\nimport { FormChangeCtx } from \"../lib/form-context\";\n\n/** Marks an overlay's panel: the part of the page whose controls a submit's answer is read from (0.21.0). */\nexport const OVERLAY_ROOT_ATTR = \"data-overlay-root\";\n\n/**\n * The controls a part of the page shows, by id or name: a save that keeps its form shows the same ones afterwards, a\n * step flow that moves on shows others, and a confirmation that replaces the form shows none (Sprint 25.12 A15).\n */\nfunction controlsOf(root: Element | null): string {\n if (!root) return \"\";\n const controls = root.querySelectorAll<HTMLElement>(\"input, select, textarea, [role='combobox'], [role='radiogroup'], [role='checkbox'], [role='switch'], [role='slider'], [role='spinbutton'], [contenteditable='true']\");\n return Array.from(controls).map((el) => el.id || el.getAttribute(\"name\") || el.getAttribute(\"role\") || el.tagName).join(\"|\");\n}\n\n/** A submitted form's answer, which the overlay reads once the page's answer is on screen. */\nexport interface OverlaySubmit {\n /** The page answered: its promise resolved (\"resolved\"), or its onSubmit returned no promise (\"returned\"). */\n settle: (how: \"resolved\" | \"returned\") => void;\n}\n\n/**\n * Sprint 25.12 A15 (R28-25): what an overlay knows of the forms inside it. A FormWrapper reports each change the user\n * makes, and the overlay asks before discarding while any form holds one. The overlay owns the marks, so a step flow\n * with one form per step keeps its changes after Next. A `dirty` prop the page passes wins over them. Hand-written flags\n * failed both ways in one run: the seats drawer had none, and the event drawer's was true before anything was typed.\n *\n * 0.21.0 (the review of 29 September): one mark per form, and a save clears only its own form's changes, and only those\n * made before its submit: two forms in one drawer, or a change typed while a save was running, were lost when the other\n * save cleared the drawer's single mark. A form that's gone keeps its changes until a later save or the overlay closing.\n */\nexport interface OverlayMark {\n /** The user gave one of this form's controls a new value. */\n report: (form: string) => void;\n /** This form is in the overlay until the returned function runs. */\n attach: (form: string) => () => void;\n /**\n * This form was submitted. Call before the page's onSubmit runs, then settle with its answer: the overlay reads it\n * once the answer is on screen, and clears the form's changes if it saved (FormWrapper's onSubmit says when).\n */\n submit: (form: string, from: { element: () => HTMLFormElement | null; failing: () => boolean }) => OverlaySubmit;\n /**\n * Work that closing would lose, such as a running upload (Sprint 25.12 A33): the overlay asks before closing while any\n * is held. Returns the release.\n */\n hold: () => () => void;\n /**\n * Text a control holds but hasn't committed, such as a half-typed tag (Sprint 25.29 item 15): the overlay always asks\n * before closing while any is held, whatever the page's `dirty` says, since a page's `dirty` counts only what's\n * committed. Returns the release.\n */\n pend: () => () => void;\n /** Whether the overlay is open: work held inside a kept-mounted overlay stops when it closes. */\n open: boolean;\n}\n\nexport const OverlayMarkCtx = createContext<OverlayMark | null>(null);\n\n/**\n * An overlay's children, with its mark. Controls inside answer to a FormWrapper inside the overlay, never to a form\n * around it (0.21.0): a \"New label\" dialog opened from an edit drawer's form, or a delete's typed confirmation, marked the\n * drawer.\n */\nexport function OverlayMarkScope({ mark, children }: { mark: OverlayMark; children: ReactNode }) {\n return <OverlayMarkCtx value={mark}><FormChangeCtx value={null}>{children}</FormChangeCtx></OverlayMarkCtx>;\n}\n\n/** A form's changes in its overlay: the last one's number (0: none since it saved), and whether the form is still there. */\ninterface FormMark { stamp: number; mounted: boolean }\n\ninterface Answer { form: string; since: number; root: HTMLElement | null; controls: string; how: \"resolved\" | \"returned\"; element: () => HTMLFormElement | null; failing: () => boolean }\n\n/**\n * An overlay's marks: set by a change, cleared by a save of the same form, and reset whenever the overlay closes; held\n * while work runs. `pending`: text a control holds uncommitted, which the page's `dirty` never replaces.\n */\nexport function useOverlayMark(open: boolean): { marked: boolean; pending: boolean; mark: OverlayMark } {\n const forms = useRef(new Map<string, FormMark>());\n const count = useRef(0);\n // answers from before the overlay last closed are void\n const round = useRef(0);\n const [changed, setChanged] = useState(false);\n const [holds, setHolds] = useState(0);\n const [pends, setPends] = useState(0);\n const [answers, setAnswers] = useState<Answer[]>([]);\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (!open) {\n setChanged(false);\n setAnswers([]);\n }\n }\n useEffect(() => {\n if (open) return;\n round.current++;\n for (const [id, f] of forms.current) {\n if (f.mounted) f.stamp = 0;\n else forms.current.delete(id);\n }\n }, [open]);\n const mark = useMemo<OverlayMark>(() => ({\n report: (form) => {\n const f = forms.current.get(form) ?? { stamp: 0, mounted: true };\n f.stamp = ++count.current;\n forms.current.set(form, f);\n setChanged(true);\n },\n attach: (form) => {\n const f = forms.current.get(form) ?? { stamp: 0, mounted: true };\n f.mounted = true;\n forms.current.set(form, f);\n return () => {\n const gone = forms.current.get(form);\n if (!gone) return;\n if (gone.stamp > 0) gone.mounted = false;\n else forms.current.delete(form);\n };\n },\n submit: (form, from) => {\n // the overlay's controls before the page answers, and the last change the save can carry\n const root = from.element()?.closest<HTMLElement>(`[${OVERLAY_ROOT_ATTR}]`) ?? null;\n const answer = { form, since: count.current, root, controls: controlsOf(root), ...from };\n const at = round.current;\n let settled = false;\n return {\n settle: (how) => {\n if (settled || at !== round.current) return;\n settled = true;\n setAnswers((list) => [...list, { ...answer, how }]);\n },\n };\n },\n hold: () => {\n setHolds((n) => n + 1);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n setHolds((n) => n - 1);\n };\n },\n pend: () => {\n setPends((n) => n + 1);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n setPends((n) => n - 1);\n };\n },\n open,\n }), [open]);\n\n // An answer is read once it is on screen (this runs after the commit that carries the page's answer):\n // - a field still invalid in the form, or a check of its own failing: nothing saved;\n // - a submit that returned no promise and now shows other controls: a step flow moved on, and saves at its end;\n // - otherwise it saved: its form's changes up to its submit clear, with those of forms that have gone (a flow's steps).\n /* eslint-disable react-hooks/set-state-in-effect -- the answer is a DOM read after the page's answer commits */\n useEffect(() => {\n if (!answers.length) return;\n for (const a of answers) {\n const form = a.element();\n if (form?.isConnected && (form.querySelector(\"[aria-invalid='true']\") || a.failing())) continue;\n if (a.how === \"returned\") {\n const now = a.root?.isConnected ? controlsOf(a.root) : \"\";\n if (now !== \"\" && now !== a.controls) continue;\n }\n for (const [id, f] of forms.current) {\n if (f.stamp === 0 || f.stamp > a.since || (id !== a.form && f.mounted)) continue;\n if (f.mounted) forms.current.set(id, { ...f, stamp: 0 });\n else forms.current.delete(id);\n }\n }\n setAnswers([]);\n setChanged([...forms.current.values()].some((f) => f.stamp > 0));\n }, [answers]);\n /* eslint-enable react-hooks/set-state-in-effect */\n return { marked: changed || holds > 0, pending: pends > 0, mark };\n}\n\n/**\n * The open overlay's guarded close (round 21): a Drawer, Dialog, or a Sheet given `dirty`, provides it so a form inside\n * can close the overlay through the same unsaved-changes question Escape and the close control ask. FormWrapper's\n * `onCancel` uses it; run 58's hand-wired Cancel closed a half-filled invite with no question while Escape asked.\n * `then` runs once the overlay has closed — never when the user keeps editing (the review of 23 September: the\n * form's own onCancel was dropped inside an overlay).\n */\nexport const OverlayCloseCtx = createContext<((then?: () => void) => void) | null>(null);\n\n/**\n * The dirty guard an overlay with a form shares (Sprint 25.2, C1): while the\n * content is dirty, every close path — Escape, backdrop, the close control —\n * asks first, inside the overlay rather than in a second dialog. The page\n * passes `dirty`; the overlay does the rest.\n */\nexport function useDirtyGuard(dirty: boolean | undefined, onClose: () => void, onDiscard?: () => void) {\n const [wantsConfirm, setWantsConfirm] = useState(false);\n const confirming = wantsConfirm && !!dirty;\n // What runs after the close goes ahead (a form's own onCancel). A close path wired as a click handler passes its event\n // here, so only a function counts; the overlay's own onClose is not run twice.\n const pending = useRef<unknown>(undefined);\n const runAfter = useCallback((then: unknown) => { if (typeof then === \"function\" && then !== onClose) (then as () => void)(); }, [onClose]);\n const requestClose = useCallback((then?: unknown) => {\n if (!dirty) { onClose(); runAfter(then); return; }\n pending.current = then;\n // A second close request while the question is showing (Escape again, another backdrop click) keeps editing.\n setWantsConfirm((c) => !c);\n }, [dirty, onClose, runAfter]);\n const keep = useCallback(() => { pending.current = undefined; setWantsConfirm(false); }, []);\n const discard = useCallback(() => {\n setWantsConfirm(false);\n onDiscard?.();\n onClose();\n const then = pending.current;\n pending.current = undefined;\n runAfter(then);\n }, [onClose, onDiscard, runAfter]);\n return { confirming, requestClose, keep, discard };\n}\n\nexport function DiscardConfirm({ open, onKeep, onDiscard }: { open: boolean; onKeep: () => void; onDiscard: () => void }) {\n const keepRef = useRef<HTMLButtonElement>(null);\n const returnTo = useRef<HTMLElement | null>(null);\n const overlay = useRef<HTMLElement | null>(null);\n // Focus moves to \"Keep editing\" while the question shows and goes back where it was when it closes — or to the\n // overlay's first control when nothing inside it had focus — so a non-modal drawer keeps answering Escape and a\n // screen reader keeps its place (Sprint 25.2 review).\n useEffect(() => {\n if (!open) return;\n const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n overlay.current = keepRef.current?.closest<HTMLElement>(\"[role='dialog'], [role='alertdialog']\") ?? null;\n returnTo.current = active && active !== document.body && overlay.current?.contains(active) ? active : null;\n keepRef.current?.focus();\n return () => {\n if (document.activeElement !== document.body) return;\n const back = returnTo.current;\n if (back && back.isConnected) { back.focus(); return; }\n const first = overlay.current?.querySelector<HTMLElement>(\"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])\");\n (first ?? overlay.current)?.focus();\n };\n }, [open]);\n if (!open) return null;\n return (\n <div\n role=\"group\"\n aria-label=\"Unsaved changes\"\n data-discard-confirm=\"\"\n className=\"shrink-0 flex flex-wrap items-center justify-between gap-ds-3 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] bg-[var(--ds-status-warning-bg)]\"\n // at the overlay's inset, as its header, body and footer are (Sprint 25.14 A36)\n style={{ padding: \"var(--ds-space-3) var(--ds-page-gutter)\" }}\n >\n <p className=\"text-sm text-[var(--ds-text-primary)]\">You have unsaved changes.</p>\n <div className=\"flex items-center gap-ds-2\">\n <Button type=\"button\" ref={keepRef} variant=\"secondary\" size=\"sm\" onClick={onKeep}>Keep editing</Button>\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={onDiscard}>Discard changes</Button>\n </div>\n </div>\n );\n}\n","import { useState, useCallback, useLayoutEffect, type RefObject } from \"react\";\nimport { readBreakpointPx, type BreakpointKey } from \"./use-breakpoint\";\n\n/**\n * True when the observed element is narrower than the breakpoint — a container\n * measurement, not the viewport, so a layout inside a sidebar shell or the\n * builder's canvas answers for the width it actually has. One implementation\n * for TableToolbar, SettingsLayout and the examples (Sprint 25.2).\n *\n * An unlaid-out element (tests, SSR) reports width 0 and is treated as not\n * compact; tests inject a width by overriding `offsetWidth`.\n */\nexport function useCompact(ref: RefObject<HTMLElement | null>, breakpoint: number | BreakpointKey, band = 0): boolean {\n const px = typeof breakpoint === \"number\" ? breakpoint : readBreakpointPx(breakpoint);\n const width = useElementWidth(ref);\n return useBelow(width, px, band);\n}\n\n/**\n * Whether a width is below a threshold, held across `band` px above it once it is (round 27 review): a layout whose\n * answer changes the width it measures — an aside that collapses removes a page scrollbar, which widens the layout\n * back over the threshold — otherwise flips on every frame at the edge (SplitView's aside toggled about 90 times in\n * 1.5 s with a 15 px scrollbar). The answer turns true below `threshold` and false again only at `threshold + band`.\n * A width of 0 (no layout yet) is never below.\n */\nexport function useBelow(width: number, threshold: number, band = 0): boolean {\n const [below, setBelow] = useState(false);\n const next = width > 0 && width < (below ? threshold + band : threshold);\n // render-time state adjustment: the answer is known now, so no frame paints the other one\n if (next !== below) setBelow(next);\n return next;\n}\n\n/** The element's laid-out width in px, kept current by a ResizeObserver; 0 until it has a layout (tests, SSR). */\nexport function useElementWidth(ref: RefObject<HTMLElement | null>): number {\n const [width, setWidth] = useState(0);\n const check = useCallback(() => {\n const next = ref.current?.offsetWidth ?? 0;\n if (next > 0) setWidth(next);\n }, [ref]);\n useLayoutEffect(() => {\n check();\n if (!ref.current || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(check);\n observer.observe(ref.current);\n return () => observer.disconnect();\n }, [check, ref]);\n return width;\n}\n"]}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { IconName } from './icons.js';
|
|
4
|
+
import { a as DropdownMenuEntry } from './DropdownMenu-DqT7ZQ0D.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sprint 32 (A2) — a dashboard's layout, as plain data: its widgets in reading order, each with a width in columns of 12
|
|
8
|
+
* and a height in rows of a fixed height. The grid lays them out as text is laid out: left to right, then the next row,
|
|
9
|
+
* each taking the first space after the one before that fits it (CSS grid's sparse auto-placement, never `dense`), so
|
|
10
|
+
* what's on screen, what a screen reader reads and where Tab goes are one order by construction.
|
|
11
|
+
*
|
|
12
|
+
* Server-safe: no React and no browser, so `gridsmith-ui/utils` exports the helpers (normaliseLayout, moveItem,
|
|
13
|
+
* resizeItem, addItem, removeItem) and a page that only adds a widget, or a test that checks a layout, needs no drag
|
|
14
|
+
* code. The rest is the grid's own: where widgets land (`placeLayout`, a copy of the browser's placement rule, held equal
|
|
15
|
+
* to Chrome's by a Chrome test), where a key or a pointer would take one (`stepTarget`, `nearestIndex`) and the words a
|
|
16
|
+
* screen reader hears for a size. Where things land is worked out here, from the layout, never measured from the
|
|
17
|
+
* screen: measuring a preview feeds back into it, the flicker sortable grids are known for.
|
|
18
|
+
*/
|
|
19
|
+
interface DashboardLayoutItem {
|
|
20
|
+
id: string;
|
|
21
|
+
/** Its width, in columns of 12. */
|
|
22
|
+
w: number;
|
|
23
|
+
/** Its height, in rows. */
|
|
24
|
+
h: number;
|
|
25
|
+
/** The narrowest it may be made, in columns (default 3, a quarter of the width; a widget is never narrower). */
|
|
26
|
+
minW?: number;
|
|
27
|
+
/** The shortest it may be made, in rows (default 2). */
|
|
28
|
+
minH?: number;
|
|
29
|
+
/** The widest it may be made, in columns (default 12). */
|
|
30
|
+
maxW?: number;
|
|
31
|
+
/** The tallest it may be made, in rows. */
|
|
32
|
+
maxH?: number;
|
|
33
|
+
}
|
|
34
|
+
type DashboardLayout = DashboardLayoutItem[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sprint 32 (A1–A13) — the dashboard grid: a dashboard the people using the app arrange themselves. Its widgets sit in a
|
|
38
|
+
* reading order on 12 columns (lib/dashboard-layout.ts), each one of the library's titled frames, which the grid only
|
|
39
|
+
* places: no frame of its own, so never a card in a card (A3). In the view it is an ordinary page (custom_interaction#20):
|
|
40
|
+
* nothing drags, and a widget's own links and clicks work as anywhere else. In its edit mode, entered on purpose, each
|
|
41
|
+
* widget gets three controls beside its frame, never inside it (A4): a move handle, a ⋯ menu (Set up, Move, Size and the
|
|
42
|
+
* page's own actions) and a resize grip. Every drag has a keyboard way and a plain click way (accessibility#26), and
|
|
43
|
+
* everything is said in the grid's own polite live region, with the widget's name, never its id (A7).
|
|
44
|
+
*
|
|
45
|
+
* The pointer and touch go through dnd-kit 6.3.1, the Kanban's: the mouse sensor after 5 px and the touch sensor after a
|
|
46
|
+
* hold (never the pointer sensor, which also answers touch and would start on a 5 px swipe), its auto-scroll for the page,
|
|
47
|
+
* and nothing of its own said. The keyboard's moves are the grid's own state, never dnd-kit's keyboard sensor (A5).
|
|
48
|
+
*/
|
|
49
|
+
/** A kind of widget the palette offers (A8): its name, an optional description and icon, and the size it starts at. */
|
|
50
|
+
interface DashboardWidgetKind {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
icon?: IconName;
|
|
55
|
+
/** Its starting width, in columns of 12. A chart's kind starts tall enough to hold the chart's floor (5 rows). */
|
|
56
|
+
w: number;
|
|
57
|
+
/** Its starting height, in rows. */
|
|
58
|
+
h: number;
|
|
59
|
+
}
|
|
60
|
+
interface DashboardItemProps {
|
|
61
|
+
/** The widget's id, as its layout entry has it. */
|
|
62
|
+
id: string;
|
|
63
|
+
/**
|
|
64
|
+
* The widget's name, its frame's title on screen ("Cash balance"): what a screen reader hears on the grid's controls
|
|
65
|
+
* ("Move Cash balance") and in what the grid says ("Cash balance, position 3 of 8"). In development a label that
|
|
66
|
+
* differs from the frame's title is warned about.
|
|
67
|
+
*/
|
|
68
|
+
label: string;
|
|
69
|
+
/** The widget: one of the library's titled frames (a ChartContainer, a StatsCard, a titled Table, a Card with a CardHeader). */
|
|
70
|
+
children: ReactNode;
|
|
71
|
+
}
|
|
72
|
+
interface DashboardGridProps {
|
|
73
|
+
/**
|
|
74
|
+
* The widgets in reading order, each with a width in columns of 12 and a height in rows: plain data the page saves as it
|
|
75
|
+
* likes. Authoritative: the grid draws what it's given, normalised (normaliseLayout), each widget once, in this order.
|
|
76
|
+
*/
|
|
77
|
+
layout: readonly DashboardLayoutItem[];
|
|
78
|
+
/** The layout after a change the person made: once per change kept (a drop, a menu choice, a keyboard drop, a resize kept). */
|
|
79
|
+
onLayoutChange?: (layout: DashboardLayout) => void;
|
|
80
|
+
/**
|
|
81
|
+
* The edit mode, entered on purpose (custom_interaction#20): handles, grips and the column guides, and each widget's
|
|
82
|
+
* content inert, so a press on a chart never opens its report mid-edit. Off, the dashboard is an ordinary page.
|
|
83
|
+
*/
|
|
84
|
+
editing?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Edit mode only: the person chose a widget to set up, from its ⋯ menu's Set up or by a click on the widget (one that
|
|
87
|
+
* began on it, never the end of a drag or a resize). Show its settings in the page's side panel.
|
|
88
|
+
*/
|
|
89
|
+
onSelect?: (id: string) => void;
|
|
90
|
+
/**
|
|
91
|
+
* A widget's own entries for its ⋯ menu (Duplicate, Remove…), after the grid's Set up, Move and Size, as a Kanban card's
|
|
92
|
+
* cardActions. A Remove that takes the widget out puts focus on the same control in the widget before, else after.
|
|
93
|
+
*/
|
|
94
|
+
itemActions?: (id: string) => DropdownMenuEntry[];
|
|
95
|
+
/**
|
|
96
|
+
* A widget of this kind added from the palette, by a click on its row (at the end) or a drop (where it lands): add the
|
|
97
|
+
* page's widget and its layout entry (addItem), and return the new widget's id, or a promise of it for an id the server
|
|
98
|
+
* makes. Focus goes to the new widget's move handle (its ⋯ on a stacked grid).
|
|
99
|
+
*/
|
|
100
|
+
onAdd?: (kind: DashboardWidgetKind, index: number) => string | PromiseLike<string>;
|
|
101
|
+
/** The widgets, each a DashboardItem: an array or a fragment of them. */
|
|
102
|
+
children?: ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* The name of the widgets' group while editing, as a screen reader hears it, and where focus goes when the last widget
|
|
105
|
+
* is removed. Default "Dashboard widgets".
|
|
106
|
+
*/
|
|
107
|
+
"aria-label"?: string;
|
|
108
|
+
className?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Sprint 32 (A8): the palette and the grid share one drag, so a widget library's kind can be dragged onto the grid. A
|
|
112
|
+
* page that uses both wraps them in a DashboardEditor (anywhere above both: the side panel and the page's content); a
|
|
113
|
+
* grid alone needs none. One grid to an editor: each tab's grid in turn, as only the open tab's is drawn.
|
|
114
|
+
*/
|
|
115
|
+
declare function DashboardEditor({ children }: {
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
}): react_jsx_runtime.JSX.Element;
|
|
118
|
+
/**
|
|
119
|
+
* One widget on a DashboardGrid (A3). The grid reads its props and draws it in a cell of its own, in the layout's order;
|
|
120
|
+
* on its own it draws its children.
|
|
121
|
+
*/
|
|
122
|
+
declare function DashboardItem({ children }: DashboardItemProps): react_jsx_runtime.JSX.Element;
|
|
123
|
+
/** The grid drawn in a drag arena: an editor's, else one of its own (a grid alone needs no editor). */
|
|
124
|
+
declare function DashboardGrid(props: DashboardGridProps): react_jsx_runtime.JSX.Element;
|
|
125
|
+
|
|
126
|
+
interface DashboardPaletteProps {
|
|
127
|
+
/** The kinds of widget the page offers (headline number, line chart, table, text note, image…), in the order listed. */
|
|
128
|
+
kinds: DashboardWidgetKind[];
|
|
129
|
+
/** The list's name, as a screen reader hears it. Default "Widget library". */
|
|
130
|
+
label?: string;
|
|
131
|
+
/**
|
|
132
|
+
* The line above the list that says what a row does: by default "Select or drag to add" while the rows drag,
|
|
133
|
+
* and "Select a widget to add it" where they don't (a grid stacked at a narrow width); none while the grid isn't
|
|
134
|
+
* editing. A function gives other words for each case (it's told whether the rows drag); false draws none.
|
|
135
|
+
*/
|
|
136
|
+
hint?: false | ((rowsDrag: boolean) => string);
|
|
137
|
+
className?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Sprint 32 (A8): the widget library the briefs ask for, a list of the kinds of widget a page offers, each with its name,
|
|
141
|
+
* an optional description and icon. Each kind is one control, a button named "Add {kind}" (the look fixes, D5): a click,
|
|
142
|
+
* Enter or Space adds the widget at the end (the plain click way, accessibility#26), and a pointer's drag of the row
|
|
143
|
+
* places it where it's dropped (the grid's sensors tell the two apart: 5 px of movement for a mouse, a 250 ms press for a
|
|
144
|
+
* finger). The grid asks the page for the widget (its onAdd), focus goes to the new widget's move handle, and the grid's
|
|
145
|
+
* live region says where it went. It reaches the grid through the DashboardEditor round them both.
|
|
146
|
+
*
|
|
147
|
+
* Where it sits, so the grid keeps its width (the guidance's composition): in an AsidePanel with `width="312px"
|
|
148
|
+
* collapseBelow="xl" resizable={false}`, docked beside the grid from 1280 px and a drawer over the page below, its body
|
|
149
|
+
* padded as the panel's heading is (each row's hover tint and focus outline reach 8 px into that padding). An Add closes
|
|
150
|
+
* the overlay the panel has become, wherever it opens over the page (the drawer, or the phone's sheet), so the new widget
|
|
151
|
+
* isn't left under it; docked beside the grid, the panel stays open. Wherever the grid is stacked (narrower than 40rem)
|
|
152
|
+
* nothing drags, and focus goes to the new widget's ⋯, since a stacked grid has no handle. Adding and dragging are off
|
|
153
|
+
* while the grid isn't editing.
|
|
154
|
+
*
|
|
155
|
+
* A line above the list says what a row does (the owner's look at the second sheet: the rows drag, so say so), as visible
|
|
156
|
+
* text before the list rather than its description, which screen readers read unreliably: its words follow whether the
|
|
157
|
+
* rows drag (`hint`), and there are none while the grid isn't editing, outside a DashboardEditor, or with no kinds. The
|
|
158
|
+
* list keeps its name, its data attribute and the className, so the line is the list's sibling, not its wrapper.
|
|
159
|
+
*/
|
|
160
|
+
declare function DashboardPalette({ kinds, label, hint, className }: DashboardPaletteProps): react_jsx_runtime.JSX.Element;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Sprint 32 (A10): a draft the person edits and then saves or cancels as a whole: a dashboard's tabs (added, renamed,
|
|
164
|
+
* reordered, removed), each tab's layout and each widget's settings, in whatever shape the page gives it. Every change can be
|
|
165
|
+
* undone and redone (custom_interaction#26), from the page's visible Undo and Redo buttons and from the keyboard:
|
|
166
|
+
* Ctrl+Z or ⌘Z undoes, with Shift it redoes, outside text fields only, so a field's own undo keeps working.
|
|
167
|
+
*/
|
|
168
|
+
interface DraftHistory<T> {
|
|
169
|
+
/** The draft now. */
|
|
170
|
+
value: T;
|
|
171
|
+
/**
|
|
172
|
+
* Changes the draft: a new value, or a function of the one before. Each change is a step that Undo takes back, and a
|
|
173
|
+
* change after an Undo drops what Redo had. Pass `merge` (a field's name, say) for a run of changes that is one step:
|
|
174
|
+
* each keystroke in a widget's title field, so Undo takes back the edit, not a letter. A change to the same value
|
|
175
|
+
* adds no step.
|
|
176
|
+
*/
|
|
177
|
+
set: (next: T | ((prev: T) => T), merge?: string) => void;
|
|
178
|
+
undo: () => void;
|
|
179
|
+
redo: () => void;
|
|
180
|
+
canUndo: boolean;
|
|
181
|
+
canRedo: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* The draft differs from what was last saved (the first value, or the last `reset`): `dirty` enables Save. Cancel always
|
|
184
|
+
* goes back to the view, calling `reset(saved)` first, so it's never disabled.
|
|
185
|
+
*/
|
|
186
|
+
dirty: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Starts again from a saved value, with no steps to undo: after Save, `reset()` takes the draft as saved; Cancel
|
|
189
|
+
* passes the saved value back, `reset(saved)`, and the draft is that. A value loaded after the first render is passed
|
|
190
|
+
* here too.
|
|
191
|
+
*/
|
|
192
|
+
reset: (next?: T) => void;
|
|
193
|
+
}
|
|
194
|
+
interface DraftHistoryOptions {
|
|
195
|
+
/** Ctrl+Z, ⌘Z and their Shift forms on the page. Default true; off where two drafts share a page, or a dialog has its own. */
|
|
196
|
+
shortcuts?: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A draft held whole until it's saved, with undo and redo: a dashboard's tabs, each tab's layout and each widget's
|
|
200
|
+
* settings in one value, or anything else a page edits before Save. `set` changes it, one step Undo takes back; `dirty`
|
|
201
|
+
* says it differs from what was last saved, and enables Save; Cancel always goes back to the view, calling
|
|
202
|
+
* `reset(saved)` first; `reset()` after Save. Ctrl+Z or ⌘Z undoes and with Shift redoes, outside text fields, unless
|
|
203
|
+
* `shortcuts` is false.
|
|
204
|
+
*/
|
|
205
|
+
declare function useDraftHistory<T>(initial: T | (() => T), { shortcuts }?: DraftHistoryOptions): DraftHistory<T>;
|
|
206
|
+
|
|
207
|
+
export { DashboardEditor, DashboardGrid, type DashboardGridProps, DashboardItem, type DashboardItemProps, type DashboardLayout, type DashboardLayoutItem, DashboardPalette, type DashboardPaletteProps, type DashboardWidgetKind, type DraftHistory, type DraftHistoryOptions, useDraftHistory };
|