@podoba/react 0.0.34 → 0.0.36
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 +3 -3
- package/src/components/asset-masonry-grid.examples.tsx +9 -0
- package/src/components/asset-masonry-grid.tsx +62 -0
- package/src/components/asset-selection-surface.examples.tsx +33 -0
- package/src/components/asset-selection-surface.tsx +184 -0
- package/src/components/brand-page-header.tsx +60 -29
- package/src/components/button.tsx +27 -4
- package/src/components/combobox.tsx +1 -1
- package/src/components/compact-action-button.examples.tsx +7 -0
- package/src/components/compact-action-button.tsx +21 -0
- package/src/components/compact-settings-dialog.examples.tsx +15 -0
- package/src/components/compact-settings-dialog.tsx +50 -0
- package/src/components/context-action-glyph.examples.tsx +9 -0
- package/src/components/context-action-glyph.tsx +61 -0
- package/src/components/context-menu.tsx +191 -107
- package/src/components/context-search-panel.examples.tsx +9 -0
- package/src/components/context-search-panel.tsx +50 -0
- package/src/components/csv-binding-presentation.examples.tsx +14 -0
- package/src/components/csv-binding-presentation.tsx +40 -0
- package/src/components/dashboard-grid.tsx +1 -1
- package/src/components/date-picker.tsx +5 -3
- package/src/components/date-selection-calendar.examples.tsx +8 -0
- package/src/components/date-selection-calendar.tsx +47 -0
- package/src/components/delivery/send-to-print-modal.tsx +351 -84
- package/src/components/dialog-action-button.examples.tsx +7 -0
- package/src/components/dialog-action-button.tsx +22 -0
- package/src/components/dialog.tsx +32 -12
- package/src/components/document-upload-panel.examples.tsx +12 -0
- package/src/components/document-upload-panel.tsx +48 -0
- package/src/components/dropdown-menu.tsx +5 -3
- package/src/components/empty-panel-action.examples.tsx +7 -0
- package/src/components/empty-panel-action.tsx +8 -0
- package/src/components/field-appearance.ts +14 -0
- package/src/components/focus-field.tsx +3 -3
- package/src/components/icons.tsx +30 -0
- package/src/components/input.examples.tsx +8 -0
- package/src/components/input.tsx +13 -5
- package/src/components/media-asset-panel.examples.tsx +11 -0
- package/src/components/media-asset-panel.tsx +58 -0
- package/src/components/media-gallery.examples.tsx +12 -0
- package/src/components/media-gallery.tsx +53 -0
- package/src/components/media-settings-dialog.examples.tsx +21 -0
- package/src/components/media-settings-dialog.tsx +83 -0
- package/src/components/preview-info-card.examples.tsx +18 -0
- package/src/components/preview-info-card.tsx +27 -0
- package/src/components/reload-icon.examples.tsx +7 -0
- package/src/components/reload-icon.tsx +6 -0
- package/src/components/request-changes-modal.examples.tsx +26 -0
- package/src/components/request-changes-modal.tsx +36 -19
- package/src/components/rich-text-editor.tsx +1 -1
- package/src/components/select.examples.tsx +17 -0
- package/src/components/select.tsx +78 -22
- package/src/components/settings-dialog-surface.examples.tsx +44 -0
- package/src/components/settings-dialog-surface.tsx +240 -0
- package/src/components/subtle.tsx +64 -8
- package/src/components/table.examples.tsx +9 -0
- package/src/components/table.tsx +29 -12
- package/src/components/template-catalog-card.examples.tsx +25 -0
- package/src/components/template-catalog-card.tsx +179 -0
- package/src/components/text.tsx +14 -1
- package/src/components/textarea.examples.tsx +8 -0
- package/src/components/textarea.tsx +16 -6
- package/src/components/tile.tsx +16 -17
- package/src/editor/block-editor.tsx +56 -14
- package/src/index.ts +19 -0
- package/src/layout/app-shell.tsx +13 -9
- package/src/layout/topbar.tsx +9 -12
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DialogActionButton } from './dialog-action-button'
|
|
2
|
+
export const examples = {
|
|
3
|
+
default: () => <DialogActionButton>Review Mapping</DialogActionButton>,
|
|
4
|
+
variants: () => <><DialogActionButton variant="secondary">Change CSV</DialogActionButton><DialogActionButton>Bind CSV</DialogActionButton></>,
|
|
5
|
+
states: () => <><DialogActionButton isDisabled>Bind CSV</DialogActionButton><DialogActionButton isPending>Binding CSV…</DialogActionButton></>,
|
|
6
|
+
}
|
|
7
|
+
export const meta = { category: 'Primitives', description: 'Source UI dialog action palette, typography and pointer/keyboard states, independent of dashboard CTAs.' }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
2
|
+
import { uic } from '../utils/uic'
|
|
3
|
+
|
|
4
|
+
export const DIALOG_ACTION_VARIANTS = ['primary', 'secondary'] as const
|
|
5
|
+
export type DialogActionVariant = (typeof DIALOG_ACTION_VARIANTS)[number]
|
|
6
|
+
|
|
7
|
+
/** Source UI dialog action, distinct from the frontend dashboard CTA Button.
|
|
8
|
+
* Shares Button's semantic palette (brand-primary / surface-card) so the two never
|
|
9
|
+
* drift apart; only the geometry — pill radius, 24/12 padding — is specific here.
|
|
10
|
+
* Keyboard focus retains the shared RAC treatment.
|
|
11
|
+
*/
|
|
12
|
+
export const DialogActionButton = uic(AriaButton, {
|
|
13
|
+
displayName: 'DialogActionButton',
|
|
14
|
+
baseClass: 'inline-flex items-center justify-center rounded-full px-6 py-3 text-body font-medium leading-5 cursor-pointer transition-colors duration-200 motion-reduce:transition-none outline-none ' +
|
|
15
|
+
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2 ' +
|
|
16
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[pending]:cursor-progress',
|
|
17
|
+
variants: { variant: {
|
|
18
|
+
primary: 'border-0 bg-brand-primary text-fg-inverted data-[hovered]:bg-neutral-600 data-[pressed]:bg-brand-primary',
|
|
19
|
+
secondary: 'border border-border bg-surface-card text-fg data-[hovered]:bg-surface-muted data-[hovered]:border-border-muted data-[pressed]:bg-surface-muted',
|
|
20
|
+
} },
|
|
21
|
+
defaultVariants: { variant: 'primary' },
|
|
22
|
+
})
|
|
@@ -32,7 +32,7 @@ export const ModalOverlay = uic(RACModalOverlay, {
|
|
|
32
32
|
export const ModalSurface = uic(RACModal, {
|
|
33
33
|
displayName: 'DialogModal',
|
|
34
34
|
baseClass:
|
|
35
|
-
'rounded-lg bg-surface p-5 outline-none shadow-modal-surface ' +
|
|
35
|
+
'relative rounded-lg bg-surface p-5 outline-none shadow-modal-surface ' +
|
|
36
36
|
'data-[entering]:animate-modal-surface-in data-[exiting]:animate-modal-surface-out',
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -92,6 +92,10 @@ export type DialogProps = RACDialogProps & {
|
|
|
92
92
|
badge?: ReactNode
|
|
93
93
|
/** Width preset — `md` (default) for forms; `sm` for tight confirms; `lg`/`xl` for wide stepper content. */
|
|
94
94
|
size?: DialogSize
|
|
95
|
+
/** Optional form-specific width cap; the viewport width preset still applies. */
|
|
96
|
+
maxWidth?: number
|
|
97
|
+
/** Responsive intrinsic detail envelope, independent of title sizing. */
|
|
98
|
+
widthPreset?: 'responsive-detail' | 'custom-task-detail'
|
|
95
99
|
children: ReactNode | ((opts: { close: () => void }) => ReactNode)
|
|
96
100
|
/**
|
|
97
101
|
* Controlled open state. Provide together with `onOpenChange` to drive the
|
|
@@ -115,6 +119,8 @@ export const Dialog = ({
|
|
|
115
119
|
badge,
|
|
116
120
|
// gs DialogContent defaults to `md` (586px form width).
|
|
117
121
|
size = 'md',
|
|
122
|
+
maxWidth,
|
|
123
|
+
widthPreset,
|
|
118
124
|
children,
|
|
119
125
|
isOpen,
|
|
120
126
|
defaultOpen,
|
|
@@ -126,6 +132,11 @@ export const Dialog = ({
|
|
|
126
132
|
// `full` is a fixed-height flex canvas: the body fills and scrolls internally so
|
|
127
133
|
// the header (+ a footer the content pins) stay put. Other sizes are content-height.
|
|
128
134
|
const isFlex = size === 'full'
|
|
135
|
+
// The Manager header treatment — block layout, floated close, label-ramp
|
|
136
|
+
// description — belongs to the OPT-IN width presets, not to "any dialog that
|
|
137
|
+
// isn't `full`". Gating it on `isFlex` made every sm/md/lg/xl dialog render a
|
|
138
|
+
// display-scale title and stranded `TITLE_CLASS` on the single `full` size.
|
|
139
|
+
const isCompact = widthPreset !== undefined
|
|
129
140
|
return (
|
|
130
141
|
// `isOpen`/`defaultOpen`/`onOpenChange` go to the RAC ModalOverlay: when
|
|
131
142
|
// provided the dialog is controlled (renders independently of a DialogTrigger);
|
|
@@ -142,19 +153,28 @@ export const Dialog = ({
|
|
|
142
153
|
onOpenChange={onOpenChange}
|
|
143
154
|
isDismissable={isDismissable}
|
|
144
155
|
>
|
|
145
|
-
<ModalSurface
|
|
156
|
+
<ModalSurface
|
|
157
|
+
className={
|
|
158
|
+
widthPreset === 'responsive-detail'
|
|
159
|
+
? 'w-[90vw] max-w-125 md:max-w-150 lg:max-w-160 xl:max-w-180 max-h-[85vh] overflow-y-auto'
|
|
160
|
+
: widthPreset === 'custom-task-detail'
|
|
161
|
+
? 'w-[90vw] min-w-[400px] max-w-[500px] md:max-w-[600px] lg:max-w-[640px] xl:max-w-[720px] max-h-[85vh] overflow-y-auto max-[460px]:min-w-0'
|
|
162
|
+
: SIZE_CLASS[size]
|
|
163
|
+
}
|
|
164
|
+
style={maxWidth === undefined ? undefined : { maxWidth }}
|
|
165
|
+
>
|
|
146
166
|
<ModalDialog
|
|
147
167
|
{...props}
|
|
148
168
|
className={isFlex ? 'flex min-h-0 flex-1 flex-col outline-none' : 'outline-none'}
|
|
149
169
|
>
|
|
150
170
|
{(renderProps) => (
|
|
151
171
|
<>
|
|
152
|
-
<div className=
|
|
172
|
+
<div className={isCompact ? 'block' : 'mb-4 flex shrink-0 items-start justify-between gap-4'}>
|
|
153
173
|
{title || description ? (
|
|
154
174
|
<div className="min-w-0">
|
|
155
175
|
{title ? (
|
|
156
176
|
<div className="flex items-center gap-2">
|
|
157
|
-
<Heading slot="title" className={`${TITLE_CLASS[size]} text-fg`}>
|
|
177
|
+
<Heading slot="title" className={isCompact ? 'm-0 mb-4 max-w-5/6 text-heading1 font-medium tracking-normal text-fg' : `${TITLE_CLASS[size]} text-fg`}>
|
|
158
178
|
{title}
|
|
159
179
|
</Heading>
|
|
160
180
|
{badge ? (
|
|
@@ -165,8 +185,8 @@ export const Dialog = ({
|
|
|
165
185
|
</div>
|
|
166
186
|
) : null}
|
|
167
187
|
{description ? (
|
|
168
|
-
//
|
|
169
|
-
<p className=
|
|
188
|
+
// Source ordinary descriptions use the 14px/18px label ramp.
|
|
189
|
+
<p className={isCompact ? 'm-0 mb-6 max-w-5/6 text-small leading-4.5 font-normal text-fg-workflow-muted' : 'mt-1 text-body text-fg'}>{description}</p>
|
|
170
190
|
) : null}
|
|
171
191
|
</div>
|
|
172
192
|
) : (
|
|
@@ -179,19 +199,19 @@ export const Dialog = ({
|
|
|
179
199
|
// gs close: 32px square (`h-8 w-8`), medium radius (`rounded-md`),
|
|
180
200
|
// tertiary icon color (`text-fg-subtle`) → primary (`text-fg`) +
|
|
181
201
|
// subtle hover bg on hover. Smooth color transition.
|
|
182
|
-
className=
|
|
202
|
+
className={`${isCompact ? 'absolute top-[calc(var(--spacing)*3.375)] end-4 max-md:end-3' : '-mr-1 -mt-1'} inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-fg-subtle outline-none transition-colors hover:bg-surface-muted hover:text-fg focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none`}
|
|
183
203
|
>
|
|
184
204
|
<svg
|
|
185
|
-
width=
|
|
186
|
-
height=
|
|
187
|
-
viewBox=
|
|
205
|
+
width={isCompact ? 15 : 18}
|
|
206
|
+
height={isCompact ? 15 : 18}
|
|
207
|
+
viewBox={isCompact ? '0 0 15 15' : '0 0 24 24'}
|
|
188
208
|
fill="none"
|
|
189
|
-
stroke=
|
|
209
|
+
stroke={isCompact ? undefined : 'currentColor'}
|
|
190
210
|
strokeWidth="2"
|
|
191
211
|
strokeLinecap="round"
|
|
192
212
|
aria-hidden="true"
|
|
193
213
|
>
|
|
194
|
-
<path d=
|
|
214
|
+
<path fill={isCompact ? 'currentColor' : undefined} d={isCompact ? 'M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z' : 'M6 6l12 12M18 6 6 18'} />
|
|
195
215
|
</svg>
|
|
196
216
|
</button>
|
|
197
217
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DocumentUploadPanel } from './document-upload-panel'
|
|
2
|
+
|
|
3
|
+
const copy = { title: 'CSV file', actionLabel: 'Select CSV', helperText: 'No CSV file selected yet.\nSelect the CSV file for content mapping.' }
|
|
4
|
+
export const examples = {
|
|
5
|
+
default: () => <DocumentUploadPanel {...copy} />,
|
|
6
|
+
dragging: () => <DocumentUploadPanel {...copy} isDragging />,
|
|
7
|
+
disabled: () => <DocumentUploadPanel {...copy} isDisabled />,
|
|
8
|
+
}
|
|
9
|
+
export const meta = {
|
|
10
|
+
category: 'Form',
|
|
11
|
+
description: 'Upload tile with a centred keyboard-accessible action and a native drop surface. Focus/hover use the shared React Aria button; caller owns file validation.',
|
|
12
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DragEventHandler, ReactNode } from 'react'
|
|
2
|
+
import { uic } from '../utils/uic'
|
|
3
|
+
import { DialogActionButton } from './dialog-action-button'
|
|
4
|
+
import { Tile } from './tile'
|
|
5
|
+
|
|
6
|
+
const Surface = uic('div', {
|
|
7
|
+
displayName: 'DocumentUploadSurface',
|
|
8
|
+
baseClass: 'relative h-95 w-full',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/** Source upload tile: label at top, action centred in the WHOLE tile, helper at bottom.
|
|
12
|
+
* File ownership, validation and persistence belong to the caller. The real button
|
|
13
|
+
* provides the keyboard alternative to native file drag/drop.
|
|
14
|
+
*/
|
|
15
|
+
export function DocumentUploadPanel({
|
|
16
|
+
title, actionLabel, helperText, isDragging = false, isDisabled = false,
|
|
17
|
+
onAction, onDrop, onDragEnter, onDragOver, onDragLeave,
|
|
18
|
+
...rest
|
|
19
|
+
}: {
|
|
20
|
+
title: ReactNode
|
|
21
|
+
actionLabel: string
|
|
22
|
+
helperText?: string
|
|
23
|
+
isDragging?: boolean
|
|
24
|
+
isDisabled?: boolean
|
|
25
|
+
onAction?: () => void
|
|
26
|
+
onDrop?: DragEventHandler<HTMLDivElement>
|
|
27
|
+
onDragEnter?: DragEventHandler<HTMLDivElement>
|
|
28
|
+
onDragOver?: DragEventHandler<HTMLDivElement>
|
|
29
|
+
onDragLeave?: DragEventHandler<HTMLDivElement>
|
|
30
|
+
'data-testid'?: string
|
|
31
|
+
}) {
|
|
32
|
+
return <Surface {...rest} onDrop={onDrop} onDragEnter={onDragEnter} onDragOver={onDragOver} onDragLeave={onDragLeave}>
|
|
33
|
+
<Tile theme="light" className={isDragging && !isDisabled ? 'ring-2 ring-inset ring-fg-subtle' : undefined}
|
|
34
|
+
eyebrow={<span className="relative z-10 mb-2 text-body font-medium leading-5">{title}</span>}
|
|
35
|
+
footer={helperText ? <p className="relative z-10 m-0 whitespace-pre-line text-small font-normal text-fg-workflow-muted" style={{ lineHeight: 'normal' }}>
|
|
36
|
+
{/* Original Tile's `.tile .tail *` wins over the upload helper classes:
|
|
37
|
+
BOTH lines inherit the regular muted supporting text. */}
|
|
38
|
+
{helperText}
|
|
39
|
+
</p> : undefined}
|
|
40
|
+
/>
|
|
41
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
42
|
+
<DialogActionButton type="button" isDisabled={isDisabled} onPress={onAction}
|
|
43
|
+
className="pointer-events-auto h-14 w-48 min-w-48 py-0">
|
|
44
|
+
{actionLabel}
|
|
45
|
+
</DialogActionButton>
|
|
46
|
+
</div>
|
|
47
|
+
</Surface>
|
|
48
|
+
}
|
|
@@ -43,7 +43,8 @@ import { uic } from '../utils/uic'
|
|
|
43
43
|
* tokens): content bg white → `surface` · 1px `#eceae1` border → `border` · 8px
|
|
44
44
|
* radius → `rounded-lg` · `shadow-lg` · item 12/16px padding → `py-3 px-4` · 6px
|
|
45
45
|
* item radius → `rounded-md` · item hover `#f7f6f2` → `surface-card` · item text
|
|
46
|
-
* `#0d0d0d` → `fg` · section label `#aba89c` 13px → `text-fg-
|
|
46
|
+
* `#0d0d0d` → `fg` · section label `#aba89c` 13px → `text-fg-muted text-compact`
|
|
47
|
+
* (#25: the source grey is 2.10:1 on `surface`; a section label is read, not ornament)
|
|
47
48
|
* · separator `#eceae1` → `border`. The destructive item uses `text-danger`
|
|
48
49
|
* (`#dc2626`) — a light-surface red that reads correctly here (unlike the dark
|
|
49
50
|
* ContextMenu, which needs the lighter `#ffb5b5`).
|
|
@@ -82,7 +83,8 @@ export const DropdownMenuSeparator = uic(RACSeparator, {
|
|
|
82
83
|
export interface DropdownMenuSectionProps<T extends object> extends Omit<RACMenuSectionProps<T>, 'children'> {
|
|
83
84
|
/**
|
|
84
85
|
* Section label (gs `.label`). Rendered as a non-interactive `Header` — 13px
|
|
85
|
-
* medium, `fg-
|
|
86
|
+
* medium, `fg-muted` — above the section's items. (The source grey `fg-subtle`
|
|
87
|
+
* is 2.10:1 on `surface`; a section label is read, not ornament — #25.)
|
|
86
88
|
*/
|
|
87
89
|
label?: ReactNode
|
|
88
90
|
/** Static section items (each a {@link DropdownMenuItem}). */
|
|
@@ -98,7 +100,7 @@ export function DropdownMenuSection<T extends object>({
|
|
|
98
100
|
return (
|
|
99
101
|
<RACMenuSection {...props}>
|
|
100
102
|
{label ? (
|
|
101
|
-
<Header className="px-4 py-3 text-compact font-medium text-fg-
|
|
103
|
+
<Header className="px-4 py-3 text-compact font-medium text-fg-muted">{label}</Header>
|
|
102
104
|
) : null}
|
|
103
105
|
{children}
|
|
104
106
|
</RACMenuSection>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EmptyPanelAction } from './empty-panel-action'
|
|
2
|
+
export const examples = {
|
|
3
|
+
default: () => <EmptyPanelAction>Add deadline</EmptyPanelAction>,
|
|
4
|
+
variants: () => <EmptyPanelAction>Select assets</EmptyPanelAction>,
|
|
5
|
+
states: () => <><EmptyPanelAction isDisabled>Add deadline</EmptyPanelAction><EmptyPanelAction isPending>Loading</EmptyPanelAction></>,
|
|
6
|
+
}
|
|
7
|
+
export const meta = { category: 'Composite', description: 'Source dashboard empty action: 56px height, 192px minimum width and 16/20 medium text. Uses RAC focus, disabled and pending states.' }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { uic } from '../utils/uic'
|
|
2
|
+
import { DialogActionButton } from './dialog-action-button'
|
|
3
|
+
|
|
4
|
+
/** Centered action for an empty dashboard panel; keeps the source UI action palette. */
|
|
5
|
+
export const EmptyPanelAction = uic(DialogActionButton, {
|
|
6
|
+
displayName: 'EmptyPanelAction',
|
|
7
|
+
baseClass: 'h-14 min-w-48 px-6 py-0 text-body font-medium leading-5',
|
|
8
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const FIELD_APPEARANCES = ['outlined', 'filled'] as const
|
|
2
|
+
export type FieldAppearance = (typeof FIELD_APPEARANCES)[number]
|
|
3
|
+
|
|
4
|
+
/** Borderless Manager field skin. Hover yields to focus; focus uses an offset
|
|
5
|
+
* outline instead of combining the browser outline with a second inset ring.
|
|
6
|
+
* Placeholder/error colors retain the library's accessible semantic tokens.
|
|
7
|
+
*/
|
|
8
|
+
export const filledFieldClasses =
|
|
9
|
+
'border-0 bg-surface-card text-small font-normal leading-4.5 text-fg ' +
|
|
10
|
+
'outline-none transition-colors duration-200 motion-reduce:transition-none placeholder:text-fg-muted placeholder:font-normal ' +
|
|
11
|
+
'data-[hovered]:bg-surface-muted data-[focused]:bg-surface-card ' +
|
|
12
|
+
'focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 ' +
|
|
13
|
+
'data-[invalid]:ring-1 data-[invalid]:ring-danger data-[invalid]:outline-danger ' +
|
|
14
|
+
'data-[disabled]:bg-surface-muted data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed'
|
|
@@ -85,8 +85,8 @@ export function FocusFields({ children, className }: { children: ReactNode; clas
|
|
|
85
85
|
|
|
86
86
|
// In the overlay, enlarge and de-chrome text controls into a bare headline editor.
|
|
87
87
|
const OVERLAY_EDITOR =
|
|
88
|
-
'[&_input]:w-full [&_input]:border-0 [&_input]:bg-transparent [&_input]:p-0 [&_input]:text-display [&_input]:font-medium [&_input]:text-fg [&_input]:outline-none [&_input]:shadow-none [&_input]:ring-0 [&_input]:placeholder:text-fg-
|
|
89
|
-
'[&_textarea]:min-h-40 [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:border-0 [&_textarea]:bg-transparent [&_textarea]:p-0 [&_textarea]:text-display [&_textarea]:font-medium [&_textarea]:text-fg [&_textarea]:outline-none [&_textarea]:placeholder:text-fg-
|
|
88
|
+
'[&_input]:w-full [&_input]:border-0 [&_input]:bg-transparent [&_input]:p-0 [&_input]:text-display [&_input]:font-medium [&_input]:text-fg [&_input]:outline-none [&_input]:shadow-none [&_input]:ring-0 [&_input]:placeholder:text-fg-muted ' +
|
|
89
|
+
'[&_textarea]:min-h-40 [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:border-0 [&_textarea]:bg-transparent [&_textarea]:p-0 [&_textarea]:text-display [&_textarea]:font-medium [&_textarea]:text-fg [&_textarea]:outline-none [&_textarea]:placeholder:text-fg-muted'
|
|
90
90
|
|
|
91
91
|
function IconBadge({ icon }: { icon: ReactNode }) {
|
|
92
92
|
return (
|
|
@@ -166,7 +166,7 @@ export function FocusField({
|
|
|
166
166
|
<div className="min-w-0 flex-1">
|
|
167
167
|
<div className="text-small text-fg-muted">{label}</div>
|
|
168
168
|
<div className="mt-1 truncate text-body text-fg">
|
|
169
|
-
{hasValue ? preview : <span className="text-fg-
|
|
169
|
+
{hasValue ? preview : <span className="text-fg-muted">{placeholder}</span>}
|
|
170
170
|
</div>
|
|
171
171
|
</div>
|
|
172
172
|
</div>
|
package/src/components/icons.tsx
CHANGED
|
@@ -78,6 +78,36 @@ export const ExternalLinkIcon = (p: IconProps) => (
|
|
|
78
78
|
<path d="M15 3h6v6M21 3l-9 9M10 5H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5" />
|
|
79
79
|
</svg>
|
|
80
80
|
)
|
|
81
|
+
export const PencilIcon = (p: IconProps) => (
|
|
82
|
+
<svg {...STROKE} {...p}>
|
|
83
|
+
<path d="M12 20h9" />
|
|
84
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L8 18l-4 1 1-4z" />
|
|
85
|
+
</svg>
|
|
86
|
+
)
|
|
87
|
+
export const CopyIcon = (p: IconProps) => (
|
|
88
|
+
<svg {...STROKE} {...p}>
|
|
89
|
+
<rect x="9" y="9" width="11" height="11" rx="2" />
|
|
90
|
+
<path d="M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3" />
|
|
91
|
+
</svg>
|
|
92
|
+
)
|
|
93
|
+
export const ShareIcon = (p: IconProps) => (
|
|
94
|
+
<svg {...STROKE} {...p}>
|
|
95
|
+
<circle cx="18" cy="5" r="3" />
|
|
96
|
+
<circle cx="6" cy="12" r="3" />
|
|
97
|
+
<circle cx="18" cy="19" r="3" />
|
|
98
|
+
<path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4" />
|
|
99
|
+
</svg>
|
|
100
|
+
)
|
|
101
|
+
export const PinIcon = (p: IconProps) => (
|
|
102
|
+
<svg {...STROKE} {...p}>
|
|
103
|
+
<path d="m9 3 6 6M10 8l-5 5 6 1 1 6 5-5M2 22l6-6" />
|
|
104
|
+
</svg>
|
|
105
|
+
)
|
|
106
|
+
export const ArchiveIcon = (p: IconProps) => (
|
|
107
|
+
<svg {...STROKE} {...p}>
|
|
108
|
+
<path d="M4 7v13h16V7M3 3h18v4H3zM9 11h6" />
|
|
109
|
+
</svg>
|
|
110
|
+
)
|
|
81
111
|
export const ExpandIcon = (p: IconProps) => (
|
|
82
112
|
<svg {...STROKE} {...p}><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg>
|
|
83
113
|
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Input } from './input'
|
|
2
|
+
|
|
3
|
+
export const examples = {
|
|
4
|
+
default: () => <Input label="Project name" placeholder="Annual report" />,
|
|
5
|
+
variants: () => <><Input label="Outlined" /><Input appearance="filled" label="Filled" placeholder="Annual report" /></>,
|
|
6
|
+
states: () => <><Input appearance="filled" label="Disabled" defaultValue="Saved content" isDisabled /><Input appearance="filled" label="Invalid" isInvalid errorMessage="Check the value." /></>,
|
|
7
|
+
}
|
|
8
|
+
export const meta = { category: 'Form', description: 'Labelled input with outlined and Manager-style filled appearances, disabled/error states and keyboard focus.' }
|
package/src/components/input.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type TextFieldProps,
|
|
9
9
|
} from 'react-aria-components'
|
|
10
10
|
import { uic } from '../utils/uic'
|
|
11
|
+
import { filledFieldClasses, type FieldAppearance } from './field-appearance'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Input — labelled single-line text field.
|
|
@@ -28,29 +29,36 @@ const StyledInput = uic(RACInput, {
|
|
|
28
29
|
// fg-subtle · focus #75e7b8 → brand-green · error → danger. This is the shared
|
|
29
30
|
// filled-field skin (textarea / combobox / date-field / number-field /
|
|
30
31
|
// search-field); `fieldSize` adds the single-line height on top.
|
|
31
|
-
baseClass:
|
|
32
|
-
|
|
32
|
+
baseClass: 'w-full rounded-lg px-4',
|
|
33
|
+
variants: {
|
|
34
|
+
appearance: {
|
|
35
|
+
filled: filledFieldClasses,
|
|
36
|
+
outlined: 'border border-border bg-surface text-small text-fg ' +
|
|
33
37
|
'outline-none transition-colors duration-200 placeholder:text-fg-muted ' +
|
|
34
38
|
'data-[hovered]:border-fg-subtle ' +
|
|
35
39
|
'data-[focused]:border-brand-green data-[focused]:ring-2 data-[focused]:ring-ring ' +
|
|
36
40
|
'data-[invalid]:border-danger data-[invalid]:ring-2 data-[invalid]:ring-danger ' +
|
|
37
41
|
'data-[disabled]:bg-surface-muted data-[disabled]:opacity-60 data-[disabled]:pointer-events-none',
|
|
38
|
-
|
|
42
|
+
},
|
|
39
43
|
// `fieldSize` (not `size`) to avoid colliding with the native <input size>
|
|
40
44
|
// attribute, which RAC's Input inherits (a numeric prop).
|
|
41
45
|
fieldSize: {
|
|
42
46
|
sm: 'h-8',
|
|
43
47
|
md: 'h-10',
|
|
48
|
+
filled: 'h-10.5 py-3',
|
|
44
49
|
lg: 'h-12',
|
|
45
50
|
tall: 'h-control-tall',
|
|
46
51
|
},
|
|
47
52
|
},
|
|
48
53
|
defaultVariants: {
|
|
54
|
+
appearance: 'outlined',
|
|
49
55
|
fieldSize: 'md',
|
|
50
56
|
},
|
|
51
57
|
})
|
|
52
58
|
|
|
53
59
|
export type InputProps = TextFieldProps & {
|
|
60
|
+
/** Filled matches the original Manager dialog fields; outlined remains the default. */
|
|
61
|
+
appearance?: FieldAppearance
|
|
54
62
|
/** Visible field label (required for accessibility). */
|
|
55
63
|
label: ReactNode
|
|
56
64
|
/** Helper text rendered under the field. */
|
|
@@ -65,10 +73,10 @@ export type InputProps = TextFieldProps & {
|
|
|
65
73
|
inputClassName?: string
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
export const Input = ({ label, description, errorMessage, placeholder, size, rootClassName, inputClassName, ...props }: InputProps) => (
|
|
76
|
+
export const Input = ({ label, description, errorMessage, placeholder, size, appearance = 'outlined', rootClassName, inputClassName, ...props }: InputProps) => (
|
|
69
77
|
<TextField {...props} className={`flex w-full flex-col gap-3 ${rootClassName ?? ''}`}>
|
|
70
78
|
<Label className="text-panel-heading font-medium text-fg">{label}</Label>
|
|
71
|
-
<StyledInput className={inputClassName} placeholder={placeholder} fieldSize={size} />
|
|
79
|
+
<StyledInput className={inputClassName} placeholder={placeholder} appearance={appearance} fieldSize={size ?? (appearance === 'filled' ? 'filled' : undefined)} />
|
|
72
80
|
{description ? (
|
|
73
81
|
<Text slot="description" className="text-label text-fg-muted">
|
|
74
82
|
{description}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MediaAssetPanel } from './media-asset-panel'
|
|
2
|
+
const labels = { heading: 'Assets', emptyDescription: 'No assets yet.', emptyHint: 'Select assets to begin.', selectLabel: 'Select assets', previousLabel: 'Previous asset', nextLabel: 'Next asset' }
|
|
3
|
+
const items = ['First', 'Second'].map(id => ({ id, content: <div className="grid h-full place-items-center bg-surface-muted">{id}</div> }))
|
|
4
|
+
export const examples = {
|
|
5
|
+
default: () => <MediaAssetPanel {...labels} items={items} mode="media" empty={false} onSelect={() => undefined} />,
|
|
6
|
+
variants: () => <MediaAssetPanel {...labels} items={items} mode="carousel" empty={false} onSelect={() => undefined} />,
|
|
7
|
+
states: () => <MediaAssetPanel {...labels} items={[]} mode="media" empty onSelect={() => undefined} />,
|
|
8
|
+
readOnly: () => <MediaAssetPanel {...labels} items={items} mode="grid" empty={false} />,
|
|
9
|
+
reference: () => <MediaAssetPanel {...labels} items={items.slice(0, 1)} mode="media" empty={false} identity={{ title: 'Annual report', caption: '1 asset reference', count: 1 }} onSelect={() => undefined} />,
|
|
10
|
+
}
|
|
11
|
+
export const meta = { category: 'Composite', description: 'Asset preview panel with centered empty action and independent carousel controls.' }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
3
|
+
import { uic } from '../utils/uic'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { MediaGallery, type MediaGalleryItem } from './media-gallery'
|
|
6
|
+
|
|
7
|
+
export const MEDIA_ASSET_PANEL_MODES = ['media', 'grid', 'carousel'] as const
|
|
8
|
+
export type MediaAssetPanelMode = (typeof MEDIA_ASSET_PANEL_MODES)[number]
|
|
9
|
+
export type MediaAssetPanelProps = {
|
|
10
|
+
items: readonly MediaGalleryItem[]
|
|
11
|
+
mode: MediaAssetPanelMode
|
|
12
|
+
empty: boolean
|
|
13
|
+
heading: ReactNode
|
|
14
|
+
emptyDescription: ReactNode
|
|
15
|
+
emptyHint: ReactNode
|
|
16
|
+
selectLabel: string
|
|
17
|
+
previousLabel: string
|
|
18
|
+
nextLabel: string
|
|
19
|
+
onSelect?: () => void
|
|
20
|
+
/** Optional reference identity; omitted for a media-only gallery. */
|
|
21
|
+
identity?: { title: ReactNode; caption: ReactNode; count: ReactNode }
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
const Surface = uic('div', {
|
|
25
|
+
displayName: 'MediaAssetPanel',
|
|
26
|
+
baseClass: 'group/media-panel relative isolate h-95 w-full shrink-0 overflow-hidden rounded-lg bg-surface-card text-fg',
|
|
27
|
+
})
|
|
28
|
+
const PanelAction = uic(AriaButton, {
|
|
29
|
+
displayName: 'MediaAssetPanelAction',
|
|
30
|
+
baseClass: 'absolute inset-0 z-10 cursor-pointer rounded-lg border-0 bg-transparent p-0 outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-inset data-[focus-visible]:ring-ring',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/** Full-bleed media with independent selection and carousel controls. */
|
|
34
|
+
export function MediaAssetPanel({ items, mode, empty, heading, emptyDescription, emptyHint, selectLabel, previousLabel, nextLabel, onSelect, identity, className }: MediaAssetPanelProps) {
|
|
35
|
+
return <Surface className={className}>
|
|
36
|
+
{onSelect ? empty
|
|
37
|
+
? <button type="button" aria-label={selectLabel} aria-hidden="true" tabIndex={-1} onClick={onSelect} className="absolute inset-0 z-10 cursor-pointer rounded-lg border-0 bg-transparent p-0" />
|
|
38
|
+
: <PanelAction aria-label={selectLabel} onPress={onSelect} /> : null}
|
|
39
|
+
{empty ? <>
|
|
40
|
+
<div className="pointer-events-none absolute inset-x-4 top-4 z-20 text-body font-medium leading-5">{heading}</div>
|
|
41
|
+
{onSelect ? <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
|
|
42
|
+
<Button className="pointer-events-auto h-14 w-48 p-0" onPress={onSelect}>{selectLabel}</Button>
|
|
43
|
+
</div> : null}
|
|
44
|
+
<p className="pointer-events-none absolute inset-x-4 bottom-4 z-20 m-0 text-small">
|
|
45
|
+
<span className="font-medium text-fg">{emptyDescription}</span><br />
|
|
46
|
+
<span className="text-fg-muted">{emptyHint}</span>
|
|
47
|
+
</p>
|
|
48
|
+
</> : <MediaGallery items={mode === 'media' ? items.slice(0, 1) : items} mode={mode === 'carousel' ? 'carousel' : 'grid'} previousLabel={previousLabel} nextLabel={nextLabel} className="pointer-events-none" />}
|
|
49
|
+
{!empty && identity ? <>
|
|
50
|
+
<div aria-hidden="true" className="pointer-events-none absolute inset-y-0 left-0 z-[1] hidden w-[min(56%,34rem)] bg-[linear-gradient(90deg,rgba(0,0,0,0.52)_0%,rgba(0,0,0,0.28)_58%,transparent_100%)] group-has-[img]/media-panel:block" />
|
|
51
|
+
<div className="pointer-events-none absolute inset-4 z-[2] grid grid-rows-[auto_minmax(0,1fr)_auto] gap-6 text-fg group-has-[img]/media-panel:text-white group-has-[img]/media-panel:[text-shadow:0_1px_3px_rgba(0,0,0,0.5)]">
|
|
52
|
+
<div className="mb-2 flex items-start justify-between gap-2 text-body font-medium leading-5"><span>{heading}</span><span className="rounded-full bg-surface-muted px-2.5 py-1 text-caption text-fg [text-shadow:none]">{identity.count}</span></div>
|
|
53
|
+
<p className="m-0 max-w-[19ch] pt-2 text-heading1 font-medium leading-[30px]">{identity.title}</p>
|
|
54
|
+
<p className="m-0 text-small opacity-80">{identity.caption}</p>
|
|
55
|
+
</div>
|
|
56
|
+
</> : null}
|
|
57
|
+
</Surface>
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MediaGallery } from './media-gallery'
|
|
2
|
+
|
|
3
|
+
const sample = (label: string) => <div className="grid h-full min-h-24 place-items-center bg-surface-card text-label text-fg">{label}</div>
|
|
4
|
+
const items = ['One', 'Two', 'Three', 'Four'].map(label => ({ id: label.toLowerCase(), content: sample(label) }))
|
|
5
|
+
|
|
6
|
+
export const examples = {
|
|
7
|
+
default: () => <MediaGallery items={items.slice(0, 1)} mode="grid" previousLabel="Previous" nextLabel="Next" className="h-48" />,
|
|
8
|
+
grid: () => <MediaGallery items={items} mode="grid" previousLabel="Previous" nextLabel="Next" className="h-48" />,
|
|
9
|
+
carousel: () => <MediaGallery items={items.slice(0, 3)} mode="carousel" previousLabel="Previous image" nextLabel="Next image" className="h-48" />,
|
|
10
|
+
empty: () => <div className="h-48"><MediaGallery items={[]} mode="grid" previousLabel="Previous" nextLabel="Next" /></div>,
|
|
11
|
+
}
|
|
12
|
+
export const meta = { category: 'Composite', description: 'Media grid and carousel with consumer-rendered previews. Coarse-pointer controls use 44px targets.' }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode, type SyntheticEvent } from 'react'
|
|
2
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
3
|
+
import { uic } from '../utils/uic'
|
|
4
|
+
|
|
5
|
+
export const MEDIA_GALLERY_MODES = ['grid', 'carousel'] as const
|
|
6
|
+
export type MediaGalleryMode = (typeof MEDIA_GALLERY_MODES)[number]
|
|
7
|
+
export type MediaGalleryItem = { id: string; content: ReactNode }
|
|
8
|
+
export type MediaGalleryProps = {
|
|
9
|
+
items: readonly MediaGalleryItem[]
|
|
10
|
+
mode: MediaGalleryMode
|
|
11
|
+
previousLabel: string
|
|
12
|
+
nextLabel: string
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GalleryRoot = uic('div', {
|
|
17
|
+
baseClass: 'relative block h-full w-full overflow-hidden bg-surface-muted',
|
|
18
|
+
displayName: 'MediaGallery',
|
|
19
|
+
})
|
|
20
|
+
const itemClass = 'block h-full min-h-0 w-full overflow-hidden [&>img]:block [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>video]:block [&>video]:h-full [&>video]:w-full [&>video]:object-cover'
|
|
21
|
+
const stopParentActivation = (event: SyntheticEvent): void => event.stopPropagation()
|
|
22
|
+
const GalleryControl = uic(AriaButton, {
|
|
23
|
+
baseClass: 'pointer-events-auto inline-flex size-10 items-center justify-center rounded-full border-0 bg-surface p-0 text-title leading-none text-fg cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 pointer-coarse:size-11',
|
|
24
|
+
displayName: 'MediaGalleryControl',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function MediaGallery({ items, mode, previousLabel, nextLabel, className }: MediaGalleryProps) {
|
|
28
|
+
const firstId = items[0]?.id ?? null
|
|
29
|
+
const [activeId, setActiveId] = useState<string | null>(firstId)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setActiveId(current => current && items.some(item => item.id === current) ? current : firstId)
|
|
32
|
+
}, [firstId, items])
|
|
33
|
+
if (items.length === 0) return null
|
|
34
|
+
if (mode === 'grid') {
|
|
35
|
+
const visibleItems = items.slice(0, 4)
|
|
36
|
+
return <GalleryRoot className={['grid gap-0.5', visibleItems.length === 1 ? 'grid-cols-1 grid-rows-1' : visibleItems.length === 2 ? 'grid-cols-2 grid-rows-1' : 'grid-cols-2 grid-rows-2', className].filter(Boolean).join(' ')}>
|
|
37
|
+
{visibleItems.map(item => <div key={item.id} className={itemClass}>{item.content}</div>)}
|
|
38
|
+
</GalleryRoot>
|
|
39
|
+
}
|
|
40
|
+
const activeIndex = Math.max(0, items.findIndex(item => item.id === activeId))
|
|
41
|
+
const activeItem = items[activeIndex] ?? items[0]
|
|
42
|
+
// `items` is non-empty past the early return above, but that does not narrow an
|
|
43
|
+
// indexed access, so make the empty case explicit rather than asserting.
|
|
44
|
+
if (!activeItem) return null
|
|
45
|
+
const selectRelative = (offset: number) => setActiveId(items[(activeIndex + offset + items.length) % items.length]?.id ?? firstId)
|
|
46
|
+
return <GalleryRoot className={className}>
|
|
47
|
+
<div className={itemClass}>{activeItem.content}</div>
|
|
48
|
+
{items.length > 1 ? <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-between p-4">
|
|
49
|
+
<GalleryControl aria-label={previousLabel} onClick={stopParentActivation} onPress={() => selectRelative(-1)}><span aria-hidden="true">‹</span></GalleryControl>
|
|
50
|
+
<GalleryControl aria-label={nextLabel} onClick={stopParentActivation} onPress={() => selectRelative(1)}><span aria-hidden="true">›</span></GalleryControl>
|
|
51
|
+
</div> : null}
|
|
52
|
+
</GalleryRoot>
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { MediaSettingsDialog, type MediaSettingsLabels, type MediaSettingsValue } from './media-settings-dialog'
|
|
4
|
+
|
|
5
|
+
const labels: MediaSettingsLabels = {
|
|
6
|
+
title: 'Media settings', displayTitle: 'Display', displayHint: 'Choose how this reference appears.', displayMode: 'Display mode', media: 'Media', gallery: 'Gallery', widthTitle: 'Width', widthHint: 'Choose the content width.', mediaWidth: 'Media width', textWidth: 'Text', wide: 'Wide', fullWidth: 'Full width', layoutTitle: 'Layout', layoutHint: 'Choose the gallery layout.', galleryLayout: 'Gallery layout', carousel: 'Carousel', grid: 'Grid', cancel: 'Cancel', save: 'Save', close: 'Close',
|
|
7
|
+
}
|
|
8
|
+
const initial: MediaSettingsValue = { displayMode: 'media', mediaWidth: 'text', galleryLayout: 'grid' }
|
|
9
|
+
|
|
10
|
+
function Example({ value = initial }: { value?: MediaSettingsValue }) {
|
|
11
|
+
const [open, setOpen] = useState(true)
|
|
12
|
+
const [current, setCurrent] = useState(value)
|
|
13
|
+
return <MediaSettingsDialog isOpen={open} onClose={() => setOpen(false)} onSave={next => { setCurrent(next); setOpen(false) }} value={current} labels={labels} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const examples = {
|
|
17
|
+
default: () => <Example />,
|
|
18
|
+
gallery: () => <Example value={{ ...initial, displayMode: 'gallery' }} />,
|
|
19
|
+
disabled: () => <MediaSettingsDialog isOpen onClose={() => undefined} onSave={() => undefined} value={initial} labels={labels} isDisabled />,
|
|
20
|
+
}
|
|
21
|
+
export const meta = { category: 'Composite', description: 'Media presentation settings with discardable draft and conditional fields.' }
|