@flamingo-stack/openframe-frontend-core 0.0.544 → 0.0.545
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/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- source-hash:
|
|
1
|
+
<!-- source-hash: 0c2069899d815c77a5b73d74742e3171 -->
|
|
2
2
|
Accessible, animated modal dialog component with compound sub-components for building structured dialogs with backdrop, scroll-lock, and keyboard dismiss support.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
@@ -72,6 +72,7 @@ function MyDialog() {
|
|
|
72
72
|
- **Scroll lock** — `react-remove-scroll` (the same library Radix primitives use internally, so it composes with the lock a Select opened inside the modal adds on top). Rendered with `forwardProps` so no extra wrapper `<div>` lands in a flex/grid parent's flow
|
|
73
73
|
- **Escape key** — document-level listener, but top-of-stack only: with stacked modals (a confirm above a form) one Escape closes just the top one, and a nested Radix layer's `preventDefault`ed Escape closes only that layer
|
|
74
74
|
- **Focus** — focus moves into the panel on open, is CONTAINED there while it is topmost (with bounded re-asserts for focus dropped by node removal), Tab cycles inside it, and focus returns to the opener on close. Portaled Radix layers (`[data-radix-popper-content-wrapper]`, `[role=listbox]`, `[role=menu]`) are exempt
|
|
75
|
+
- **No focus ring on the panel** — the panel is `tabIndex={-1}`, a focus TARGET rather than a control, so it carries `outline-none`. Opening by click hid the UA ring via `:focus-visible` heuristics, but a modal opened straight from a URL on page load has no preceding pointer event and the browser drew a ring around the whole dialog on every refresh. Controls inside keep their own rings
|
|
75
76
|
- **Backdrop click** — calls `onClose`
|
|
76
77
|
- **Two-column scroll model** — below `lg` there is one column and the outer region scrolls (identical to a single-column modal on a phone); from `lg` up the outer region is `overflow-hidden` and each column scrolls independently
|
|
77
78
|
- **Software keyboard** — the wrapper pads by `--of-keyboard-inset` rather than shrinking, since neither mobile shell shrinks the layout viewport when the IME opens
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<!-- source-hash:
|
|
2
|
-
A deprecated modal component system providing an accessible, composable modal dialog with scroll locking, keyboard handling, and overlay dismissal. All exports are superseded by `ModalV2` from `./modal-v2`.
|
|
1
|
+
<!-- source-hash: ec832e4f08292ae27c4a94e9d050afaf -->
|
|
2
|
+
A deprecated modal component system providing an accessible, composable modal dialog with scroll locking, keyboard handling, and overlay dismissal. All exports are superseded by `ModalV2` from `./modal-v2`. What remains here is the compact legacy dialog used by the confirm modals that have not moved yet.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -13,6 +13,15 @@ A deprecated modal component system providing an accessible, composable modal di
|
|
|
13
13
|
|
|
14
14
|
All components use `React.forwardRef` and accept an optional `className` for Tailwind overrides.
|
|
15
15
|
|
|
16
|
+
## Removed exports
|
|
17
|
+
|
|
18
|
+
There is no sizing or two-column layout here any more. `size` / `MODAL_SIZE_CLASSES` and the
|
|
19
|
+
`TWO_COLUMN_MODAL_BODY_CLASS` / `TWO_COLUMN_MODAL_GRID_CLASS` / `TWO_COLUMN_MODAL_COLUMN_CLASS`
|
|
20
|
+
class-string constants were removed once `ModalV2` gained a `size` prop and `ModalV2TwoColumn` —
|
|
21
|
+
the class strings were copied per call site and drifted, so a component owns the scroll model
|
|
22
|
+
instead. Width on this legacy dialog is a plain `className` override; anything that needs a size
|
|
23
|
+
variant or a two-column body belongs on `ModalV2`.
|
|
24
|
+
|
|
16
25
|
## Usage Example
|
|
17
26
|
|
|
18
27
|
> ⚠️ **Deprecated.** Use [`ModalV2`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/src/components/modal-v2.tsx) instead.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'react'
|
|
3
3
|
import { fn } from 'storybook/test'
|
|
4
4
|
import { Autocomplete, type AutocompleteOption } from '../components/ui/autocomplete'
|
|
5
5
|
import { Button } from '../components/ui/button'
|
|
@@ -12,6 +12,12 @@ const meta: ModalStoryMeta = {
|
|
|
12
12
|
component: Modal,
|
|
13
13
|
parameters: {
|
|
14
14
|
layout: 'centered',
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'Deprecated. The compact legacy dialog kept for the confirm modals that have not moved yet — new work uses `ModalV2` (UI/ModalV2), which owns the focus trap, Escape stacking, keyboard-inset handling, the `size` variants and the two-column body. Sizing and two-column layout are NOT available here: the `size` prop and the `TWO_COLUMN_MODAL_*` class-string constants this component used to export were removed once `ModalV2` gained `size` + `ModalV2TwoColumn`. Width is a plain `className` override.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
15
21
|
},
|
|
16
22
|
argTypes: {
|
|
17
23
|
isOpen: {
|
|
@@ -82,8 +88,9 @@ const autocompleteOptions: AutocompleteOption<string>[] = [
|
|
|
82
88
|
]
|
|
83
89
|
|
|
84
90
|
/**
|
|
85
|
-
* Modal with Autocomplete inside.
|
|
86
|
-
*
|
|
91
|
+
* Modal with Autocomplete inside. The Autocomplete dropdown renders inline in
|
|
92
|
+
* the modal's own DOM tree (no portal), so the panel needs `overflow-visible`
|
|
93
|
+
* for the list to escape the dialog box.
|
|
87
94
|
*/
|
|
88
95
|
export const WithAutocomplete: Story = {
|
|
89
96
|
args: {
|
|
@@ -94,11 +101,6 @@ export const WithAutocomplete: Story = {
|
|
|
94
101
|
render: function Render(args) {
|
|
95
102
|
const [isOpen, setIsOpen] = useState(args.isOpen)
|
|
96
103
|
const [selected, setSelected] = useState<string[]>(['enterprise'])
|
|
97
|
-
const [modalElement, setModalElement] = useState<HTMLDivElement | null>(null)
|
|
98
|
-
|
|
99
|
-
const modalRef = useCallback((node: HTMLDivElement | null) => {
|
|
100
|
-
setModalElement(node)
|
|
101
|
-
}, [])
|
|
102
104
|
|
|
103
105
|
return (
|
|
104
106
|
<>
|
|
@@ -106,7 +108,6 @@ export const WithAutocomplete: Story = {
|
|
|
106
108
|
Open Modal with Autocomplete
|
|
107
109
|
</Button>
|
|
108
110
|
<Modal
|
|
109
|
-
ref={modalRef}
|
|
110
111
|
isOpen={isOpen}
|
|
111
112
|
onClose={() => setIsOpen(false)}
|
|
112
113
|
className="max-w-lg overflow-visible"
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { Button } from '../components/ui/button';
|
|
5
|
+
import { Input } from '../components/ui/input';
|
|
6
|
+
import {
|
|
7
|
+
ModalV2,
|
|
8
|
+
ModalV2Content,
|
|
9
|
+
ModalV2Footer,
|
|
10
|
+
ModalV2Header,
|
|
11
|
+
ModalV2Title,
|
|
12
|
+
ModalV2TwoColumn,
|
|
13
|
+
} from '../components/ui/modal-v2';
|
|
14
|
+
import { Textarea } from '../components/ui/textarea';
|
|
15
|
+
|
|
16
|
+
type ModalV2StoryMeta = Meta<typeof ModalV2>;
|
|
17
|
+
|
|
18
|
+
const meta: ModalV2StoryMeta = {
|
|
19
|
+
title: 'UI/ModalV2',
|
|
20
|
+
component: ModalV2,
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: 'fullscreen',
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
'The current modal. `size` picks the panel width (`default` 28rem / `medium` 42rem / `wide` 1400px); `wide` also FIXES the desktop panel height so it stops jumping as content hydrates in. Use `ModalV2TwoColumn` instead of `ModalV2Content` for two-column editors — below `lg` it is one column with a single outer scroll, from `lg` up each column scrolls independently while header and footer stay pinned.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
decorators: [
|
|
31
|
+
Story => (
|
|
32
|
+
<div className="min-h-[100dvh] bg-ods-bg p-[var(--spacing-system-xl)]">
|
|
33
|
+
<Story />
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
argTypes: {
|
|
38
|
+
isOpen: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
description: 'Whether the modal is open',
|
|
41
|
+
},
|
|
42
|
+
size: {
|
|
43
|
+
control: 'radio',
|
|
44
|
+
options: ['default', 'medium', 'wide'],
|
|
45
|
+
description:
|
|
46
|
+
'Panel width. `default` = confirms, `medium` = single-column forms, `wide` = two-column editors (also fixes the desktop height)',
|
|
47
|
+
},
|
|
48
|
+
className: {
|
|
49
|
+
control: 'text',
|
|
50
|
+
description: 'Custom className for the modal panel',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default meta;
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
|
|
58
|
+
/* ------------------------------------------------------------------ *
|
|
59
|
+
* Story content — deliberately long so the scroll model is reviewable.
|
|
60
|
+
* ------------------------------------------------------------------ */
|
|
61
|
+
|
|
62
|
+
const VENDOR_FIELDS: { label: string; value: string }[] = [
|
|
63
|
+
{ label: 'Vendor name', value: 'Acme Endpoint Security' },
|
|
64
|
+
{ label: 'Slug', value: 'acme-endpoint-security' },
|
|
65
|
+
{ label: 'Website', value: 'https://acme-endpoint.example' },
|
|
66
|
+
{ label: 'Support email', value: 'support@acme-endpoint.example' },
|
|
67
|
+
{ label: 'Headquarters', value: 'Austin, TX' },
|
|
68
|
+
{ label: 'Founded', value: '2014' },
|
|
69
|
+
{ label: 'Employees', value: '240' },
|
|
70
|
+
{ label: 'Category', value: 'Endpoint protection' },
|
|
71
|
+
{ label: 'Sub-category', value: 'EDR' },
|
|
72
|
+
{ label: 'Deployment', value: 'Cloud, on-premise' },
|
|
73
|
+
{ label: 'Agent platforms', value: 'Windows, macOS, Linux' },
|
|
74
|
+
{ label: 'Compliance', value: 'SOC 2 Type II, ISO 27001' },
|
|
75
|
+
{ label: 'Integration partner', value: 'OpenFrame' },
|
|
76
|
+
{ label: 'Account manager', value: 'Dana Whitfield' },
|
|
77
|
+
{ label: 'Renewal date', value: '2026-11-30' },
|
|
78
|
+
{ label: 'Contract owner', value: 'Procurement' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const ACTIVITY_ROWS: { title: string; meta: string; body: string }[] = [
|
|
82
|
+
{
|
|
83
|
+
title: 'Pricing refreshed',
|
|
84
|
+
meta: 'Today · 09:12',
|
|
85
|
+
body: 'Per-device tier moved from $4.20 to $4.60. Annual commitment unchanged.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: 'Feature matrix synced',
|
|
89
|
+
meta: 'Yesterday · 17:40',
|
|
90
|
+
body: 'Ransomware rollback and USB device control marked as generally available.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: 'Case study published',
|
|
94
|
+
meta: '3 days ago',
|
|
95
|
+
body: 'Regional MSP consolidated three agents onto one console after migration.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: 'Security review passed',
|
|
99
|
+
meta: '6 days ago',
|
|
100
|
+
body: 'SOC 2 Type II report re-issued; no exceptions carried forward.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: 'Support SLA updated',
|
|
104
|
+
meta: '2 weeks ago',
|
|
105
|
+
body: 'P1 response tightened to 30 minutes for partners on the managed plan.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
title: 'Partner tier changed',
|
|
109
|
+
meta: '3 weeks ago',
|
|
110
|
+
body: 'Promoted to Gold. Deal registration discount now applies automatically.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: 'Console redesign shipped',
|
|
114
|
+
meta: '1 month ago',
|
|
115
|
+
body: 'Alert triage moved into a single queue with saved views per technician.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
title: 'API v3 announced',
|
|
119
|
+
meta: '1 month ago',
|
|
120
|
+
body: 'Webhook payloads gain device tags; v2 stays supported through next year.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: 'Outage postmortem',
|
|
124
|
+
meta: '2 months ago',
|
|
125
|
+
body: 'Ingest backlog cleared in 90 minutes; regional failover added since.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
title: 'Onboarding docs rewritten',
|
|
129
|
+
meta: '2 months ago',
|
|
130
|
+
body: 'Silent-install flags documented for every supported RMM deployment tool.',
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
function VendorFields({ count = VENDOR_FIELDS.length }: { count?: number }) {
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<p className="text-h5 text-ods-text-secondary">Vendor profile</p>
|
|
138
|
+
{VENDOR_FIELDS.slice(0, count).map(field => (
|
|
139
|
+
<Input key={field.label} label={field.label} defaultValue={field.value} className="w-full" />
|
|
140
|
+
))}
|
|
141
|
+
<Textarea
|
|
142
|
+
label="Internal notes"
|
|
143
|
+
rows={4}
|
|
144
|
+
defaultValue="Renewal negotiated alongside the backup vendor. Keep both contracts on the same anniversary so the bundle discount survives."
|
|
145
|
+
/>
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ActivityRows({ count = ACTIVITY_ROWS.length }: { count?: number }) {
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
<p className="text-h5 text-ods-text-secondary">Recent activity</p>
|
|
154
|
+
{ACTIVITY_ROWS.slice(0, count).map(row => (
|
|
155
|
+
<div key={row.title} className="rounded-md border border-ods-border p-[var(--spacing-system-mf)]">
|
|
156
|
+
<div className="flex items-baseline justify-between gap-[var(--spacing-system-xsf)]">
|
|
157
|
+
<span className="text-h3 text-ods-text-primary">{row.title}</span>
|
|
158
|
+
<span className="text-h6 text-ods-text-tertiary">{row.meta}</span>
|
|
159
|
+
</div>
|
|
160
|
+
<p className="mt-[var(--spacing-system-xs)] text-h6 text-ods-text-secondary">{row.body}</p>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `size="default"` (28rem) — the confirm-dialog width. Header, one short
|
|
169
|
+
* paragraph, footer.
|
|
170
|
+
*/
|
|
171
|
+
export const Default: Story = {
|
|
172
|
+
args: {
|
|
173
|
+
isOpen: true,
|
|
174
|
+
onClose: fn(),
|
|
175
|
+
size: 'default',
|
|
176
|
+
children: null,
|
|
177
|
+
},
|
|
178
|
+
render: function Render(args) {
|
|
179
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
184
|
+
Open default modal
|
|
185
|
+
</Button>
|
|
186
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
187
|
+
<ModalV2Header>
|
|
188
|
+
<ModalV2Title>Archive vendor</ModalV2Title>
|
|
189
|
+
</ModalV2Header>
|
|
190
|
+
<ModalV2Content>
|
|
191
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
192
|
+
Acme Endpoint Security will stop appearing in comparisons and search. Existing references keep working and
|
|
193
|
+
you can restore it at any time.
|
|
194
|
+
</p>
|
|
195
|
+
</ModalV2Content>
|
|
196
|
+
<ModalV2Footer className="justify-end">
|
|
197
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
198
|
+
Cancel
|
|
199
|
+
</Button>
|
|
200
|
+
<Button variant="destructive" onClick={() => setIsOpen(false)}>
|
|
201
|
+
Archive
|
|
202
|
+
</Button>
|
|
203
|
+
</ModalV2Footer>
|
|
204
|
+
</ModalV2>
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* `size="medium"` (42rem) — the single-column form width. The body is a plain
|
|
212
|
+
* `ModalV2Content`, so header and footer stay pinned while it scrolls.
|
|
213
|
+
*/
|
|
214
|
+
export const Medium: Story = {
|
|
215
|
+
args: {
|
|
216
|
+
isOpen: true,
|
|
217
|
+
onClose: fn(),
|
|
218
|
+
size: 'medium',
|
|
219
|
+
children: null,
|
|
220
|
+
},
|
|
221
|
+
render: function Render(args) {
|
|
222
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<>
|
|
226
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
227
|
+
Open medium modal
|
|
228
|
+
</Button>
|
|
229
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
230
|
+
<ModalV2Header>
|
|
231
|
+
<ModalV2Title>Edit vendor</ModalV2Title>
|
|
232
|
+
</ModalV2Header>
|
|
233
|
+
<ModalV2Content className="space-y-[var(--spacing-system-lf)] pr-[var(--spacing-system-sf)]">
|
|
234
|
+
<VendorFields count={8} />
|
|
235
|
+
</ModalV2Content>
|
|
236
|
+
<ModalV2Footer className="justify-end">
|
|
237
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
238
|
+
Cancel
|
|
239
|
+
</Button>
|
|
240
|
+
<Button onClick={() => setIsOpen(false)}>Save</Button>
|
|
241
|
+
</ModalV2Footer>
|
|
242
|
+
</ModalV2>
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `size="wide"` (1400px) with a single-column body and deliberately sparse
|
|
250
|
+
* content: on desktop the panel keeps its full `min(90dvh,100%-2rem)` height
|
|
251
|
+
* instead of hugging the content, which is what stops it jumping as an entity
|
|
252
|
+
* hydrates in. Mobile still gets a content-sized bottom sheet.
|
|
253
|
+
*/
|
|
254
|
+
export const Wide: Story = {
|
|
255
|
+
args: {
|
|
256
|
+
isOpen: true,
|
|
257
|
+
onClose: fn(),
|
|
258
|
+
size: 'wide',
|
|
259
|
+
children: null,
|
|
260
|
+
},
|
|
261
|
+
render: function Render(args) {
|
|
262
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
267
|
+
Open wide modal
|
|
268
|
+
</Button>
|
|
269
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
270
|
+
<ModalV2Header>
|
|
271
|
+
<ModalV2Title>Vendor loading</ModalV2Title>
|
|
272
|
+
</ModalV2Header>
|
|
273
|
+
<ModalV2Content>
|
|
274
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
275
|
+
Two fields of content in a panel that keeps its full height. Compare with the medium story above, which
|
|
276
|
+
shrinks to fit its content.
|
|
277
|
+
</p>
|
|
278
|
+
<div className="mt-[var(--spacing-system-lf)] space-y-[var(--spacing-system-lf)]">
|
|
279
|
+
<Input label="Vendor name" defaultValue="Acme Endpoint Security" className="w-full" />
|
|
280
|
+
<Input label="Slug" defaultValue="acme-endpoint-security" className="w-full" />
|
|
281
|
+
</div>
|
|
282
|
+
</ModalV2Content>
|
|
283
|
+
<ModalV2Footer className="justify-end">
|
|
284
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
285
|
+
Cancel
|
|
286
|
+
</Button>
|
|
287
|
+
<Button onClick={() => setIsOpen(false)}>Save</Button>
|
|
288
|
+
</ModalV2Footer>
|
|
289
|
+
</ModalV2>
|
|
290
|
+
</>
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The two-column editor: `size="wide"` + `ModalV2TwoColumn`. Both columns
|
|
297
|
+
* overflow, and by different amounts — scroll the left one to the bottom and
|
|
298
|
+
* the right one does not move. Narrow the viewport below `lg` and the two
|
|
299
|
+
* collapse into a single column with one outer scrollbar.
|
|
300
|
+
*/
|
|
301
|
+
export const WideTwoColumn: Story = {
|
|
302
|
+
args: {
|
|
303
|
+
isOpen: true,
|
|
304
|
+
onClose: fn(),
|
|
305
|
+
size: 'wide',
|
|
306
|
+
children: null,
|
|
307
|
+
},
|
|
308
|
+
render: function Render(args) {
|
|
309
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<>
|
|
313
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
314
|
+
Open two-column editor
|
|
315
|
+
</Button>
|
|
316
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
317
|
+
<ModalV2Header>
|
|
318
|
+
<ModalV2Title>Acme Endpoint Security</ModalV2Title>
|
|
319
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
320
|
+
16 fields on the left, 10 activity entries on the right. Each side scrolls on its own at desktop widths.
|
|
321
|
+
</p>
|
|
322
|
+
</ModalV2Header>
|
|
323
|
+
<ModalV2TwoColumn left={<VendorFields />} right={<ActivityRows />} />
|
|
324
|
+
<ModalV2Footer className="justify-end">
|
|
325
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
326
|
+
Cancel
|
|
327
|
+
</Button>
|
|
328
|
+
<Button onClick={() => setIsOpen(false)}>Save</Button>
|
|
329
|
+
</ModalV2Footer>
|
|
330
|
+
</ModalV2>
|
|
331
|
+
</>
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The regression case for the row sizing. The left column is very long and the
|
|
338
|
+
* right one holds two entries: with an implicit `auto` grid row the row would
|
|
339
|
+
* size to the TALLER column, the columns would get no bounded height to scroll
|
|
340
|
+
* inside, and the outer `overflow-hidden` would clip the bottom of the left
|
|
341
|
+
* column instead. `lg:grid-rows-[minmax(0,1fr)]` is what keeps the left column
|
|
342
|
+
* scrollable and the right one short.
|
|
343
|
+
*/
|
|
344
|
+
export const TwoColumnUnequalColumns: Story = {
|
|
345
|
+
args: {
|
|
346
|
+
isOpen: true,
|
|
347
|
+
onClose: fn(),
|
|
348
|
+
size: 'wide',
|
|
349
|
+
children: null,
|
|
350
|
+
},
|
|
351
|
+
render: function Render(args) {
|
|
352
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<>
|
|
356
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
357
|
+
Open lopsided two-column editor
|
|
358
|
+
</Button>
|
|
359
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
360
|
+
<ModalV2Header>
|
|
361
|
+
<ModalV2Title>Lopsided columns</ModalV2Title>
|
|
362
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
363
|
+
Scroll the left column to its last field — the panel does not grow and nothing is clipped.
|
|
364
|
+
</p>
|
|
365
|
+
</ModalV2Header>
|
|
366
|
+
<ModalV2TwoColumn left={<VendorFields />} right={<ActivityRows count={2} />} />
|
|
367
|
+
<ModalV2Footer className="justify-end">
|
|
368
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
369
|
+
Cancel
|
|
370
|
+
</Button>
|
|
371
|
+
<Button onClick={() => setIsOpen(false)}>Save</Button>
|
|
372
|
+
</ModalV2Footer>
|
|
373
|
+
</ModalV2>
|
|
374
|
+
</>
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Header and footer live outside the scrolling region, so both stay pinned while
|
|
381
|
+
* the columns move under them. Both are given a divider here to make the seam
|
|
382
|
+
* between the pinned chrome and the scrolling body obvious during review.
|
|
383
|
+
*/
|
|
384
|
+
export const PinnedHeaderAndFooter: Story = {
|
|
385
|
+
args: {
|
|
386
|
+
isOpen: true,
|
|
387
|
+
onClose: fn(),
|
|
388
|
+
size: 'wide',
|
|
389
|
+
children: null,
|
|
390
|
+
},
|
|
391
|
+
render: function Render(args) {
|
|
392
|
+
const [isOpen, setIsOpen] = useState(args.isOpen);
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<>
|
|
396
|
+
<Button onClick={() => setIsOpen(true)} variant="outline">
|
|
397
|
+
Open pinned header/footer editor
|
|
398
|
+
</Button>
|
|
399
|
+
<ModalV2 isOpen={isOpen} onClose={() => setIsOpen(false)} size={args.size}>
|
|
400
|
+
<ModalV2Header className="border-b border-ods-border pb-[var(--spacing-system-mf)]">
|
|
401
|
+
<ModalV2Title>Pinned chrome</ModalV2Title>
|
|
402
|
+
<p className="text-h6 text-ods-text-secondary">
|
|
403
|
+
Scroll either column to its end — this header and the footer below stay put, and the close button stays
|
|
404
|
+
reachable the whole time.
|
|
405
|
+
</p>
|
|
406
|
+
</ModalV2Header>
|
|
407
|
+
<ModalV2TwoColumn left={<VendorFields />} right={<ActivityRows count={6} />} />
|
|
408
|
+
<ModalV2Footer className="items-center justify-between border-t border-ods-border pt-[var(--spacing-system-mf)]">
|
|
409
|
+
<span className="text-h6 text-ods-text-secondary">Last saved 4 minutes ago</span>
|
|
410
|
+
<div className="flex gap-[var(--spacing-system-mf)]">
|
|
411
|
+
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
|
412
|
+
Cancel
|
|
413
|
+
</Button>
|
|
414
|
+
<Button onClick={() => setIsOpen(false)}>Save</Button>
|
|
415
|
+
</div>
|
|
416
|
+
</ModalV2Footer>
|
|
417
|
+
</ModalV2>
|
|
418
|
+
</>
|
|
419
|
+
);
|
|
420
|
+
},
|
|
421
|
+
};
|