@flamingo-stack/openframe-frontend-core 0.0.546 → 0.0.547
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/dist/{chunk-ERRD2KX7.cjs → chunk-2JFML4CT.cjs} +61 -61
- package/dist/{chunk-ERRD2KX7.cjs.map → chunk-2JFML4CT.cjs.map} +1 -1
- package/dist/{chunk-YVLIC36O.js → chunk-2XBUB2OM.js} +4 -4
- package/dist/{chunk-CKU7C2NQ.cjs → chunk-343RZD5C.cjs} +11 -11
- package/dist/{chunk-CKU7C2NQ.cjs.map → chunk-343RZD5C.cjs.map} +1 -1
- package/dist/{chunk-6MOD65VQ.cjs → chunk-4S6HEFR7.cjs} +87 -87
- package/dist/{chunk-6MOD65VQ.cjs.map → chunk-4S6HEFR7.cjs.map} +1 -1
- package/dist/{chunk-5W2OUL2H.cjs → chunk-4TPHYUPY.cjs} +9 -9
- package/dist/{chunk-5W2OUL2H.cjs.map → chunk-4TPHYUPY.cjs.map} +1 -1
- package/dist/{chunk-3DERWWWX.cjs → chunk-53WLIAKO.cjs} +37 -37
- package/dist/{chunk-3DERWWWX.cjs.map → chunk-53WLIAKO.cjs.map} +1 -1
- package/dist/{chunk-AFQZIKIC.cjs → chunk-57V3X73U.cjs} +27 -27
- package/dist/{chunk-AFQZIKIC.cjs.map → chunk-57V3X73U.cjs.map} +1 -1
- package/dist/{chunk-CHZ57UPO.js → chunk-5UBALP5G.js} +4 -4
- package/dist/{chunk-K3GU3GXP.js → chunk-64FJ5QLI.js} +2 -2
- package/dist/{chunk-RCEVUZEI.js → chunk-77C6Q622.js} +2 -2
- package/dist/{chunk-QZLWVIT4.cjs → chunk-77NHAQIC.cjs} +14 -14
- package/dist/{chunk-QZLWVIT4.cjs.map → chunk-77NHAQIC.cjs.map} +1 -1
- package/dist/{chunk-3MMSQ7SC.js → chunk-7NWFATGX.js} +2 -2
- package/dist/{chunk-HDDPE36K.js → chunk-APKL3K57.js} +2 -2
- package/dist/{chunk-YMXJJ4MY.cjs → chunk-CZGRFEDX.cjs} +31 -31
- package/dist/{chunk-YMXJJ4MY.cjs.map → chunk-CZGRFEDX.cjs.map} +1 -1
- package/dist/{chunk-CJ4KV7D2.js → chunk-DSOJBDJ2.js} +2 -2
- package/dist/{chunk-EOZBJY22.js → chunk-FORSAXLQ.js} +2 -2
- package/dist/{chunk-DCQN7QEU.js → chunk-HLJXBIQT.js} +5 -5
- package/dist/{chunk-5NZLZTUD.js → chunk-I5AVSFMX.js} +6 -6
- package/dist/{chunk-M4H6XBEL.js → chunk-KMOPF7BT.js} +3693 -3579
- package/dist/chunk-KMOPF7BT.js.map +1 -0
- package/dist/{chunk-CHQOAONB.cjs → chunk-NFUVR7OE.cjs} +5 -5
- package/dist/{chunk-CHQOAONB.cjs.map → chunk-NFUVR7OE.cjs.map} +1 -1
- package/dist/{chunk-TW6NP2IH.cjs → chunk-QUPQLQ4R.cjs} +776 -662
- package/dist/chunk-QUPQLQ4R.cjs.map +1 -0
- package/dist/{chunk-SDXCNTEC.cjs → chunk-UHP554ZG.cjs} +7 -7
- package/dist/{chunk-SDXCNTEC.cjs.map → chunk-UHP554ZG.cjs.map} +1 -1
- package/dist/{chunk-5CWG5JAO.cjs → chunk-UYH2NLM4.cjs} +4 -4
- package/dist/{chunk-5CWG5JAO.cjs.map → chunk-UYH2NLM4.cjs.map} +1 -1
- package/dist/{chunk-MEJDX5UP.cjs → chunk-V7WUMODL.cjs} +3 -3
- package/dist/{chunk-MEJDX5UP.cjs.map → chunk-V7WUMODL.cjs.map} +1 -1
- package/dist/{chunk-3UCQWXA3.cjs → chunk-WGJFHOU4.cjs} +26 -26
- package/dist/{chunk-3UCQWXA3.cjs.map → chunk-WGJFHOU4.cjs.map} +1 -1
- package/dist/{chunk-J6AT6PCW.js → chunk-WLDFAJDL.js} +2 -2
- package/dist/{chunk-OYNCKOLU.js → chunk-XCJPDYKU.js} +2 -2
- package/dist/{chunk-Q6I6XGMX.js → chunk-XDPVVHBF.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +134 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/admin-form-modal.d.ts +73 -0
- package/dist/components/ui/admin-form-modal.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/components/ui/.admin-form-modal.md +18 -0
- package/src/components/ui/admin-form-modal.tsx +255 -0
- package/src/components/ui/index.ts +1 -0
- package/dist/chunk-M4H6XBEL.js.map +0 -1
- package/dist/chunk-TW6NP2IH.cjs.map +0 -1
- /package/dist/{chunk-YVLIC36O.js.map → chunk-2XBUB2OM.js.map} +0 -0
- /package/dist/{chunk-CHZ57UPO.js.map → chunk-5UBALP5G.js.map} +0 -0
- /package/dist/{chunk-K3GU3GXP.js.map → chunk-64FJ5QLI.js.map} +0 -0
- /package/dist/{chunk-RCEVUZEI.js.map → chunk-77C6Q622.js.map} +0 -0
- /package/dist/{chunk-3MMSQ7SC.js.map → chunk-7NWFATGX.js.map} +0 -0
- /package/dist/{chunk-HDDPE36K.js.map → chunk-APKL3K57.js.map} +0 -0
- /package/dist/{chunk-CJ4KV7D2.js.map → chunk-DSOJBDJ2.js.map} +0 -0
- /package/dist/{chunk-EOZBJY22.js.map → chunk-FORSAXLQ.js.map} +0 -0
- /package/dist/{chunk-DCQN7QEU.js.map → chunk-HLJXBIQT.js.map} +0 -0
- /package/dist/{chunk-5NZLZTUD.js.map → chunk-I5AVSFMX.js.map} +0 -0
- /package/dist/{chunk-J6AT6PCW.js.map → chunk-WLDFAJDL.js.map} +0 -0
- /package/dist/{chunk-OYNCKOLU.js.map → chunk-XCJPDYKU.js.map} +0 -0
- /package/dist/{chunk-Q6I6XGMX.js.map → chunk-XDPVVHBF.js.map} +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId, type ReactNode } from 'react';
|
|
4
|
+
import { Button } from './button';
|
|
5
|
+
import {
|
|
6
|
+
ModalV2,
|
|
7
|
+
ModalV2Content,
|
|
8
|
+
ModalV2Footer,
|
|
9
|
+
ModalV2Header,
|
|
10
|
+
ModalV2Title,
|
|
11
|
+
ModalV2TwoColumn,
|
|
12
|
+
type ModalV2Size,
|
|
13
|
+
} from './modal-v2';
|
|
14
|
+
import { Skeleton } from './skeleton';
|
|
15
|
+
import { UnsavedChangesChip, useGuardedClose } from './modal-guarded-close';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Label widths for the loading placeholder. Uneven on purpose — a column of
|
|
19
|
+
* identical bars reads as a rendering artifact rather than a form arriving.
|
|
20
|
+
*/
|
|
21
|
+
const SKELETON_FIELD_WIDTHS = ['w-24', 'w-32', 'w-20', 'w-40', 'w-28', 'w-36'];
|
|
22
|
+
|
|
23
|
+
/** Layout: pass `children` for one column, or `leftColumn`+`rightColumn` for two. */
|
|
24
|
+
type AdminFormModalLayout =
|
|
25
|
+
| { children: ReactNode; leftColumn?: never; rightColumn?: never }
|
|
26
|
+
| { leftColumn: ReactNode; rightColumn: ReactNode; children?: never };
|
|
27
|
+
|
|
28
|
+
type AdminFormModalBaseProps = {
|
|
29
|
+
title: ReactNode;
|
|
30
|
+
/** Secondary line under the title (what this record is, why it matters). */
|
|
31
|
+
subtitle?: ReactNode;
|
|
32
|
+
isOpen: boolean;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
onSave: () => void;
|
|
35
|
+
saveLabel: string;
|
|
36
|
+
canSave?: boolean;
|
|
37
|
+
saving?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The entity is still being fetched — the modal was opened from a URL on a
|
|
40
|
+
* cold load, so there is no list row to borrow from. Content is hidden behind
|
|
41
|
+
* a placeholder and Save is held until the form has real values to submit.
|
|
42
|
+
*/
|
|
43
|
+
loading?: boolean;
|
|
44
|
+
error?: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Placeholder to show while `loading`, for modals whose body is NOT a field
|
|
47
|
+
* grid. The default bars imply a form; a modal that resolves into a list or a
|
|
48
|
+
* table would visibly change shape when the data lands.
|
|
49
|
+
*/
|
|
50
|
+
loadingContent?: ReactNode;
|
|
51
|
+
/** Secondary actions pinned to the footer's LEFT edge (Delete, Preview…). */
|
|
52
|
+
footerExtras?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Additional PRIMARY actions, rendered next to Save (Publish, Mark
|
|
55
|
+
* Complete…). A second primary action put in `footerExtras` lands at the far
|
|
56
|
+
* left, a whole modal-width away from the button it belongs beside.
|
|
57
|
+
*/
|
|
58
|
+
footerActions?: ReactNode;
|
|
59
|
+
/** Status text between the extras and the buttons (e.g. "3 rules selected"). */
|
|
60
|
+
footerStatus?: ReactNode;
|
|
61
|
+
/** Unsaved edits exist: shows the footer chip and guards close with a confirm. */
|
|
62
|
+
dirty?: boolean;
|
|
63
|
+
/** Names WHAT is dirty (chip tooltip) — turns "why is this dirty?!" into a hover. */
|
|
64
|
+
dirtyDetail?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Panel width. Two-column layouts are always `wide` — passing
|
|
67
|
+
* `leftColumn`/`rightColumn` upgrades this automatically.
|
|
68
|
+
*/
|
|
69
|
+
size?: ModalV2Size;
|
|
70
|
+
contentClassName?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* THE admin form-modal shell: header, scrolling content (one column or two), an
|
|
75
|
+
* error line, and a Cancel/Save footer with optional extra actions.
|
|
76
|
+
*
|
|
77
|
+
* Every admin form modal renders this rather than reassembling `ModalV2` +
|
|
78
|
+
* header + footer by hand. Three copies of the same shell had already been
|
|
79
|
+
* written in one feature (repo form, rule form, bindings), which is how the
|
|
80
|
+
* Save button ends up saying "Saving…" in one place and spinning silently in
|
|
81
|
+
* another.
|
|
82
|
+
*
|
|
83
|
+
* Width is a `size` variant, never a call-site `max-w-*` — the widths had
|
|
84
|
+
* drifted across seven values (`max-w-md` through `max-w-[1400px]`) for modals
|
|
85
|
+
* doing the same job.
|
|
86
|
+
*/
|
|
87
|
+
export function AdminFormModal({
|
|
88
|
+
title,
|
|
89
|
+
subtitle,
|
|
90
|
+
isOpen,
|
|
91
|
+
onClose,
|
|
92
|
+
onSave,
|
|
93
|
+
saveLabel,
|
|
94
|
+
canSave = true,
|
|
95
|
+
saving = false,
|
|
96
|
+
loading = false,
|
|
97
|
+
error = null,
|
|
98
|
+
loadingContent,
|
|
99
|
+
footerExtras,
|
|
100
|
+
footerActions,
|
|
101
|
+
footerStatus,
|
|
102
|
+
dirty = false,
|
|
103
|
+
dirtyDetail,
|
|
104
|
+
size,
|
|
105
|
+
contentClassName = 'space-y-[var(--spacing-system-lf)]',
|
|
106
|
+
children,
|
|
107
|
+
leftColumn,
|
|
108
|
+
rightColumn,
|
|
109
|
+
}: AdminFormModalBaseProps & AdminFormModalLayout) {
|
|
110
|
+
const twoColumn = leftColumn !== undefined && rightColumn !== undefined;
|
|
111
|
+
const effectiveSize: ModalV2Size = twoColumn ? 'wide' : (size ?? 'medium');
|
|
112
|
+
/** Placeholder column count follows the panel width so the skeleton occupies
|
|
113
|
+
* the same shape the real content will. */
|
|
114
|
+
const skeletonColumns = effectiveSize === 'wide' ? 2 : 1;
|
|
115
|
+
const formId = useId();
|
|
116
|
+
// The lib's unified dirty-state contract: closing a dirty settings form must
|
|
117
|
+
// never silently discard the edits — `useGuardedClose` asks the house
|
|
118
|
+
// confirm (ModalV2, never window.confirm) before running `onClose`.
|
|
119
|
+
const { guardedClose, dialog } = useGuardedClose(dirty, onClose, {
|
|
120
|
+
title: 'Discard unsaved changes?',
|
|
121
|
+
body: 'Your edits have not been saved. Close and discard them?',
|
|
122
|
+
});
|
|
123
|
+
// NO field autofocus here. `ModalV2` owns focus now (focus-in on open, focus
|
|
124
|
+
// containment while topmost, Tab trap, restore to the opener), so the shell
|
|
125
|
+
// reaching in to focus "the first field" only fought it — and picked widgets
|
|
126
|
+
// that ACT on focus: the author selector's combobox opened its popover on
|
|
127
|
+
// every `?edit=<id>` load, and on a cold URL load the programmatic focus had
|
|
128
|
+
// no preceding pointer event, so the UA painted a focus ring too. Focus lands
|
|
129
|
+
// on the dialog itself, which is the correct dialog behavior; the first Tab
|
|
130
|
+
// reaches the first field.
|
|
131
|
+
if (!isOpen) return null;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<ModalV2 isOpen onClose={guardedClose} size={effectiveSize}>
|
|
135
|
+
<ModalV2Header>
|
|
136
|
+
<ModalV2Title>{title}</ModalV2Title>
|
|
137
|
+
{/* A <div>, not a <p>. `subtitle` is a ReactNode and callers legitimately
|
|
138
|
+
pass badges and chips — `Badge` renders a <div>, which is invalid
|
|
139
|
+
inside <p>. The browser then re-parents it during hydration and React
|
|
140
|
+
throws a mismatch (seen on role-baselines, whose subtitle carries a
|
|
141
|
+
weight Badge). Typography is unchanged. */}
|
|
142
|
+
{subtitle && (
|
|
143
|
+
<div className="text-h6 text-ods-text-secondary">{subtitle}</div>
|
|
144
|
+
)}
|
|
145
|
+
</ModalV2Header>
|
|
146
|
+
|
|
147
|
+
{/* A real <form>: Enter in a text input previously did nothing in every
|
|
148
|
+
admin modal routed through this shell. */}
|
|
149
|
+
<form
|
|
150
|
+
id={formId}
|
|
151
|
+
// Inherit the ModalV2 flex chain — an unstyled form between the shell
|
|
152
|
+
// and ModalV2Content made flex-1/min-h-0 inert, so tall modals pushed
|
|
153
|
+
// the Cancel/Save footer past 90vh.
|
|
154
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
155
|
+
|
|
156
|
+
onSubmit={(e) => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
if (canSave && !saving) onSave();
|
|
159
|
+
}}
|
|
160
|
+
// Some modals contain a FILTER input, not a form field. Enter there must
|
|
161
|
+
// not ship the save — in the bindings modal that would commit an
|
|
162
|
+
// absolute-state rule-set replacement mid-search.
|
|
163
|
+
onKeyDown={(e) => {
|
|
164
|
+
if (
|
|
165
|
+
e.key === 'Enter' &&
|
|
166
|
+
(e.target as HTMLElement)?.dataset?.noSubmitOnEnter === 'true'
|
|
167
|
+
) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{loading ? (
|
|
173
|
+
// NOT `contentClassName` — that describes the real content's layout
|
|
174
|
+
// (master-detail modals pass `flex`, which turns the placeholder into
|
|
175
|
+
// a shrink-to-fit flex item pinned to the left of a 1400px panel).
|
|
176
|
+
// The placeholder owns its own layout and mirrors the column count so
|
|
177
|
+
// the modal doesn't visibly re-flow when the entity lands.
|
|
178
|
+
<ModalV2Content>
|
|
179
|
+
{loadingContent ? (
|
|
180
|
+
<div aria-busy="true">{loadingContent}</div>
|
|
181
|
+
) : (
|
|
182
|
+
<div
|
|
183
|
+
aria-busy="true"
|
|
184
|
+
className={
|
|
185
|
+
// Mirrors the PANEL WIDTH, not the slot API. `wide` covers the
|
|
186
|
+
// master-detail modals too (seo-redirect, managed-repo,
|
|
187
|
+
// exclusions) — they pass `children`, so keying on `twoColumn`
|
|
188
|
+
// drew one column of full-width bars across 1400px, which reads
|
|
189
|
+
// as a broken form rather than a loading one.
|
|
190
|
+
skeletonColumns === 2
|
|
191
|
+
? 'grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:grid-cols-2'
|
|
192
|
+
: 'grid grid-cols-1 gap-[var(--spacing-system-xl)]'
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
<span className="sr-only">Loading…</span>
|
|
196
|
+
{Array.from({ length: skeletonColumns }, (_, col) => (
|
|
197
|
+
<div key={col} className="space-y-[var(--spacing-system-lf)]" aria-hidden="true">
|
|
198
|
+
{SKELETON_FIELD_WIDTHS.map((labelWidth, i) => (
|
|
199
|
+
<div key={i} className="space-y-[var(--spacing-system-xsf)]">
|
|
200
|
+
<Skeleton className={`h-4 ${labelWidth}`} />
|
|
201
|
+
<Skeleton className="h-10 w-full" />
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</ModalV2Content>
|
|
209
|
+
) : twoColumn ? (
|
|
210
|
+
<ModalV2TwoColumn left={leftColumn} right={rightColumn} />
|
|
211
|
+
) : (
|
|
212
|
+
<ModalV2Content className={contentClassName}>{children}</ModalV2Content>
|
|
213
|
+
)}
|
|
214
|
+
{/* Outside the scrolling region(s): in a two-column modal an error
|
|
215
|
+
belongs to the save, not to either column, and must stay visible
|
|
216
|
+
wherever each column happens to be scrolled to. */}
|
|
217
|
+
{error && (
|
|
218
|
+
<p role="alert" className="text-h6 text-ods-error shrink-0">
|
|
219
|
+
{error}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
</form>
|
|
223
|
+
|
|
224
|
+
<ModalV2Footer>
|
|
225
|
+
<div className="flex w-full items-center justify-between gap-[var(--spacing-system-xsf)]">
|
|
226
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)]">{footerExtras}</div>
|
|
227
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
228
|
+
{footerStatus}
|
|
229
|
+
{dirty && <UnsavedChangesChip detail={dirtyDetail} />}
|
|
230
|
+
{footerActions}
|
|
231
|
+
<Button variant="outline" size="small-legacy" onClick={guardedClose} disabled={saving}>
|
|
232
|
+
Cancel
|
|
233
|
+
</Button>
|
|
234
|
+
<Button
|
|
235
|
+
size="small-legacy"
|
|
236
|
+
type="submit"
|
|
237
|
+
form={formId}
|
|
238
|
+
// NO `onClick` — the `form` association is the single activation
|
|
239
|
+
// path. Carrying both fired `onSave()` twice per click (the click
|
|
240
|
+
// handler, then the form's default submit), and the pending-state
|
|
241
|
+
// guard cannot catch it because React Query's re-render happens a
|
|
242
|
+
// microtask later. On the create path that meant two POSTs and two
|
|
243
|
+
// registry rows with the same name.
|
|
244
|
+
loading={saving}
|
|
245
|
+
disabled={saving || loading || !canSave}
|
|
246
|
+
>
|
|
247
|
+
{saveLabel}
|
|
248
|
+
</Button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</ModalV2Footer>
|
|
252
|
+
{dialog}
|
|
253
|
+
</ModalV2>
|
|
254
|
+
);
|
|
255
|
+
}
|