@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.
Files changed (102) hide show
  1. package/dist/{chunk-ERRD2KX7.cjs → chunk-2JFML4CT.cjs} +61 -61
  2. package/dist/{chunk-ERRD2KX7.cjs.map → chunk-2JFML4CT.cjs.map} +1 -1
  3. package/dist/{chunk-YVLIC36O.js → chunk-2XBUB2OM.js} +4 -4
  4. package/dist/{chunk-CKU7C2NQ.cjs → chunk-343RZD5C.cjs} +11 -11
  5. package/dist/{chunk-CKU7C2NQ.cjs.map → chunk-343RZD5C.cjs.map} +1 -1
  6. package/dist/{chunk-6MOD65VQ.cjs → chunk-4S6HEFR7.cjs} +87 -87
  7. package/dist/{chunk-6MOD65VQ.cjs.map → chunk-4S6HEFR7.cjs.map} +1 -1
  8. package/dist/{chunk-5W2OUL2H.cjs → chunk-4TPHYUPY.cjs} +9 -9
  9. package/dist/{chunk-5W2OUL2H.cjs.map → chunk-4TPHYUPY.cjs.map} +1 -1
  10. package/dist/{chunk-3DERWWWX.cjs → chunk-53WLIAKO.cjs} +37 -37
  11. package/dist/{chunk-3DERWWWX.cjs.map → chunk-53WLIAKO.cjs.map} +1 -1
  12. package/dist/{chunk-AFQZIKIC.cjs → chunk-57V3X73U.cjs} +27 -27
  13. package/dist/{chunk-AFQZIKIC.cjs.map → chunk-57V3X73U.cjs.map} +1 -1
  14. package/dist/{chunk-CHZ57UPO.js → chunk-5UBALP5G.js} +4 -4
  15. package/dist/{chunk-K3GU3GXP.js → chunk-64FJ5QLI.js} +2 -2
  16. package/dist/{chunk-RCEVUZEI.js → chunk-77C6Q622.js} +2 -2
  17. package/dist/{chunk-QZLWVIT4.cjs → chunk-77NHAQIC.cjs} +14 -14
  18. package/dist/{chunk-QZLWVIT4.cjs.map → chunk-77NHAQIC.cjs.map} +1 -1
  19. package/dist/{chunk-3MMSQ7SC.js → chunk-7NWFATGX.js} +2 -2
  20. package/dist/{chunk-HDDPE36K.js → chunk-APKL3K57.js} +2 -2
  21. package/dist/{chunk-YMXJJ4MY.cjs → chunk-CZGRFEDX.cjs} +31 -31
  22. package/dist/{chunk-YMXJJ4MY.cjs.map → chunk-CZGRFEDX.cjs.map} +1 -1
  23. package/dist/{chunk-CJ4KV7D2.js → chunk-DSOJBDJ2.js} +2 -2
  24. package/dist/{chunk-EOZBJY22.js → chunk-FORSAXLQ.js} +2 -2
  25. package/dist/{chunk-DCQN7QEU.js → chunk-HLJXBIQT.js} +5 -5
  26. package/dist/{chunk-5NZLZTUD.js → chunk-I5AVSFMX.js} +6 -6
  27. package/dist/{chunk-M4H6XBEL.js → chunk-KMOPF7BT.js} +3693 -3579
  28. package/dist/chunk-KMOPF7BT.js.map +1 -0
  29. package/dist/{chunk-CHQOAONB.cjs → chunk-NFUVR7OE.cjs} +5 -5
  30. package/dist/{chunk-CHQOAONB.cjs.map → chunk-NFUVR7OE.cjs.map} +1 -1
  31. package/dist/{chunk-TW6NP2IH.cjs → chunk-QUPQLQ4R.cjs} +776 -662
  32. package/dist/chunk-QUPQLQ4R.cjs.map +1 -0
  33. package/dist/{chunk-SDXCNTEC.cjs → chunk-UHP554ZG.cjs} +7 -7
  34. package/dist/{chunk-SDXCNTEC.cjs.map → chunk-UHP554ZG.cjs.map} +1 -1
  35. package/dist/{chunk-5CWG5JAO.cjs → chunk-UYH2NLM4.cjs} +4 -4
  36. package/dist/{chunk-5CWG5JAO.cjs.map → chunk-UYH2NLM4.cjs.map} +1 -1
  37. package/dist/{chunk-MEJDX5UP.cjs → chunk-V7WUMODL.cjs} +3 -3
  38. package/dist/{chunk-MEJDX5UP.cjs.map → chunk-V7WUMODL.cjs.map} +1 -1
  39. package/dist/{chunk-3UCQWXA3.cjs → chunk-WGJFHOU4.cjs} +26 -26
  40. package/dist/{chunk-3UCQWXA3.cjs.map → chunk-WGJFHOU4.cjs.map} +1 -1
  41. package/dist/{chunk-J6AT6PCW.js → chunk-WLDFAJDL.js} +2 -2
  42. package/dist/{chunk-OYNCKOLU.js → chunk-XCJPDYKU.js} +2 -2
  43. package/dist/{chunk-Q6I6XGMX.js → chunk-XDPVVHBF.js} +2 -2
  44. package/dist/components/case-studies/index.cjs +8 -8
  45. package/dist/components/case-studies/index.js +2 -2
  46. package/dist/components/chat/index.cjs +2 -2
  47. package/dist/components/chat/index.js +1 -1
  48. package/dist/components/contact/index.cjs +3 -3
  49. package/dist/components/contact/index.js +2 -2
  50. package/dist/components/docs/index.cjs +5 -5
  51. package/dist/components/docs/index.js +4 -4
  52. package/dist/components/embeds/index.cjs +3 -3
  53. package/dist/components/embeds/index.js +2 -2
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/index.cjs +2 -2
  57. package/dist/components/features/index.js +1 -1
  58. package/dist/components/help-center-pages/index.cjs +22 -22
  59. package/dist/components/help-center-pages/index.js +13 -13
  60. package/dist/components/index.cjs +134 -132
  61. package/dist/components/index.cjs.map +1 -1
  62. package/dist/components/index.js +13 -11
  63. package/dist/components/index.js.map +1 -1
  64. package/dist/components/meeting-scheduler/index.cjs +34 -34
  65. package/dist/components/meeting-scheduler/index.js +3 -3
  66. package/dist/components/navigation/index.cjs +2 -2
  67. package/dist/components/navigation/index.js +1 -1
  68. package/dist/components/onboarding-guides/index.cjs +5 -5
  69. package/dist/components/onboarding-guides/index.js +4 -4
  70. package/dist/components/related-content/index.cjs +3 -3
  71. package/dist/components/related-content/index.js +2 -2
  72. package/dist/components/tickets/index.cjs +6 -6
  73. package/dist/components/tickets/index.js +5 -5
  74. package/dist/components/ui/admin-form-modal.d.ts +73 -0
  75. package/dist/components/ui/admin-form-modal.d.ts.map +1 -0
  76. package/dist/components/ui/index.cjs +4 -2
  77. package/dist/components/ui/index.cjs.map +1 -1
  78. package/dist/components/ui/index.d.ts +1 -0
  79. package/dist/components/ui/index.d.ts.map +1 -1
  80. package/dist/components/ui/index.js +3 -1
  81. package/dist/index.cjs +4 -2
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.js +3 -1
  84. package/package.json +1 -1
  85. package/src/components/ui/.admin-form-modal.md +18 -0
  86. package/src/components/ui/admin-form-modal.tsx +255 -0
  87. package/src/components/ui/index.ts +1 -0
  88. package/dist/chunk-M4H6XBEL.js.map +0 -1
  89. package/dist/chunk-TW6NP2IH.cjs.map +0 -1
  90. /package/dist/{chunk-YVLIC36O.js.map → chunk-2XBUB2OM.js.map} +0 -0
  91. /package/dist/{chunk-CHZ57UPO.js.map → chunk-5UBALP5G.js.map} +0 -0
  92. /package/dist/{chunk-K3GU3GXP.js.map → chunk-64FJ5QLI.js.map} +0 -0
  93. /package/dist/{chunk-RCEVUZEI.js.map → chunk-77C6Q622.js.map} +0 -0
  94. /package/dist/{chunk-3MMSQ7SC.js.map → chunk-7NWFATGX.js.map} +0 -0
  95. /package/dist/{chunk-HDDPE36K.js.map → chunk-APKL3K57.js.map} +0 -0
  96. /package/dist/{chunk-CJ4KV7D2.js.map → chunk-DSOJBDJ2.js.map} +0 -0
  97. /package/dist/{chunk-EOZBJY22.js.map → chunk-FORSAXLQ.js.map} +0 -0
  98. /package/dist/{chunk-DCQN7QEU.js.map → chunk-HLJXBIQT.js.map} +0 -0
  99. /package/dist/{chunk-5NZLZTUD.js.map → chunk-I5AVSFMX.js.map} +0 -0
  100. /package/dist/{chunk-J6AT6PCW.js.map → chunk-WLDFAJDL.js.map} +0 -0
  101. /package/dist/{chunk-OYNCKOLU.js.map → chunk-XCJPDYKU.js.map} +0 -0
  102. /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
+ }
@@ -43,6 +43,7 @@ export * from './image-gallery-modal'
43
43
  export * from './modal'
44
44
  export * from './modal-v2'
45
45
  export * from './modal-guarded-close'
46
+ export * from './admin-form-modal'
46
47
  export * from './separator'
47
48
  export * from './sheet'
48
49
  export * from './drawer'