@dashforge/tw 0.11.0-beta → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -12
- package/COVERAGE.md +2 -2
- package/PARITY.md +7 -9
- package/PERFORMANCE.md +3 -4
- package/README.md +74 -15
- package/THEME-AUDIT.md +3 -4
- package/dashforge-tw-1.0.0.tgz +0 -0
- package/dist/index.esm.js +3127 -748
- package/dist/src/components/Alert/Alert.d.ts +65 -0
- package/dist/src/components/Alert/Alert.d.ts.map +1 -0
- package/dist/src/components/Alert/alert.types.d.ts +130 -0
- package/dist/src/components/Alert/alert.types.d.ts.map +1 -0
- package/dist/src/components/Alert/alert.variants.d.ts +85 -0
- package/dist/src/components/Alert/alert.variants.d.ts.map +1 -0
- package/dist/src/components/Avatar/Avatar.d.ts +48 -0
- package/dist/src/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/src/components/Avatar/avatar.types.d.ts +162 -0
- package/dist/src/components/Avatar/avatar.types.d.ts.map +1 -0
- package/dist/src/components/Avatar/avatar.variants.d.ts +208 -0
- package/dist/src/components/Avatar/avatar.variants.d.ts.map +1 -0
- package/dist/src/components/Badge/Badge.d.ts +63 -0
- package/dist/src/components/Badge/Badge.d.ts.map +1 -0
- package/dist/src/components/Badge/badge.types.d.ts +124 -0
- package/dist/src/components/Badge/badge.types.d.ts.map +1 -0
- package/dist/src/components/Badge/badge.variants.d.ts +262 -0
- package/dist/src/components/Badge/badge.variants.d.ts.map +1 -0
- package/dist/src/components/Box/Box.d.ts.map +1 -1
- package/dist/src/components/Box/box.types.d.ts +53 -0
- package/dist/src/components/Box/box.types.d.ts.map +1 -1
- package/dist/src/components/Button/Button.d.ts.map +1 -1
- package/dist/src/components/Button/button.types.d.ts +35 -0
- package/dist/src/components/Button/button.types.d.ts.map +1 -1
- package/dist/src/components/Card/Card.d.ts +107 -0
- package/dist/src/components/Card/Card.d.ts.map +1 -0
- package/dist/src/components/Card/card.types.d.ts +119 -0
- package/dist/src/components/Card/card.types.d.ts.map +1 -0
- package/dist/src/components/Chip/Chip.d.ts +67 -0
- package/dist/src/components/Chip/Chip.d.ts.map +1 -0
- package/dist/src/components/Chip/chip.types.d.ts +113 -0
- package/dist/src/components/Chip/chip.types.d.ts.map +1 -0
- package/dist/src/components/Chip/chip.variants.d.ts +155 -0
- package/dist/src/components/Chip/chip.variants.d.ts.map +1 -0
- package/dist/src/components/IconButton/IconButton.d.ts +57 -0
- package/dist/src/components/IconButton/IconButton.d.ts.map +1 -0
- package/dist/src/components/IconButton/iconButton.types.d.ts +96 -0
- package/dist/src/components/IconButton/iconButton.types.d.ts.map +1 -0
- package/dist/src/components/IconButton/iconButton.variants.d.ts +26 -0
- package/dist/src/components/IconButton/iconButton.variants.d.ts.map +1 -0
- package/dist/src/components/Menu/Menu.d.ts +118 -0
- package/dist/src/components/Menu/Menu.d.ts.map +1 -0
- package/dist/src/components/Menu/menu.types.d.ts +179 -0
- package/dist/src/components/Menu/menu.types.d.ts.map +1 -0
- package/dist/src/components/Menu/menu.variants.d.ts +129 -0
- package/dist/src/components/Menu/menu.variants.d.ts.map +1 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.types.d.ts +40 -3
- package/dist/src/components/Snackbar/snackbar.types.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.variants.d.ts +12 -57
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
- package/dist/src/components/Spinner/Spinner.d.ts +44 -0
- package/dist/src/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/src/components/Spinner/spinner.types.d.ts +89 -0
- package/dist/src/components/Spinner/spinner.types.d.ts.map +1 -0
- package/dist/src/components/Spinner/spinner.variants.d.ts +111 -0
- package/dist/src/components/Spinner/spinner.variants.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts +15 -72
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -1
- package/dist/src/components/_shared/severity/index.d.ts +21 -0
- package/dist/src/components/_shared/severity/index.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severity.types.d.ts +48 -0
- package/dist/src/components/_shared/severity/severity.types.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severityIcons.d.ts +45 -0
- package/dist/src/components/_shared/severity/severityIcons.d.ts.map +1 -0
- package/dist/src/components/_shared/severity/severityVariants.d.ts +22 -0
- package/dist/src/components/_shared/severity/severityVariants.d.ts.map +1 -0
- package/dist/src/index.d.ts +27 -2
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/components/Alert/Alert.test.tsx +302 -0
- package/src/components/Alert/Alert.tsx +186 -0
- package/src/components/Alert/alert.types.ts +144 -0
- package/src/components/Alert/alert.variants.ts +71 -0
- package/src/components/Autocomplete/autocomplete.variants.ts +1 -1
- package/src/components/Avatar/Avatar.test.tsx +287 -0
- package/src/components/Avatar/Avatar.tsx +304 -0
- package/src/components/Avatar/avatar.types.ts +205 -0
- package/src/components/Avatar/avatar.variants.ts +194 -0
- package/src/components/Badge/Badge.test.tsx +385 -0
- package/src/components/Badge/Badge.tsx +174 -0
- package/src/components/Badge/badge.types.ts +154 -0
- package/src/components/Badge/badge.variants.ts +161 -0
- package/src/components/Box/Box.test.tsx +21 -0
- package/src/components/Box/Box.tsx +32 -3
- package/src/components/Box/box.types.ts +55 -0
- package/src/components/Button/Button.test.tsx +21 -0
- package/src/components/Button/Button.tsx +28 -27
- package/src/components/Button/button.types.ts +36 -0
- package/src/components/Card/Card.test.tsx +291 -0
- package/src/components/Card/Card.tsx +241 -0
- package/src/components/Card/card.types.ts +132 -0
- package/src/components/Chip/Chip.test.tsx +316 -0
- package/src/components/Chip/Chip.tsx +224 -0
- package/src/components/Chip/chip.types.ts +128 -0
- package/src/components/Chip/chip.variants.ts +173 -0
- package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +1 -1
- package/src/components/IconButton/IconButton.test.tsx +367 -0
- package/src/components/IconButton/IconButton.tsx +159 -0
- package/src/components/IconButton/iconButton.types.ts +106 -0
- package/src/components/IconButton/iconButton.variants.ts +31 -0
- package/src/components/LeftNav/leftNav.variants.ts +2 -2
- package/src/components/Menu/Menu.test.tsx +408 -0
- package/src/components/Menu/Menu.tsx +335 -0
- package/src/components/Menu/menu.types.ts +221 -0
- package/src/components/Menu/menu.variants.ts +132 -0
- package/src/components/Pagination/pagination.variants.ts +1 -1
- package/src/components/Snackbar/Snackbar.tsx +46 -28
- package/src/components/Snackbar/snackbar.types.ts +47 -3
- package/src/components/Snackbar/snackbar.variants.ts +29 -22
- package/src/components/Spinner/Spinner.test.tsx +199 -0
- package/src/components/Spinner/Spinner.tsx +158 -0
- package/src/components/Spinner/spinner.types.ts +113 -0
- package/src/components/Spinner/spinner.variants.ts +83 -0
- package/src/components/Table/cells/RenderChip.tsx +24 -87
- package/src/components/Table/cells/RowActionsMenu.tsx +1 -1
- package/src/components/TextField/TextField.test.tsx +5 -1
- package/src/components/_shared/severity/index.ts +33 -0
- package/src/components/_shared/severity/severity.types.ts +50 -0
- package/src/components/_shared/severity/severityIcons.tsx +104 -0
- package/src/components/_shared/severity/severityVariants.test.ts +136 -0
- package/src/components/_shared/severity/severityVariants.ts +115 -0
- package/src/index.ts +163 -1
- package/vite.config.ts +8 -0
- package/vitest.config.mts +8 -1
- package/LICENSE +0 -21
package/dist/index.esm.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useMemo, forwardRef, useContext,
|
|
1
|
+
import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
2
|
+
import { useMemo, forwardRef, useContext, useState, useEffect, useId, useRef, useCallback, Fragment, createContext, memo, Children, isValidElement, cloneElement, useLayoutEffect } from 'react';
|
|
3
3
|
import { Slot } from '@radix-ui/react-slot';
|
|
4
|
+
import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
|
|
4
5
|
import clsx from 'clsx';
|
|
5
6
|
import { twMerge } from 'tailwind-merge';
|
|
6
7
|
import { useRbacOptional, resolveAccessState } from '@dashforge/rbac';
|
|
7
8
|
import { tv } from 'tailwind-variants';
|
|
8
9
|
export { tv } from 'tailwind-variants';
|
|
9
|
-
import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
|
|
10
10
|
import { useDashFieldMeta } from '@dashforge/forms';
|
|
11
11
|
import * as RadixCheckbox from '@radix-ui/react-checkbox';
|
|
12
12
|
import * as RadixSwitch from '@radix-ui/react-switch';
|
|
13
13
|
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
|
|
14
14
|
import { useCalendar, resolveCalendarKey, formatLongDate, parseISODate, formatTime, generateTimeOptions, timeStringToMinutes, getTodayISODate, useDateRange } from '@dashforge/calendar-core';
|
|
15
15
|
import * as RadixPopover from '@radix-ui/react-popover';
|
|
16
|
+
import * as RadixMenu from '@radix-ui/react-dropdown-menu';
|
|
16
17
|
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
17
18
|
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
|
18
19
|
import * as RadixAccordion from '@radix-ui/react-accordion';
|
|
@@ -132,6 +133,190 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
132
133
|
]);
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Tailwind-variants recipe for `<Spinner>`.
|
|
138
|
+
*
|
|
139
|
+
* Axes:
|
|
140
|
+
* - `size` — 5 steps (xs/sm/md/lg/xl) → w/h spacing tokens
|
|
141
|
+
* - `thickness` — 3 steps (thin/md/thick) → SVG stroke-width
|
|
142
|
+
* - `color` — 7 intents (or omitted → `currentColor`)
|
|
143
|
+
*
|
|
144
|
+
* The stroke + fill of the SVG paths are governed by
|
|
145
|
+
* `stroke="currentColor"` (set inline in Spinner.tsx), so the root
|
|
146
|
+
* `text-*` class drives the color. When `color` is omitted, no
|
|
147
|
+
* `text-*` class is emitted at all — `currentColor` resolves to
|
|
148
|
+
* whatever the parent's text color is.
|
|
149
|
+
*/ const spinnerVariants = tv({
|
|
150
|
+
base: 'inline-block animate-spin motion-reduce:animate-none',
|
|
151
|
+
variants: {
|
|
152
|
+
size: {
|
|
153
|
+
xs: 'w-3 h-3',
|
|
154
|
+
sm: 'w-4 h-4',
|
|
155
|
+
md: 'w-5 h-5',
|
|
156
|
+
lg: 'w-6 h-6',
|
|
157
|
+
xl: 'w-8 h-8'
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* Color → text-{color}-600 (mid-tone, readable on most surfaces).
|
|
161
|
+
* Note: text-{color}-600 acts on currentColor inside the SVG via
|
|
162
|
+
* `stroke="currentColor"`. Neutral uses `text-neutral-600` which
|
|
163
|
+
* auto-inverts via the dashforgePreset CSS-var swap (no `dark:`
|
|
164
|
+
* needed). Color rows do NOT carry `dark:` either — the chosen
|
|
165
|
+
* 600 step works in both modes for the spinner's specific use
|
|
166
|
+
* (small element, brief visibility).
|
|
167
|
+
*/ color: {
|
|
168
|
+
neutral: 'text-neutral-600',
|
|
169
|
+
primary: 'text-primary-600',
|
|
170
|
+
secondary: 'text-secondary-600',
|
|
171
|
+
success: 'text-success-600',
|
|
172
|
+
warning: 'text-warning-600',
|
|
173
|
+
danger: 'text-danger-600',
|
|
174
|
+
info: 'text-info-600'
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
defaultVariants: {
|
|
178
|
+
size: 'md'
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* SVG stroke-width per thickness step. Picked to read well at every
|
|
183
|
+
* size — thin (1.5px) is the minimum readable; thick (3px) saturates
|
|
184
|
+
* the available radius at sizes xs/sm.
|
|
185
|
+
*/ const SPINNER_STROKE_WIDTH = {
|
|
186
|
+
thin: 1.5,
|
|
187
|
+
md: 2.25,
|
|
188
|
+
thick: 3
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The track ring renders at this opacity of `currentColor`. 0.2 is
|
|
192
|
+
* the sweet spot — visible enough to anchor the eye, faint enough
|
|
193
|
+
* to read as background.
|
|
194
|
+
*/ const SPINNER_TRACK_OPACITY = 0.2;
|
|
195
|
+
/** Re-export the color name list so playground demos can iterate. */ const SPINNER_COLORS = [
|
|
196
|
+
'neutral',
|
|
197
|
+
'primary',
|
|
198
|
+
'secondary',
|
|
199
|
+
'success',
|
|
200
|
+
'warning',
|
|
201
|
+
'danger',
|
|
202
|
+
'info'
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `<Spinner>` — rotating-arc loading indicator.
|
|
207
|
+
*
|
|
208
|
+
* Visual: a partial-arc SVG rotating via Tailwind's `animate-spin`
|
|
209
|
+
* (pure CSS, GPU-accelerated). Optional `withTrack` renders a faint
|
|
210
|
+
* ghost ring (20% opacity of currentColor) behind the arc — the
|
|
211
|
+
* standard "premium spinner" pattern used by Stripe / Vercel / Linear.
|
|
212
|
+
*
|
|
213
|
+
* Color resolution:
|
|
214
|
+
* - `color="primary"` (or any 7 intent) → `text-{color}-600` class
|
|
215
|
+
* → SVG `stroke="currentColor"` picks it up
|
|
216
|
+
* - `color` omitted → no `text-*` class emitted → SVG inherits
|
|
217
|
+
* parent's text color via `currentColor`. **This is the right
|
|
218
|
+
* default for nested usage** (inside Button, Alert, Card).
|
|
219
|
+
*
|
|
220
|
+
* A11y:
|
|
221
|
+
* - `role="status"` on the wrapper
|
|
222
|
+
* - `aria-live="polite"` so SR queues the announcement
|
|
223
|
+
* - visually-hidden text label inside (`'Loading'` default)
|
|
224
|
+
* - `animate-spin` is gated on `motion-reduce` (WCAG 2.3.3)
|
|
225
|
+
*
|
|
226
|
+
* Delay (anti-flash):
|
|
227
|
+
* - `delay={150}` mounts as `null` for the first 150ms, then swaps
|
|
228
|
+
* to the actual SVG. Quick operations that finish before 150ms
|
|
229
|
+
* never render the spinner — eliminates the "flash" UX bug.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```tsx
|
|
233
|
+
* // 1. Inside a Button — inherits text color
|
|
234
|
+
* <button><Spinner size="sm" /> Saving...</button>
|
|
235
|
+
*
|
|
236
|
+
* // 2. Standalone with explicit color
|
|
237
|
+
* <Spinner color="primary" size="lg" label="Loading dashboard data" />
|
|
238
|
+
*
|
|
239
|
+
* // 3. Anti-flash on quick submits
|
|
240
|
+
* <Spinner visibleWhen={() => isSubmitting} delay={150} />
|
|
241
|
+
*
|
|
242
|
+
* // 4. With track for busy backgrounds
|
|
243
|
+
* <Spinner color="primary" withTrack size="xl" />
|
|
244
|
+
* ```
|
|
245
|
+
*/ const Spinner = /*#__PURE__*/ forwardRef(function Spinner(props, ref) {
|
|
246
|
+
const { size = 'md', color, thickness = 'md', withTrack = false, label = 'Loading', delay = 0, visibleWhen, sx, className } = props;
|
|
247
|
+
// Bridge — engine-reactive visibility. Hook called unconditionally
|
|
248
|
+
// (rules-of-hooks). Outside a `<DashForm>`, predicate evaluated as
|
|
249
|
+
// a plain closure.
|
|
250
|
+
const bridge = useContext(DashFormContext);
|
|
251
|
+
const isBridgeVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
252
|
+
// Anti-flash delay — render `null` for the first `delay` ms after
|
|
253
|
+
// the spinner becomes visible. Skipping the timer entirely when
|
|
254
|
+
// delay=0 keeps the synchronous render path for the common case.
|
|
255
|
+
const [delayElapsed, setDelayElapsed] = useState(delay <= 0);
|
|
256
|
+
useEffect(()=>{
|
|
257
|
+
if (delay <= 0) {
|
|
258
|
+
setDelayElapsed(true);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
setDelayElapsed(false);
|
|
262
|
+
const handle = setTimeout(()=>setDelayElapsed(true), delay);
|
|
263
|
+
return ()=>clearTimeout(handle);
|
|
264
|
+
}, [
|
|
265
|
+
delay
|
|
266
|
+
]);
|
|
267
|
+
if (!isBridgeVisible || !delayElapsed) return null;
|
|
268
|
+
const classes = cn(spinnerVariants({
|
|
269
|
+
size,
|
|
270
|
+
color
|
|
271
|
+
}), sx, className);
|
|
272
|
+
const strokeWidth = SPINNER_STROKE_WIDTH[thickness];
|
|
273
|
+
// Decorative mode — when label is explicitly empty (""), suppress
|
|
274
|
+
// role="status" and the visually-hidden label entirely. Used when
|
|
275
|
+
// the Spinner is embedded inside another component that already
|
|
276
|
+
// announces its loading state via `aria-busy` (e.g., Button) — the
|
|
277
|
+
// inner spinner is then purely visual chrome.
|
|
278
|
+
const isDecorative = label === '';
|
|
279
|
+
return jsxs("span", {
|
|
280
|
+
ref: ref,
|
|
281
|
+
className: classes,
|
|
282
|
+
role: isDecorative ? undefined : 'status',
|
|
283
|
+
"aria-live": isDecorative ? undefined : 'polite',
|
|
284
|
+
"aria-hidden": isDecorative ? true : undefined,
|
|
285
|
+
children: [
|
|
286
|
+
jsxs("svg", {
|
|
287
|
+
viewBox: "0 0 24 24",
|
|
288
|
+
fill: "none",
|
|
289
|
+
"aria-hidden": "true",
|
|
290
|
+
width: "100%",
|
|
291
|
+
height: "100%",
|
|
292
|
+
children: [
|
|
293
|
+
withTrack && jsx("circle", {
|
|
294
|
+
cx: "12",
|
|
295
|
+
cy: "12",
|
|
296
|
+
r: "9",
|
|
297
|
+
stroke: "currentColor",
|
|
298
|
+
strokeWidth: strokeWidth,
|
|
299
|
+
strokeOpacity: SPINNER_TRACK_OPACITY,
|
|
300
|
+
fill: "none"
|
|
301
|
+
}),
|
|
302
|
+
jsx("path", {
|
|
303
|
+
d: "M21 12a9 9 0 0 0-9-9",
|
|
304
|
+
stroke: "currentColor",
|
|
305
|
+
strokeWidth: strokeWidth,
|
|
306
|
+
strokeLinecap: "round",
|
|
307
|
+
fill: "none"
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
}),
|
|
311
|
+
!isDecorative && jsx("span", {
|
|
312
|
+
className: "absolute -m-px h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)]",
|
|
313
|
+
children: label
|
|
314
|
+
})
|
|
315
|
+
]
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
Spinner.displayName = 'Spinner';
|
|
319
|
+
|
|
135
320
|
/**
|
|
136
321
|
* Tailwind-variants recipe for `<Button>`.
|
|
137
322
|
*
|
|
@@ -308,36 +493,13 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
308
493
|
});
|
|
309
494
|
|
|
310
495
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
496
|
+
* Map Button size to Spinner size. Button has 3 sizes (sm/md/lg);
|
|
497
|
+
* Spinner has 5 (xs/sm/md/lg/xl). Picking the closest match keeps
|
|
498
|
+
* the spinner visually proportional to the button label.
|
|
313
499
|
*
|
|
314
500
|
* @internal
|
|
315
|
-
*/ function
|
|
316
|
-
|
|
317
|
-
return jsxs("svg", {
|
|
318
|
-
"aria-hidden": "true",
|
|
319
|
-
width: dim,
|
|
320
|
-
height: dim,
|
|
321
|
-
viewBox: "0 0 24 24",
|
|
322
|
-
fill: "none",
|
|
323
|
-
className: "animate-spin",
|
|
324
|
-
children: [
|
|
325
|
-
jsx("circle", {
|
|
326
|
-
cx: "12",
|
|
327
|
-
cy: "12",
|
|
328
|
-
r: "9",
|
|
329
|
-
stroke: "currentColor",
|
|
330
|
-
strokeWidth: "2.5",
|
|
331
|
-
strokeOpacity: "0.25"
|
|
332
|
-
}),
|
|
333
|
-
jsx("path", {
|
|
334
|
-
d: "M21 12a9 9 0 0 0-9-9",
|
|
335
|
-
stroke: "currentColor",
|
|
336
|
-
strokeWidth: "2.5",
|
|
337
|
-
strokeLinecap: "round"
|
|
338
|
-
})
|
|
339
|
-
]
|
|
340
|
-
});
|
|
501
|
+
*/ function buttonSizeToSpinnerSize(size) {
|
|
502
|
+
return size === 'sm' ? 'xs' : size === 'lg' ? 'md' : 'sm';
|
|
341
503
|
}
|
|
342
504
|
/**
|
|
343
505
|
* Dashforge TW Button — action component with RBAC support.
|
|
@@ -381,8 +543,9 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
381
543
|
* ```
|
|
382
544
|
*/ const Button = /*#__PURE__*/ forwardRef(function Button(props, ref) {
|
|
383
545
|
var _rest_type;
|
|
384
|
-
const { access, disabled, variant, color, size, fullWidth, loading, asChild, sx, children } = props, rest = _object_without_properties_loose(props, [
|
|
546
|
+
const { access, visibleWhen, disabled, variant, color, size, fullWidth, loading, asChild, sx, children } = props, rest = _object_without_properties_loose(props, [
|
|
385
547
|
"access",
|
|
548
|
+
"visibleWhen",
|
|
386
549
|
"disabled",
|
|
387
550
|
"variant",
|
|
388
551
|
"color",
|
|
@@ -393,10 +556,17 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
393
556
|
"sx",
|
|
394
557
|
"children"
|
|
395
558
|
]);
|
|
396
|
-
//
|
|
559
|
+
// Bridge — both hooks called unconditionally (rules-of-hooks).
|
|
560
|
+
// Inside a `<DashForm>`, `useEngineVisibility` subscribes to engine
|
|
561
|
+
// state and re-evaluates the predicate on changes; outside a form,
|
|
562
|
+
// the predicate is called with `null` engine (consumer captures
|
|
563
|
+
// external state in the closure).
|
|
564
|
+
const bridge = useContext(DashFormContext);
|
|
565
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
397
566
|
const accessState = useAccessState(access);
|
|
398
|
-
// Early return for hidden state
|
|
399
|
-
|
|
567
|
+
// Early return for hidden state — either RBAC denies visibility or
|
|
568
|
+
// the engine-reactive predicate returned false.
|
|
569
|
+
if (!isVisible || !accessState.visible) return null;
|
|
400
570
|
// Effective disabled = explicit OR RBAC-disabled OR RBAC-readonly OR loading.
|
|
401
571
|
// Buttons do not support a true readonly state — RBAC readonly is mapped
|
|
402
572
|
// to disabled (matches the MUI-side semantics in @dashforge/ui).
|
|
@@ -435,8 +605,11 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
435
605
|
className: classes
|
|
436
606
|
}, rest, {
|
|
437
607
|
children: [
|
|
438
|
-
loading && jsx(
|
|
439
|
-
size: size != null ? size : 'md'
|
|
608
|
+
loading && jsx(Spinner, {
|
|
609
|
+
size: buttonSizeToSpinnerSize(size != null ? size : 'md'),
|
|
610
|
+
thickness: "thick",
|
|
611
|
+
withTrack: true,
|
|
612
|
+
label: ""
|
|
440
613
|
}),
|
|
441
614
|
children
|
|
442
615
|
]
|
|
@@ -2502,7 +2675,7 @@ function _object_without_properties_loose(source, excluded) {
|
|
|
2502
2675
|
'rounded-md border bg-neutral-50 border-neutral-300 shadow-lg',
|
|
2503
2676
|
'min-w-[var(--trigger-width)]'
|
|
2504
2677
|
],
|
|
2505
|
-
listBox: 'outline-none py-1',
|
|
2678
|
+
listBox: 'outline-none py-1 list-none pl-0 m-0',
|
|
2506
2679
|
listItem: [
|
|
2507
2680
|
'cursor-pointer select-none px-3 py-2 text-sm text-neutral-900',
|
|
2508
2681
|
'outline-none',
|
|
@@ -5425,7 +5598,7 @@ function formatRange(range, locale) {
|
|
|
5425
5598
|
'flex items-center gap-2 px-3 h-14 shrink-0',
|
|
5426
5599
|
'border-b border-neutral-200 text-neutral-900 font-semibold'
|
|
5427
5600
|
],
|
|
5428
|
-
list: 'flex flex-col gap-0.5 p-2 flex-1 overflow-y-auto',
|
|
5601
|
+
list: 'flex flex-col gap-0.5 p-2 flex-1 overflow-y-auto list-none m-0',
|
|
5429
5602
|
item: 'list-none',
|
|
5430
5603
|
itemLink: [
|
|
5431
5604
|
'flex items-center gap-2 px-2 py-2 rounded-md',
|
|
@@ -5458,7 +5631,7 @@ function formatRange(range, locale) {
|
|
|
5458
5631
|
'outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
|
|
5459
5632
|
'transition-colors w-full'
|
|
5460
5633
|
],
|
|
5461
|
-
groupChildren: 'flex flex-col gap-0.5 pl-6',
|
|
5634
|
+
groupChildren: 'flex flex-col gap-0.5 pl-6 list-none m-0',
|
|
5462
5635
|
footer: [
|
|
5463
5636
|
'border-t border-neutral-200 p-3 shrink-0',
|
|
5464
5637
|
'flex items-center gap-2'
|
|
@@ -6357,13 +6530,22 @@ const ConfirmContext = /*#__PURE__*/ createContext(null);
|
|
|
6357
6530
|
/**
|
|
6358
6531
|
* Tailwind-variants recipe for `<Snackbar>` (stacked toast surface).
|
|
6359
6532
|
*
|
|
6533
|
+
* **Sprint 4.4 refactor (1.1.0):** the severity color block has been
|
|
6534
|
+
* REMOVED from this recipe. The 3×4 (variant × severity) color matrix
|
|
6535
|
+
* now lives in `_shared/severity/severityVariants` and is consumed by
|
|
6536
|
+
* Snackbar AND Alert (and any future Banner). This recipe owns layout
|
|
6537
|
+
* + spacing + transitions only — the surface / border / icon colors
|
|
6538
|
+
* are merged in at render time via `cn()`.
|
|
6539
|
+
*
|
|
6360
6540
|
* Slots:
|
|
6361
6541
|
* - `container` — fixed-position outer stack
|
|
6362
|
-
* - `item` — single snackbar surface
|
|
6363
|
-
*
|
|
6542
|
+
* - `item` — single snackbar surface (layout only; severity
|
|
6543
|
+
* colors injected at render via `getSeverityClasses`)
|
|
6544
|
+
* - `icon` — leading severity icon (layout only; tone class
|
|
6545
|
+
* injected at render)
|
|
6364
6546
|
* - `message` — main text
|
|
6365
6547
|
* - `action` — optional action button
|
|
6366
|
-
* - `closeButton` — trailing `×` button
|
|
6548
|
+
* - `closeButton` — trailing close `×` button
|
|
6367
6549
|
*/ const snackbarVariants = tv({
|
|
6368
6550
|
slots: {
|
|
6369
6551
|
container: [
|
|
@@ -6374,28 +6556,33 @@ const ConfirmContext = /*#__PURE__*/ createContext(null);
|
|
|
6374
6556
|
'pointer-events-auto',
|
|
6375
6557
|
'flex items-start gap-3 px-4 py-3 w-full',
|
|
6376
6558
|
'rounded-lg border shadow-lg',
|
|
6377
|
-
'text-sm
|
|
6378
|
-
// Subtle enter transition — opacity
|
|
6379
|
-
//
|
|
6559
|
+
'text-sm',
|
|
6560
|
+
// Subtle enter transition — opacity-only, kept short so a burst
|
|
6561
|
+
// of snackbars feels snappy. Gated on motion-reduce
|
|
6380
6562
|
// (WCAG 2.3.3) — users who request reduced motion see snackbars
|
|
6381
|
-
// pop in instantly without the
|
|
6382
|
-
'transition-
|
|
6563
|
+
// pop in instantly without the fade.
|
|
6564
|
+
'transition-opacity duration-200 motion-reduce:transition-none motion-reduce:duration-0',
|
|
6383
6565
|
'data-[state=entered]:opacity-100 data-[state=exited]:opacity-0'
|
|
6384
6566
|
],
|
|
6385
|
-
icon: 'shrink-0 mt-0.5
|
|
6567
|
+
icon: 'shrink-0 mt-0.5 inline-flex items-center justify-center',
|
|
6386
6568
|
message: 'flex-1 min-w-0 leading-relaxed',
|
|
6387
6569
|
action: [
|
|
6388
6570
|
'shrink-0 inline-flex items-center justify-center px-2 h-7',
|
|
6389
6571
|
'rounded-md text-xs font-medium',
|
|
6390
|
-
|
|
6391
|
-
'
|
|
6392
|
-
|
|
6572
|
+
// Action button uses currentColor + a soft hover tint so it
|
|
6573
|
+
// adapts to whatever (variant, severity) surface it's on. The
|
|
6574
|
+
// hover tint relies on a 10% opacity overlay of the current
|
|
6575
|
+
// foreground — works in both light and dark severity surfaces.
|
|
6576
|
+
'opacity-90 hover:opacity-100 hover:bg-current/10',
|
|
6577
|
+
'outline-none focus-visible:ring-2 focus-visible:ring-current',
|
|
6578
|
+
'transition-opacity'
|
|
6393
6579
|
],
|
|
6394
6580
|
closeButton: [
|
|
6395
6581
|
'shrink-0 inline-flex items-center justify-center w-6 h-6',
|
|
6396
|
-
'rounded-md
|
|
6397
|
-
'
|
|
6398
|
-
'
|
|
6582
|
+
'rounded-md',
|
|
6583
|
+
'opacity-70 hover:opacity-100',
|
|
6584
|
+
'outline-none focus-visible:ring-2 focus-visible:ring-current',
|
|
6585
|
+
'transition-opacity'
|
|
6399
6586
|
]
|
|
6400
6587
|
},
|
|
6401
6588
|
variants: {
|
|
@@ -6418,32 +6605,208 @@ const ConfirmContext = /*#__PURE__*/ createContext(null);
|
|
|
6418
6605
|
'bottom-right': {
|
|
6419
6606
|
container: 'bottom-0 right-0 items-end'
|
|
6420
6607
|
}
|
|
6421
|
-
},
|
|
6422
|
-
severity: {
|
|
6423
|
-
info: {
|
|
6424
|
-
item: 'border-primary-200',
|
|
6425
|
-
icon: 'text-primary-600'
|
|
6426
|
-
},
|
|
6427
|
-
success: {
|
|
6428
|
-
item: 'border-success-200',
|
|
6429
|
-
icon: 'text-success-600'
|
|
6430
|
-
},
|
|
6431
|
-
warning: {
|
|
6432
|
-
item: 'border-warning-200',
|
|
6433
|
-
icon: 'text-warning-600'
|
|
6434
|
-
},
|
|
6435
|
-
danger: {
|
|
6436
|
-
item: 'border-danger-200',
|
|
6437
|
-
icon: 'text-danger-600'
|
|
6438
|
-
}
|
|
6439
6608
|
}
|
|
6440
6609
|
},
|
|
6441
6610
|
defaultVariants: {
|
|
6442
|
-
position: 'bottom-right'
|
|
6443
|
-
severity: 'info'
|
|
6611
|
+
position: 'bottom-right'
|
|
6444
6612
|
}
|
|
6445
6613
|
});
|
|
6446
6614
|
|
|
6615
|
+
/**
|
|
6616
|
+
* The 3×4 severity color matrix.
|
|
6617
|
+
*
|
|
6618
|
+
* Every class is a Tailwind utility that resolves through
|
|
6619
|
+
* `dashforgePreset()` to a CSS variable on the host — patchable at
|
|
6620
|
+
* runtime via `patchTheme({ <severity>: { …scale } })` without
|
|
6621
|
+
* recompiling. **Zero hardcoded hex values, zero `dark:` variants**:
|
|
6622
|
+
* the dashforge preset auto-inverts the neutral surface ladder via CSS
|
|
6623
|
+
* variable swap; the severity scales themselves do NOT auto-invert
|
|
6624
|
+
* (they read the same scale in light + dark, which is the desired
|
|
6625
|
+
* behaviour for semantic colors).
|
|
6626
|
+
*
|
|
6627
|
+
* The matrix is indexed `[variant][severity]` so a consumer-facing
|
|
6628
|
+
* `<Alert variant="filled" severity="warning">` resolves with a single
|
|
6629
|
+
* lookup.
|
|
6630
|
+
*/ const SEVERITY_MATRIX = {
|
|
6631
|
+
standard: {
|
|
6632
|
+
info: {
|
|
6633
|
+
surface: 'bg-info-50 text-info-900',
|
|
6634
|
+
border: 'border-info-100',
|
|
6635
|
+
icon: 'text-info-600'
|
|
6636
|
+
},
|
|
6637
|
+
success: {
|
|
6638
|
+
surface: 'bg-success-50 text-success-900',
|
|
6639
|
+
border: 'border-success-100',
|
|
6640
|
+
icon: 'text-success-600'
|
|
6641
|
+
},
|
|
6642
|
+
warning: {
|
|
6643
|
+
surface: 'bg-warning-50 text-warning-900',
|
|
6644
|
+
border: 'border-warning-100',
|
|
6645
|
+
icon: 'text-warning-600'
|
|
6646
|
+
},
|
|
6647
|
+
danger: {
|
|
6648
|
+
surface: 'bg-danger-50 text-danger-900',
|
|
6649
|
+
border: 'border-danger-100',
|
|
6650
|
+
icon: 'text-danger-600'
|
|
6651
|
+
}
|
|
6652
|
+
},
|
|
6653
|
+
filled: {
|
|
6654
|
+
info: {
|
|
6655
|
+
surface: 'bg-info-600 text-info-50',
|
|
6656
|
+
border: 'border-info-700',
|
|
6657
|
+
icon: 'text-info-50'
|
|
6658
|
+
},
|
|
6659
|
+
success: {
|
|
6660
|
+
surface: 'bg-success-600 text-success-50',
|
|
6661
|
+
border: 'border-success-700',
|
|
6662
|
+
icon: 'text-success-50'
|
|
6663
|
+
},
|
|
6664
|
+
warning: {
|
|
6665
|
+
surface: 'bg-warning-600 text-warning-50',
|
|
6666
|
+
border: 'border-warning-700',
|
|
6667
|
+
icon: 'text-warning-50'
|
|
6668
|
+
},
|
|
6669
|
+
danger: {
|
|
6670
|
+
surface: 'bg-danger-600 text-danger-50',
|
|
6671
|
+
border: 'border-danger-700',
|
|
6672
|
+
icon: 'text-danger-50'
|
|
6673
|
+
}
|
|
6674
|
+
},
|
|
6675
|
+
outlined: {
|
|
6676
|
+
info: {
|
|
6677
|
+
surface: 'bg-transparent text-info-700',
|
|
6678
|
+
border: 'border-info-300',
|
|
6679
|
+
icon: 'text-info-600'
|
|
6680
|
+
},
|
|
6681
|
+
success: {
|
|
6682
|
+
surface: 'bg-transparent text-success-700',
|
|
6683
|
+
border: 'border-success-300',
|
|
6684
|
+
icon: 'text-success-600'
|
|
6685
|
+
},
|
|
6686
|
+
warning: {
|
|
6687
|
+
surface: 'bg-transparent text-warning-700',
|
|
6688
|
+
border: 'border-warning-300',
|
|
6689
|
+
icon: 'text-warning-600'
|
|
6690
|
+
},
|
|
6691
|
+
danger: {
|
|
6692
|
+
surface: 'bg-transparent text-danger-700',
|
|
6693
|
+
border: 'border-danger-300',
|
|
6694
|
+
icon: 'text-danger-600'
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
};
|
|
6698
|
+
/**
|
|
6699
|
+
* Resolve the Tailwind classes for one (variant, severity) pair.
|
|
6700
|
+
*
|
|
6701
|
+
* @example
|
|
6702
|
+
* const { surface, border, icon } = getSeverityClasses('standard', 'warning');
|
|
6703
|
+
* // surface: 'bg-warning-50 text-warning-900'
|
|
6704
|
+
* // border: 'border-warning-100'
|
|
6705
|
+
* // icon: 'text-warning-600'
|
|
6706
|
+
*/ function getSeverityClasses(variant, severity) {
|
|
6707
|
+
return SEVERITY_MATRIX[variant][severity];
|
|
6708
|
+
}
|
|
6709
|
+
/**
|
|
6710
|
+
* ARIA role for a severity. Follows the W3C WAI-ARIA spec:
|
|
6711
|
+
* - `alert` for assertive states (warning / danger) — interrupts the
|
|
6712
|
+
* screen-reader announcement queue.
|
|
6713
|
+
* - `status` for polite states (info / success) — queued behind other
|
|
6714
|
+
* announcements.
|
|
6715
|
+
*
|
|
6716
|
+
* Consumers can override via an explicit `role` prop.
|
|
6717
|
+
*/ function getSeverityRole(severity) {
|
|
6718
|
+
return severity === 'warning' || severity === 'danger' ? 'alert' : 'status';
|
|
6719
|
+
}
|
|
6720
|
+
|
|
6721
|
+
const BASE_PROPS = {
|
|
6722
|
+
width: '1.25em',
|
|
6723
|
+
height: '1.25em',
|
|
6724
|
+
viewBox: '0 0 20 20',
|
|
6725
|
+
fill: 'none',
|
|
6726
|
+
stroke: 'currentColor',
|
|
6727
|
+
strokeWidth: 1.5,
|
|
6728
|
+
strokeLinecap: 'round',
|
|
6729
|
+
strokeLinejoin: 'round',
|
|
6730
|
+
'aria-hidden': 'true'
|
|
6731
|
+
};
|
|
6732
|
+
function InfoIcon(props) {
|
|
6733
|
+
return jsxs("svg", _extends({}, BASE_PROPS, props, {
|
|
6734
|
+
children: [
|
|
6735
|
+
jsx("circle", {
|
|
6736
|
+
cx: "10",
|
|
6737
|
+
cy: "10",
|
|
6738
|
+
r: "8.25"
|
|
6739
|
+
}),
|
|
6740
|
+
jsx("path", {
|
|
6741
|
+
d: "M10 6.5h.01M10 9.5v4"
|
|
6742
|
+
})
|
|
6743
|
+
]
|
|
6744
|
+
}));
|
|
6745
|
+
}
|
|
6746
|
+
function SuccessIcon(props) {
|
|
6747
|
+
return jsxs("svg", _extends({}, BASE_PROPS, props, {
|
|
6748
|
+
children: [
|
|
6749
|
+
jsx("circle", {
|
|
6750
|
+
cx: "10",
|
|
6751
|
+
cy: "10",
|
|
6752
|
+
r: "8.25"
|
|
6753
|
+
}),
|
|
6754
|
+
jsx("path", {
|
|
6755
|
+
d: "m6.5 10 2.5 2.5 4.5-5"
|
|
6756
|
+
})
|
|
6757
|
+
]
|
|
6758
|
+
}));
|
|
6759
|
+
}
|
|
6760
|
+
function WarningIcon(props) {
|
|
6761
|
+
return jsxs("svg", _extends({}, BASE_PROPS, props, {
|
|
6762
|
+
children: [
|
|
6763
|
+
jsx("path", {
|
|
6764
|
+
d: "M10 2.5 18 17H2L10 2.5Z"
|
|
6765
|
+
}),
|
|
6766
|
+
jsx("path", {
|
|
6767
|
+
d: "M10 8v3.5M10 14.5h.01"
|
|
6768
|
+
})
|
|
6769
|
+
]
|
|
6770
|
+
}));
|
|
6771
|
+
}
|
|
6772
|
+
function DangerIcon(props) {
|
|
6773
|
+
return jsxs("svg", _extends({}, BASE_PROPS, props, {
|
|
6774
|
+
children: [
|
|
6775
|
+
jsx("circle", {
|
|
6776
|
+
cx: "10",
|
|
6777
|
+
cy: "10",
|
|
6778
|
+
r: "8.25"
|
|
6779
|
+
}),
|
|
6780
|
+
jsx("path", {
|
|
6781
|
+
d: "m7 7 6 6M13 7l-6 6"
|
|
6782
|
+
})
|
|
6783
|
+
]
|
|
6784
|
+
}));
|
|
6785
|
+
}
|
|
6786
|
+
/**
|
|
6787
|
+
* Map of severity → default Icon component. Used by Alert (and
|
|
6788
|
+
* eventually Snackbar / Banner / etc.) to render the per-severity
|
|
6789
|
+
* default when the consumer doesn't pass a custom `icon` prop and
|
|
6790
|
+
* doesn't opt-out with `icon={false}`.
|
|
6791
|
+
*/ const SEVERITY_DEFAULT_ICON = {
|
|
6792
|
+
info: InfoIcon,
|
|
6793
|
+
success: SuccessIcon,
|
|
6794
|
+
warning: WarningIcon,
|
|
6795
|
+
danger: DangerIcon
|
|
6796
|
+
};
|
|
6797
|
+
/**
|
|
6798
|
+
* Convenience helper — render the default icon for a severity as a
|
|
6799
|
+
* `ReactNode`, ready to inject into a JSX slot.
|
|
6800
|
+
*
|
|
6801
|
+
* @example
|
|
6802
|
+
* <span className={severityClasses.icon}>
|
|
6803
|
+
* {getDefaultSeverityIcon('warning')}
|
|
6804
|
+
* </span>
|
|
6805
|
+
*/ function getDefaultSeverityIcon(severity) {
|
|
6806
|
+
const Icon = SEVERITY_DEFAULT_ICON[severity];
|
|
6807
|
+
return jsx(Icon, {});
|
|
6808
|
+
}
|
|
6809
|
+
|
|
6447
6810
|
const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
6448
6811
|
/**
|
|
6449
6812
|
* `useSnackbar()` — returns `{ enqueue, dismiss, dismissAll }`.
|
|
@@ -6457,17 +6820,6 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
|
6457
6820
|
}
|
|
6458
6821
|
return ctx;
|
|
6459
6822
|
}
|
|
6460
|
-
/**
|
|
6461
|
-
* Unicode glyphs for severities — keeps the bundle small (no icon dep)
|
|
6462
|
-
* and stays accessible because consumers pass their own message text.
|
|
6463
|
-
* The glyph is `aria-hidden`; the screen reader announces the message
|
|
6464
|
-
* via the live region.
|
|
6465
|
-
*/ const SEVERITY_GLYPH = {
|
|
6466
|
-
info: 'ⓘ',
|
|
6467
|
-
success: '✓',
|
|
6468
|
-
warning: '⚠',
|
|
6469
|
-
danger: '✕'
|
|
6470
|
-
};
|
|
6471
6823
|
/**
|
|
6472
6824
|
* Dashforge TW Snackbar provider — transient, stacked toast notifications.
|
|
6473
6825
|
*
|
|
@@ -6630,23 +6982,31 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
|
6630
6982
|
"aria-live": "polite",
|
|
6631
6983
|
"aria-atomic": "false",
|
|
6632
6984
|
children: visible.map((rec)=>{
|
|
6633
|
-
var _ref, _rec_severity, _ref1, _rec_showClose;
|
|
6985
|
+
var _ref, _rec_severity, _ref1, _rec_variant, _ref2, _rec_showClose;
|
|
6634
6986
|
var _slotProps_item, _slotProps_icon, _slotProps_message, _slotProps_action, _slotProps_closeButton;
|
|
6635
6987
|
const sev = (_ref = (_rec_severity = rec.severity) != null ? _rec_severity : defaults == null ? void 0 : defaults.severity) != null ? _ref : 'info';
|
|
6636
|
-
const
|
|
6988
|
+
const variant = (_ref1 = (_rec_variant = rec.variant) != null ? _rec_variant : defaults == null ? void 0 : defaults.variant) != null ? _ref1 : 'standard';
|
|
6989
|
+
const showClose = (_ref2 = (_rec_showClose = rec.showClose) != null ? _rec_showClose : defaults == null ? void 0 : defaults.showClose) != null ? _ref2 : true;
|
|
6637
6990
|
const itemClasses = snackbarVariants({
|
|
6638
|
-
position
|
|
6639
|
-
severity: sev
|
|
6991
|
+
position
|
|
6640
6992
|
});
|
|
6993
|
+
// Severity color classes — sourced from the shared 3×4 matrix
|
|
6994
|
+
// (`_shared/severity/`), same as Alert. Keeps the two
|
|
6995
|
+
// components visually in lockstep.
|
|
6996
|
+
const severityClasses = getSeverityClasses(variant, sev);
|
|
6997
|
+
// Icon resolution — same tristate as Alert:
|
|
6998
|
+
// undefined → default per-severity SVG (shared with Alert)
|
|
6999
|
+
// ReactNode → consumer's icon
|
|
7000
|
+
// false → no icon
|
|
7001
|
+
const renderedIcon = rec.icon === false ? null : rec.icon !== undefined ? rec.icon : getDefaultSeverityIcon(sev);
|
|
6641
7002
|
return jsxs("div", {
|
|
6642
7003
|
"data-state": "entered",
|
|
6643
7004
|
"data-tick": rec.tick,
|
|
6644
|
-
className: cn(itemClasses.item(), slotProps == null ? void 0 : (_slotProps_item = slotProps.item) == null ? void 0 : _slotProps_item.className),
|
|
7005
|
+
className: cn(itemClasses.item(), severityClasses.surface, severityClasses.border, slotProps == null ? void 0 : (_slotProps_item = slotProps.item) == null ? void 0 : _slotProps_item.className),
|
|
6645
7006
|
children: [
|
|
6646
|
-
jsx("span", {
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
children: SEVERITY_GLYPH[sev]
|
|
7007
|
+
renderedIcon !== null && jsx("span", {
|
|
7008
|
+
className: cn(itemClasses.icon(), severityClasses.icon, slotProps == null ? void 0 : (_slotProps_icon = slotProps.icon) == null ? void 0 : _slotProps_icon.className),
|
|
7009
|
+
children: renderedIcon
|
|
6650
7010
|
}),
|
|
6651
7011
|
jsx("div", {
|
|
6652
7012
|
className: cn(itemClasses.message(), slotProps == null ? void 0 : (_slotProps_message = slotProps.message) == null ? void 0 : _slotProps_message.className),
|
|
@@ -6667,7 +7027,20 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
|
6667
7027
|
"aria-label": "Dismiss notification",
|
|
6668
7028
|
onClick: ()=>dismiss(rec.id),
|
|
6669
7029
|
className: cn(itemClasses.closeButton(), slotProps == null ? void 0 : (_slotProps_closeButton = slotProps.closeButton) == null ? void 0 : _slotProps_closeButton.className),
|
|
6670
|
-
children: "
|
|
7030
|
+
children: jsx("svg", {
|
|
7031
|
+
width: "1em",
|
|
7032
|
+
height: "1em",
|
|
7033
|
+
viewBox: "0 0 20 20",
|
|
7034
|
+
fill: "none",
|
|
7035
|
+
stroke: "currentColor",
|
|
7036
|
+
strokeWidth: 1.5,
|
|
7037
|
+
strokeLinecap: "round",
|
|
7038
|
+
strokeLinejoin: "round",
|
|
7039
|
+
"aria-hidden": "true",
|
|
7040
|
+
children: jsx("path", {
|
|
7041
|
+
d: "m6 6 8 8M14 6l-8 8"
|
|
7042
|
+
})
|
|
7043
|
+
})
|
|
6671
7044
|
})
|
|
6672
7045
|
]
|
|
6673
7046
|
}, rec.id);
|
|
@@ -6678,277 +7051,1446 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
|
|
|
6678
7051
|
}
|
|
6679
7052
|
|
|
6680
7053
|
/**
|
|
6681
|
-
*
|
|
7054
|
+
* Tailwind-variants recipe for `<Alert>`.
|
|
6682
7055
|
*
|
|
6683
|
-
*
|
|
6684
|
-
*
|
|
6685
|
-
*
|
|
6686
|
-
*
|
|
6687
|
-
*
|
|
6688
|
-
*
|
|
7056
|
+
* Slots:
|
|
7057
|
+
* - `root` — outer surface, severity + variant color classes
|
|
7058
|
+
* come from `_shared/severity/severityVariants`
|
|
7059
|
+
* (NOT baked into this recipe, to keep the matrix
|
|
7060
|
+
* shared across Alert / Snackbar / future Banner)
|
|
7061
|
+
* - `icon` — leading severity icon container
|
|
7062
|
+
* - `titleSlot` — `<AlertTitle>` rendered as the first content row
|
|
7063
|
+
* - `content` — body text container
|
|
7064
|
+
* - `action` — trailing action / close button container
|
|
7065
|
+
* - `closeButton` — built-in close `×` (when `onClose` is defined)
|
|
7066
|
+
*
|
|
7067
|
+
* The severity color classes are merged at render time via `cn()` in
|
|
7068
|
+
* `Alert.tsx` — see the call sites of `getSeverityClasses(variant,
|
|
7069
|
+
* severity)`. This recipe owns layout + spacing + typography only.
|
|
7070
|
+
*/ const alertVariants = tv({
|
|
7071
|
+
slots: {
|
|
7072
|
+
root: [
|
|
7073
|
+
'relative flex items-start gap-3',
|
|
7074
|
+
'px-4 py-3',
|
|
7075
|
+
'rounded-lg border',
|
|
7076
|
+
'text-sm',
|
|
7077
|
+
// Smooth visibility transition (opacity only) — matches the
|
|
7078
|
+
// Snackbar entry timing, gated on motion-reduce per WCAG 2.3.3.
|
|
7079
|
+
'transition-opacity duration-150 motion-reduce:transition-none motion-reduce:duration-0'
|
|
7080
|
+
],
|
|
7081
|
+
icon: [
|
|
7082
|
+
'shrink-0 mt-0.5',
|
|
7083
|
+
'inline-flex items-center justify-center'
|
|
7084
|
+
],
|
|
7085
|
+
titleSlot: [
|
|
7086
|
+
'block font-medium leading-snug mb-1'
|
|
7087
|
+
],
|
|
7088
|
+
content: [
|
|
7089
|
+
'flex-1 min-w-0',
|
|
7090
|
+
'leading-relaxed'
|
|
7091
|
+
],
|
|
7092
|
+
action: [
|
|
7093
|
+
'shrink-0 inline-flex items-center',
|
|
7094
|
+
'ml-auto'
|
|
7095
|
+
],
|
|
7096
|
+
closeButton: [
|
|
7097
|
+
'shrink-0 inline-flex items-center justify-center w-6 h-6',
|
|
7098
|
+
'rounded-md',
|
|
7099
|
+
'opacity-70 hover:opacity-100',
|
|
7100
|
+
'outline-none focus-visible:ring-2 focus-visible:ring-current',
|
|
7101
|
+
'transition-opacity'
|
|
7102
|
+
]
|
|
7103
|
+
},
|
|
7104
|
+
variants: {
|
|
7105
|
+
/**
|
|
7106
|
+
* Layout density. Defaults to comfortable spacing; `compact`
|
|
7107
|
+
* tightens the vertical padding for inline contexts (form
|
|
7108
|
+
* validation summary at the top of a tight modal, etc.).
|
|
7109
|
+
*/ density: {
|
|
7110
|
+
comfortable: {
|
|
7111
|
+
root: 'px-4 py-3'
|
|
7112
|
+
},
|
|
7113
|
+
compact: {
|
|
7114
|
+
root: 'px-3 py-2 text-[13px]'
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
},
|
|
7118
|
+
defaultVariants: {
|
|
7119
|
+
density: 'comfortable'
|
|
7120
|
+
}
|
|
7121
|
+
});
|
|
7122
|
+
|
|
7123
|
+
/**
|
|
7124
|
+
* `<AlertTitle>` — optional title block, rendered as the first child
|
|
7125
|
+
* of `<Alert>`. Pattern mirrors MUI exactly (sub-component, not a
|
|
7126
|
+
* prop), so the JSX shape ports 1:1 from `@mui/material`.
|
|
6689
7127
|
*
|
|
6690
|
-
*
|
|
6691
|
-
*
|
|
6692
|
-
*
|
|
6693
|
-
*
|
|
7128
|
+
* @example
|
|
7129
|
+
* <Alert severity="warning">
|
|
7130
|
+
* <AlertTitle>Subscription expires soon</AlertTitle>
|
|
7131
|
+
* Renew before 2026-06-20 to keep access.
|
|
7132
|
+
* </Alert>
|
|
7133
|
+
*/ function AlertTitle({ children, className }) {
|
|
7134
|
+
const { titleSlot } = alertVariants();
|
|
7135
|
+
return jsx("div", {
|
|
7136
|
+
className: cn(titleSlot(), className),
|
|
7137
|
+
children: children
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
/**
|
|
7141
|
+
* `<Alert>` — inline persistent status surface.
|
|
6694
7142
|
*
|
|
6695
|
-
*
|
|
6696
|
-
*
|
|
6697
|
-
*
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
});
|
|
7143
|
+
* **API**: full MUI Alert parity (`severity`, `variant`, `icon`,
|
|
7144
|
+
* `onClose`, `action`, `role`, `<AlertTitle>` sub-component). One
|
|
7145
|
+
* deliberate divergence: `severity="danger"` instead of MUI's
|
|
7146
|
+
* `severity="error"` — aligned to the `danger.*` token palette.
|
|
7147
|
+
*
|
|
7148
|
+
* **Bridge integration** (Dashforge extras):
|
|
7149
|
+
* - `visibleWhen` — engine-reactive predicate (re-evaluated on
|
|
7150
|
+
* every engine state change when mounted inside a `<DashForm>`)
|
|
7151
|
+
* - `access` — RBAC gating via `@dashforge/rbac`
|
|
7152
|
+
*
|
|
7153
|
+
* **Token-driven**: every color class flows from `dashforgePreset()`
|
|
7154
|
+
* via CSS variables. No hardcoded hex, no `dark:` variants. The
|
|
7155
|
+
* 3×4 color matrix lives in `_shared/severity/severityVariants.ts`
|
|
7156
|
+
* and is shared with the refactored `<Snackbar>` (and future Banner).
|
|
7157
|
+
*
|
|
7158
|
+
* **A11y**: `role="alert"` for assertive severities (warning, danger)
|
|
7159
|
+
* and `role="status"` for polite ones (info, success). Override via
|
|
7160
|
+
* the `role` prop. The icon is `aria-hidden` — the message text is
|
|
7161
|
+
* the semantic carrier.
|
|
7162
|
+
*
|
|
7163
|
+
* @example
|
|
7164
|
+
* // Default — soft tinted surface, default icon
|
|
7165
|
+
* <Alert severity="info">New feature: export to CSV is now in beta.</Alert>
|
|
7166
|
+
*
|
|
7167
|
+
* // With title (sub-component) + close button
|
|
7168
|
+
* <Alert severity="warning" onClose={dismiss}>
|
|
7169
|
+
* <AlertTitle>Subscription expires soon</AlertTitle>
|
|
7170
|
+
* Your Pro plan ends in 3 days.
|
|
7171
|
+
* </Alert>
|
|
7172
|
+
*
|
|
7173
|
+
* // Custom icon
|
|
7174
|
+
* import { Bell } from 'lucide-react';
|
|
7175
|
+
* <Alert severity="info" icon={<Bell size={20} />}>3 new notifications</Alert>
|
|
7176
|
+
*
|
|
7177
|
+
* // No icon — colored surface alone carries the severity
|
|
7178
|
+
* <Alert severity="success" icon={false}>Saved 2 minutes ago.</Alert>
|
|
7179
|
+
*
|
|
7180
|
+
* // Custom action — CTA replaces the auto-close button
|
|
7181
|
+
* <Alert severity="warning" action={<button onClick={renew}>Renew</button>}>
|
|
7182
|
+
* Trial expires in 3 days.
|
|
7183
|
+
* </Alert>
|
|
7184
|
+
*
|
|
7185
|
+
* // Filled variant — for high-priority callouts
|
|
7186
|
+
* <Alert severity="danger" variant="filled">
|
|
7187
|
+
* Couldn't save — 3 fields below contain validation errors.
|
|
7188
|
+
* </Alert>
|
|
7189
|
+
*/ function Alert({ severity, variant = 'standard', children, icon: iconProp, onClose, closeText = 'Close', action, role: roleProp, visibleWhen, access, sx, className, slotProps }) {
|
|
7190
|
+
var _slotProps_root, _slotProps_icon, _slotProps_content, _slotProps_action, _slotProps_closeButton;
|
|
7191
|
+
// Bridge — both hooks safely return defaults outside a DashForm /
|
|
7192
|
+
// RbacProvider, matching the TextField pattern.
|
|
7193
|
+
const bridge = useContext(DashFormContext);
|
|
7194
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
7195
|
+
const accessState = useAccessState(access);
|
|
7196
|
+
if (!isVisible || !accessState.visible) return null;
|
|
7197
|
+
const v = alertVariants();
|
|
7198
|
+
const severityClasses = getSeverityClasses(variant, severity);
|
|
7199
|
+
const role = roleProp != null ? roleProp : getSeverityRole(severity);
|
|
7200
|
+
// Icon resolution:
|
|
7201
|
+
// - false → no icon
|
|
7202
|
+
// - undefined → default per severity (lib's inline SVG)
|
|
7203
|
+
// - ReactNode → consumer's custom icon
|
|
7204
|
+
const renderedIcon = iconProp === false ? null : iconProp !== undefined ? iconProp : getDefaultSeverityIcon(severity);
|
|
7205
|
+
return jsxs("div", _extends({
|
|
7206
|
+
role: role,
|
|
7207
|
+
className: cn(v.root(), severityClasses.surface, severityClasses.border, sx, className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className)
|
|
7208
|
+
}, slotProps == null ? void 0 : slotProps.root, {
|
|
7209
|
+
children: [
|
|
7210
|
+
renderedIcon !== null && jsx("span", _extends({
|
|
7211
|
+
className: cn(v.icon(), severityClasses.icon, slotProps == null ? void 0 : (_slotProps_icon = slotProps.icon) == null ? void 0 : _slotProps_icon.className)
|
|
7212
|
+
}, slotProps == null ? void 0 : slotProps.icon, {
|
|
7213
|
+
children: renderedIcon
|
|
7214
|
+
})),
|
|
7215
|
+
jsx("div", _extends({
|
|
7216
|
+
className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className)
|
|
7217
|
+
}, slotProps == null ? void 0 : slotProps.content, {
|
|
7218
|
+
children: children
|
|
7219
|
+
})),
|
|
7220
|
+
action ? jsx("div", _extends({
|
|
7221
|
+
className: cn(v.action(), slotProps == null ? void 0 : (_slotProps_action = slotProps.action) == null ? void 0 : _slotProps_action.className)
|
|
7222
|
+
}, slotProps == null ? void 0 : slotProps.action, {
|
|
7223
|
+
children: action
|
|
7224
|
+
})) : onClose ? jsx("button", _extends({
|
|
7225
|
+
type: "button",
|
|
7226
|
+
"aria-label": closeText,
|
|
7227
|
+
onClick: onClose,
|
|
7228
|
+
className: cn(v.closeButton(), slotProps == null ? void 0 : (_slotProps_closeButton = slotProps.closeButton) == null ? void 0 : _slotProps_closeButton.className)
|
|
7229
|
+
}, slotProps == null ? void 0 : slotProps.closeButton, {
|
|
7230
|
+
children: jsx("svg", {
|
|
7231
|
+
width: "1em",
|
|
7232
|
+
height: "1em",
|
|
7233
|
+
viewBox: "0 0 20 20",
|
|
7234
|
+
fill: "none",
|
|
7235
|
+
stroke: "currentColor",
|
|
7236
|
+
strokeWidth: 1.5,
|
|
7237
|
+
strokeLinecap: "round",
|
|
7238
|
+
strokeLinejoin: "round",
|
|
7239
|
+
"aria-hidden": "true",
|
|
7240
|
+
children: jsx("path", {
|
|
7241
|
+
d: "m6 6 8 8M14 6l-8 8"
|
|
7242
|
+
})
|
|
7243
|
+
})
|
|
7244
|
+
})) : null
|
|
7245
|
+
]
|
|
7246
|
+
}));
|
|
7247
|
+
}
|
|
7248
|
+
|
|
7249
|
+
const ICON_BUTTON_SIZE_OVERRIDES = {
|
|
7250
|
+
sm: 'w-8 px-0',
|
|
7251
|
+
md: 'w-10 px-0',
|
|
7252
|
+
lg: 'w-12 px-0'
|
|
7253
|
+
};
|
|
7254
|
+
/** Always-applied layout primitives — not token-driven (geometric CSS). */ const ICON_BUTTON_BASE = 'aspect-square inline-flex items-center justify-center';
|
|
6799
7255
|
|
|
6800
7256
|
/**
|
|
6801
|
-
*
|
|
6802
|
-
*
|
|
6803
|
-
* caption/overline get `<span>` (inline, no implicit block break).
|
|
7257
|
+
* Map IconButton size to Spinner size. Same mapping as Button —
|
|
7258
|
+
* Spinner has 5 steps, the parent has 3.
|
|
6804
7259
|
*
|
|
6805
|
-
*
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
h1: 'h1',
|
|
6810
|
-
h2: 'h2',
|
|
6811
|
-
h3: 'h3',
|
|
6812
|
-
h4: 'h4',
|
|
6813
|
-
h5: 'h5',
|
|
6814
|
-
h6: 'h6',
|
|
6815
|
-
subtitle1: 'p',
|
|
6816
|
-
subtitle2: 'p',
|
|
6817
|
-
body1: 'p',
|
|
6818
|
-
body2: 'p',
|
|
6819
|
-
caption: 'span',
|
|
6820
|
-
overline: 'span'
|
|
6821
|
-
};
|
|
7260
|
+
* @internal
|
|
7261
|
+
*/ function iconButtonSizeToSpinnerSize(size) {
|
|
7262
|
+
return size === 'sm' ? 'xs' : size === 'lg' ? 'md' : 'sm';
|
|
7263
|
+
}
|
|
6822
7264
|
/**
|
|
6823
|
-
*
|
|
6824
|
-
* surface in @dashforge/tw.
|
|
7265
|
+
* Dashforge TW IconButton — square, icon-only action button.
|
|
6825
7266
|
*
|
|
6826
|
-
*
|
|
6827
|
-
*
|
|
6828
|
-
*
|
|
6829
|
-
* intent-coloured palette to the consumer. That's fine for one-off
|
|
6830
|
-
* marketing surfaces, but at app scale it means every `<h2>` and every
|
|
6831
|
-
* body paragraph re-derives its own utility chain — and they drift.
|
|
7267
|
+
* Reuses `buttonVariants` 1:1 (same `variant` / `color` / `size` /
|
|
7268
|
+
* `loading` axes) so every `<Button>` style maps to the equivalent
|
|
7269
|
+
* `<IconButton>` with zero code duplication. The ONLY additions are:
|
|
6832
7270
|
*
|
|
6833
|
-
*
|
|
6834
|
-
*
|
|
6835
|
-
*
|
|
6836
|
-
*
|
|
6837
|
-
* semantic layer follows the visual layer; `as` and `asChild` are the
|
|
6838
|
-
* two escape hatches when you need something else.
|
|
7271
|
+
* - `ICON_BUTTON_SIZE_OVERRIDES` — forces `w-N + px-0` so the button
|
|
7272
|
+
* is square at every size token
|
|
7273
|
+
* - `ICON_BUTTON_BASE` — `aspect-square inline-flex items-center
|
|
7274
|
+
* justify-center` for layout
|
|
6839
7275
|
*
|
|
6840
|
-
*
|
|
6841
|
-
*
|
|
6842
|
-
* label, `<TextField>`'s helper text, MDX prose in our own docs.
|
|
6843
|
-
* • Composes ON TOP of `@dashforge/tw-tokens` colour scales (so
|
|
6844
|
-
* `color="primary"` paints `text-primary-700` in light, `-400` in dark,
|
|
6845
|
-
* reactive to `setMode()`).
|
|
6846
|
-
* • Polymorphic via Radix Slot — pairs cleanly with router `<Link>`,
|
|
6847
|
-
* `<button>`, or `<a>` without injecting an extra wrapper element.
|
|
7276
|
+
* Both additions are token-driven (spacing) or layout-primitive
|
|
7277
|
+
* (geometry) — no hardcoded hex, no `dark:` variants.
|
|
6848
7278
|
*
|
|
6849
|
-
*
|
|
6850
|
-
*
|
|
6851
|
-
*
|
|
6852
|
-
*
|
|
6853
|
-
*
|
|
6854
|
-
*
|
|
6855
|
-
*
|
|
6856
|
-
*
|
|
6857
|
-
*
|
|
6858
|
-
*
|
|
6859
|
-
*
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
7279
|
+
* **`aria-label` is REQUIRED** at the type level — icon-only buttons
|
|
7280
|
+
* are invisible to assistive tech without an explicit accessible
|
|
7281
|
+
* name. The TypeScript compiler enforces this rather than relying on
|
|
7282
|
+
* runtime warnings.
|
|
7283
|
+
*
|
|
7284
|
+
* RBAC integration mirrors `<Button>`: `access` with
|
|
7285
|
+
* `onUnauthorized: 'hide' | 'disable' | 'readonly'`. No `visibleWhen`
|
|
7286
|
+
* (mirror Button — buttons should disable, not hide, on
|
|
7287
|
+
* state-driven unavailability; permission-driven hiding is `access`).
|
|
7288
|
+
*
|
|
7289
|
+
* @example
|
|
7290
|
+
* ```tsx
|
|
7291
|
+
* import { Trash2 } from 'lucide-react';
|
|
7292
|
+
*
|
|
7293
|
+
* <IconButton aria-label="Delete row" color="danger" onClick={onDelete}>
|
|
7294
|
+
* <Trash2 size={20} />
|
|
7295
|
+
* </IconButton>
|
|
7296
|
+
*
|
|
7297
|
+
* // Polymorphism via Radix Slot
|
|
7298
|
+
* <IconButton asChild aria-label="Open settings" variant="ghost">
|
|
7299
|
+
* <Link to="/settings"><SettingsIcon size={20} /></Link>
|
|
7300
|
+
* </IconButton>
|
|
7301
|
+
*
|
|
7302
|
+
* // RBAC-gated
|
|
7303
|
+
* <IconButton
|
|
7304
|
+
* aria-label="Publish article"
|
|
7305
|
+
* variant="solid"
|
|
7306
|
+
* color="primary"
|
|
7307
|
+
* access={{ resource: 'article', action: 'publish', onUnauthorized: 'disable' }}
|
|
7308
|
+
* >
|
|
7309
|
+
* <PublishIcon size={20} />
|
|
7310
|
+
* </IconButton>
|
|
7311
|
+
*
|
|
7312
|
+
* // Loading swap
|
|
7313
|
+
* <IconButton aria-label="Save" loading={isSaving}>
|
|
7314
|
+
* <SaveIcon size={20} />
|
|
7315
|
+
* </IconButton>
|
|
7316
|
+
* ```
|
|
7317
|
+
*/ const IconButton = /*#__PURE__*/ forwardRef(function IconButton(props, ref) {
|
|
7318
|
+
var _rest_type;
|
|
7319
|
+
const { access, visibleWhen, disabled, variant, color, size = 'md', loading, asChild, sx, className, children } = props, rest = _object_without_properties_loose(props, [
|
|
7320
|
+
"access",
|
|
7321
|
+
"visibleWhen",
|
|
7322
|
+
"disabled",
|
|
6863
7323
|
"variant",
|
|
6864
7324
|
"color",
|
|
6865
|
-
"
|
|
6866
|
-
"
|
|
6867
|
-
"truncate",
|
|
6868
|
-
"noWrap",
|
|
6869
|
-
"gutterBottom",
|
|
6870
|
-
"as",
|
|
7325
|
+
"size",
|
|
7326
|
+
"loading",
|
|
6871
7327
|
"asChild",
|
|
6872
7328
|
"sx",
|
|
7329
|
+
"className",
|
|
6873
7330
|
"children"
|
|
6874
7331
|
]);
|
|
6875
|
-
|
|
7332
|
+
// Bridge — both hooks called unconditionally (rules-of-hooks).
|
|
7333
|
+
const bridge = useContext(DashFormContext);
|
|
7334
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
7335
|
+
const accessState = useAccessState(access);
|
|
7336
|
+
if (!isVisible || !accessState.visible) return null;
|
|
7337
|
+
// Effective disabled = explicit OR RBAC.disabled OR RBAC.readonly OR loading.
|
|
7338
|
+
// Mirrors `<Button>` exactly — IconButton has no true read-only
|
|
7339
|
+
// state, so RBAC readonly maps to disabled (same as MUI side).
|
|
7340
|
+
const effectiveDisabled = Boolean(disabled) || accessState.disabled || accessState.readonly || Boolean(loading);
|
|
7341
|
+
const classes = cn(buttonVariants({
|
|
6876
7342
|
variant,
|
|
6877
7343
|
color,
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
// asChild wins over `as` when both are passed (see component header).
|
|
7344
|
+
size,
|
|
7345
|
+
loading
|
|
7346
|
+
}), ICON_BUTTON_SIZE_OVERRIDES[size], ICON_BUTTON_BASE, sx, className);
|
|
7347
|
+
// `aria-busy` while loading — distinguishes "wait for action to
|
|
7348
|
+
// finish" from "you can't do this" in screen-reader announcements.
|
|
7349
|
+
const ariaBusy = loading ? true : undefined;
|
|
6885
7350
|
if (asChild) {
|
|
6886
|
-
return jsx(Slot,
|
|
7351
|
+
return jsx(Slot, {
|
|
6887
7352
|
ref: ref,
|
|
6888
|
-
className: classes
|
|
6889
|
-
|
|
7353
|
+
className: classes,
|
|
7354
|
+
"data-disabled": effectiveDisabled || undefined,
|
|
7355
|
+
"aria-disabled": effectiveDisabled || undefined,
|
|
7356
|
+
"aria-busy": ariaBusy,
|
|
6890
7357
|
children: children
|
|
6891
|
-
})
|
|
7358
|
+
});
|
|
6892
7359
|
}
|
|
6893
|
-
|
|
6894
|
-
const Tag = (_ref = as != null ? as : VARIANT_TO_TAG[variant]) != null ? _ref : 'span';
|
|
6895
|
-
return jsx(Tag, _extends({
|
|
7360
|
+
return jsx("button", _extends({
|
|
6896
7361
|
ref: ref,
|
|
7362
|
+
type: (_rest_type = rest.type) != null ? _rest_type : 'button',
|
|
7363
|
+
disabled: effectiveDisabled,
|
|
7364
|
+
"aria-busy": ariaBusy,
|
|
6897
7365
|
className: classes
|
|
6898
7366
|
}, rest, {
|
|
6899
|
-
children:
|
|
7367
|
+
children: loading ? jsx(Spinner, {
|
|
7368
|
+
size: iconButtonSizeToSpinnerSize(size),
|
|
7369
|
+
thickness: "thick",
|
|
7370
|
+
withTrack: true,
|
|
7371
|
+
label: ""
|
|
7372
|
+
}) : children
|
|
6900
7373
|
}));
|
|
6901
7374
|
});
|
|
6902
|
-
Typography.displayName = 'Typography';
|
|
6903
7375
|
|
|
6904
7376
|
/**
|
|
6905
|
-
*
|
|
6906
|
-
*
|
|
6907
|
-
* Architectural choice (planned with the user — F9 deep dive):
|
|
6908
|
-
*
|
|
6909
|
-
* Box replaces FOUR overlapping concepts from MUI in one component:
|
|
6910
|
-
* • Box (typed div)
|
|
6911
|
-
* • Paper (surface with elevation)
|
|
6912
|
-
* • Card (Paper specialisation)
|
|
6913
|
-
* • Surface (Joy UI's outlined / soft / solid / plain variants)
|
|
6914
|
-
*
|
|
6915
|
-
* The reason for the consolidation: in MUI you have to compose two or
|
|
6916
|
-
* three of these to express even basic intent ("an outlined card with
|
|
6917
|
-
* warning tone"). Here, one `<Box variant="outlined" color="warning">`
|
|
6918
|
-
* says exactly that.
|
|
6919
|
-
*
|
|
6920
|
-
* Variant taxonomy (5 axes, intentionally non-overlapping with Stack/Grid):
|
|
7377
|
+
* Tailwind-variants recipe for the Menu family.
|
|
6921
7378
|
*
|
|
6922
|
-
*
|
|
6923
|
-
*
|
|
6924
|
-
*
|
|
6925
|
-
* • soft — semi-transparent intent bg + intent text. The "callout".
|
|
6926
|
-
* • solid — solid intent bg + contrasting text. The "CTA banner".
|
|
7379
|
+
* The whole menu family is styled here in one TV instance so the
|
|
7380
|
+
* compound rules (item color × selected × danger, etc.) live in
|
|
7381
|
+
* one source of truth.
|
|
6927
7382
|
*
|
|
6928
|
-
*
|
|
6929
|
-
*
|
|
6930
|
-
*
|
|
7383
|
+
* Slots:
|
|
7384
|
+
* - `content` — floating panel (DropdownMenu.Content)
|
|
7385
|
+
* - `item` — clickable row (DropdownMenu.Item)
|
|
7386
|
+
* - `itemIcon` — leading icon container
|
|
7387
|
+
* - `itemLabel` — text content slot
|
|
7388
|
+
* - `itemEnd` — trailing slot (shortcut hint, chevron, …)
|
|
7389
|
+
* - `label` — non-interactive heading (DropdownMenu.Label)
|
|
7390
|
+
* - `separator` — divider (DropdownMenu.Separator)
|
|
7391
|
+
* - `skeleton` — placeholder item (visual only, no Radix slot)
|
|
7392
|
+
*/ const menuVariants = tv({
|
|
7393
|
+
slots: {
|
|
7394
|
+
content: [
|
|
7395
|
+
'z-50 min-w-[180px] py-1.5',
|
|
7396
|
+
// Surface elevation: `bg-white` is hardcoded (does NOT
|
|
7397
|
+
// auto-invert via the dashforgePreset CSS-var swap), so we
|
|
7398
|
+
// pair it with `dark:bg-neutral-100` (target #171717 — one
|
|
7399
|
+
// elevation tier above page surface in dark mode). This is
|
|
7400
|
+
// Category B of THEME-AUDIT.md — must stay on the same
|
|
7401
|
+
// string literal so the whitelist regex matches.
|
|
7402
|
+
'rounded-md border border-neutral-200 bg-white dark:bg-neutral-100 shadow-lg',
|
|
7403
|
+
// Smooth fade-in via Radix's data-state attributes. Motion-
|
|
7404
|
+
// reduce gated.
|
|
7405
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
7406
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
7407
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
7408
|
+
'motion-reduce:animate-none motion-reduce:transition-none',
|
|
7409
|
+
// List reset (defensive — same defensive pattern as
|
|
7410
|
+
// Autocomplete listbox after the listbox-bullets fix in 1.0.1).
|
|
7411
|
+
'list-none pl-0 m-0'
|
|
7412
|
+
],
|
|
7413
|
+
item: [
|
|
7414
|
+
'group relative flex cursor-pointer select-none items-center gap-2',
|
|
7415
|
+
'px-3 py-1.5 mx-1 my-0.5',
|
|
7416
|
+
'rounded-md text-sm text-neutral-900',
|
|
7417
|
+
'outline-none',
|
|
7418
|
+
// Radix sets data-highlighted when the item is keyboard-focused
|
|
7419
|
+
// OR hovered. We anchor the active visual state to that single
|
|
7420
|
+
// attribute so keyboard + mouse interactions look identical.
|
|
7421
|
+
'data-[highlighted]:bg-neutral-100',
|
|
7422
|
+
// Disabled (Radix sets data-disabled).
|
|
7423
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none',
|
|
7424
|
+
'transition-colors duration-100',
|
|
7425
|
+
'motion-reduce:transition-none'
|
|
7426
|
+
],
|
|
7427
|
+
itemIcon: 'shrink-0 inline-flex items-center justify-center w-4 h-4',
|
|
7428
|
+
itemLabel: 'flex-1 truncate',
|
|
7429
|
+
itemEnd: 'shrink-0 ml-3 inline-flex items-center text-xs text-neutral-500',
|
|
7430
|
+
label: [
|
|
7431
|
+
'px-3 py-1 mx-1 mt-1 text-xs font-medium uppercase tracking-wide',
|
|
7432
|
+
'text-neutral-500'
|
|
7433
|
+
],
|
|
7434
|
+
separator: 'h-px my-1 mx-1 bg-neutral-200',
|
|
7435
|
+
skeleton: [
|
|
7436
|
+
'flex items-center gap-2 px-3 py-1.5 mx-1 my-0.5',
|
|
7437
|
+
'animate-pulse motion-reduce:animate-none'
|
|
7438
|
+
]
|
|
7439
|
+
},
|
|
7440
|
+
variants: {
|
|
7441
|
+
/**
|
|
7442
|
+
* Item color intent. `'danger'` highlights destructive actions
|
|
7443
|
+
* (Delete, Sign out) with red bg on hover. Both bg and text
|
|
7444
|
+
* shift to the danger palette when the item is highlighted.
|
|
7445
|
+
*/ color: {
|
|
7446
|
+
default: {},
|
|
7447
|
+
danger: {
|
|
7448
|
+
item: [
|
|
7449
|
+
'text-danger-700',
|
|
7450
|
+
// Hover + keyboard focus → red soft bg + saturated text.
|
|
7451
|
+
'data-[highlighted]:bg-danger-50 data-[highlighted]:text-danger-800',
|
|
7452
|
+
// Dark mode: bg-danger-50 doesn't auto-invert (color
|
|
7453
|
+
// palettes don't), so we add the explicit dark variant.
|
|
7454
|
+
// The `text-danger-700` base also gets a dark counterpart
|
|
7455
|
+
// for readability on the dark surface.
|
|
7456
|
+
'dark:text-danger-300',
|
|
7457
|
+
'dark:data-[highlighted]:bg-danger-950 dark:data-[highlighted]:text-danger-200'
|
|
7458
|
+
],
|
|
7459
|
+
itemIcon: 'text-danger-600 dark:text-danger-400'
|
|
7460
|
+
}
|
|
7461
|
+
},
|
|
7462
|
+
/**
|
|
7463
|
+
* Visual `selected` hint — adds a faint primary bg + the
|
|
7464
|
+
* leading edge gets a primary accent. Pure visual; v1 has no
|
|
7465
|
+
* controlled selection state machine.
|
|
7466
|
+
*/ selected: {
|
|
7467
|
+
true: {
|
|
7468
|
+
item: [
|
|
7469
|
+
'bg-primary-50 text-primary-900',
|
|
7470
|
+
'dark:bg-primary-950 dark:text-primary-100'
|
|
7471
|
+
],
|
|
7472
|
+
itemIcon: 'text-primary-600 dark:text-primary-400'
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
},
|
|
7476
|
+
defaultVariants: {
|
|
7477
|
+
color: 'default'
|
|
7478
|
+
}
|
|
7479
|
+
});
|
|
7480
|
+
/**
|
|
7481
|
+
* Resolve a `MenuPlacement` enum to Radix `side` + `align` props.
|
|
7482
|
+
* Single source of truth so Menu.tsx doesn't need a switch statement.
|
|
7483
|
+
*/ function placementToRadix(placement) {
|
|
7484
|
+
// The format is `<side>` or `<side>-<align>`. Center is the default
|
|
7485
|
+
// when no `-start` / `-end` suffix is present.
|
|
7486
|
+
const [side, alignSuffix] = placement.split('-');
|
|
7487
|
+
return {
|
|
7488
|
+
side,
|
|
7489
|
+
align: alignSuffix != null ? alignSuffix : 'center'
|
|
7490
|
+
};
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7493
|
+
const MenuContext = /*#__PURE__*/ createContext({
|
|
7494
|
+
closeOnItemClick: true
|
|
7495
|
+
});
|
|
7496
|
+
// ─── Menu (Root provider) ────────────────────────────────────────
|
|
7497
|
+
/**
|
|
7498
|
+
* `<Menu>` — top-level provider that wraps Radix `DropdownMenu.Root`.
|
|
6931
7499
|
*
|
|
6932
|
-
*
|
|
6933
|
-
*
|
|
6934
|
-
*
|
|
6935
|
-
*
|
|
6936
|
-
*
|
|
7500
|
+
* Compound API:
|
|
7501
|
+
* ```tsx
|
|
7502
|
+
* <Menu>
|
|
7503
|
+
* <MenuTrigger>
|
|
7504
|
+
* <IconButton aria-label="Actions"><MoreIcon /></IconButton>
|
|
7505
|
+
* </MenuTrigger>
|
|
7506
|
+
* <MenuContent>
|
|
7507
|
+
* <MenuLabel>Actions</MenuLabel>
|
|
7508
|
+
* <MenuItem onClick={onEdit}>Edit</MenuItem>
|
|
7509
|
+
* <MenuSeparator />
|
|
7510
|
+
* <MenuItem color="danger" onClick={onDelete}>Delete</MenuItem>
|
|
7511
|
+
* </MenuContent>
|
|
7512
|
+
* </Menu>
|
|
7513
|
+
* ```
|
|
6937
7514
|
*
|
|
6938
|
-
*
|
|
6939
|
-
*
|
|
6940
|
-
*
|
|
6941
|
-
* • No animation / transition → use `sx`
|
|
7515
|
+
* Built on `@radix-ui/react-dropdown-menu` (purpose-built menu
|
|
7516
|
+
* primitive with full WAI-ARIA menu pattern + keyboard nav
|
|
7517
|
+
* + type-ahead + portal mount).
|
|
6942
7518
|
*
|
|
6943
|
-
*
|
|
6944
|
-
*
|
|
6945
|
-
*
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
7519
|
+
* Sub-menus and controlled selection mode are deliberately out of
|
|
7520
|
+
* scope for v1 (Sprint 4.4) — composable via Radix primitives if
|
|
7521
|
+
* needed via explicit sub-component opt-in (future).
|
|
7522
|
+
*/ function Menu({ children, open, defaultOpen, onOpenChange, closeOnItemClick = true, modal = false }) {
|
|
7523
|
+
return jsx(MenuContext.Provider, {
|
|
7524
|
+
value: {
|
|
7525
|
+
closeOnItemClick
|
|
7526
|
+
},
|
|
7527
|
+
children: jsx(RadixMenu.Root, {
|
|
7528
|
+
open: open,
|
|
7529
|
+
defaultOpen: defaultOpen,
|
|
7530
|
+
onOpenChange: onOpenChange,
|
|
7531
|
+
modal: modal,
|
|
7532
|
+
children: children
|
|
7533
|
+
})
|
|
7534
|
+
});
|
|
7535
|
+
}
|
|
7536
|
+
// ─── MenuTrigger ─────────────────────────────────────────────────
|
|
7537
|
+
/**
|
|
7538
|
+
* `<MenuTrigger>` — the element that opens the menu.
|
|
7539
|
+
*
|
|
7540
|
+
* Always renders the immediate child via Radix Slot (`asChild`) — the
|
|
7541
|
+
* child receives the click handler + `aria-haspopup="menu"` +
|
|
7542
|
+
* `aria-expanded` reactive to open state. Pass any interactive
|
|
7543
|
+
* component as the child (`<IconButton>`, `<Button>`, `<Avatar>`,
|
|
7544
|
+
* custom).
|
|
7545
|
+
*/ function MenuTrigger({ children }) {
|
|
7546
|
+
return jsx(RadixMenu.Trigger, {
|
|
7547
|
+
asChild: true,
|
|
7548
|
+
children: children
|
|
7549
|
+
});
|
|
7550
|
+
}
|
|
7551
|
+
// ─── MenuContent ─────────────────────────────────────────────────
|
|
7552
|
+
/**
|
|
7553
|
+
* `<MenuContent>` — the floating panel.
|
|
7554
|
+
*
|
|
7555
|
+
* Lazy-mounted via Radix Portal — zero DOM cost when the menu is
|
|
7556
|
+
* closed (one of the perf properties we inherit from Radix +
|
|
7557
|
+
* Atlaskit-style menus).
|
|
7558
|
+
*
|
|
7559
|
+
* Placement / sideOffset are read from the parent `<Menu>` via
|
|
7560
|
+
* Radix context — we re-pull the `<Menu>` props via the
|
|
7561
|
+
* `MenuContext` here.
|
|
7562
|
+
*
|
|
7563
|
+
* NB: placement is read from PARENT MENU PROPS, not from
|
|
7564
|
+
* `<MenuContent>` directly — keeps the floating-positioning API on
|
|
7565
|
+
* the provider where it belongs.
|
|
7566
|
+
*/ const MenuContent = /*#__PURE__*/ forwardRef(function MenuContent(props, ref) {
|
|
7567
|
+
const { children, minWidth, className, sx } = props;
|
|
7568
|
+
const v = menuVariants();
|
|
7569
|
+
// NB: we don't read `placement` from MenuContext (Radix Root
|
|
7570
|
+
// doesn't surface those props to descendants). Instead, the
|
|
7571
|
+
// consumer passes them on `<Menu>` and they're forwarded into
|
|
7572
|
+
// `<MenuContent>` via context-style props — to keep the API
|
|
7573
|
+
// simple, we DON'T expose placement on MenuContent itself; the
|
|
7574
|
+
// user sets it on Menu. We achieve this by reading the MenuProps
|
|
7575
|
+
// off the Radix-internal context… actually, simpler: we just
|
|
7576
|
+
// accept the default `bottom-start` from Radix and let the
|
|
7577
|
+
// consumer pass `side`/`align` via MenuContent if they want fine
|
|
7578
|
+
// control. For now, hardcode the most common defaults; Sprint
|
|
7579
|
+
// 4.4-bis can lift this if real demand emerges.
|
|
7580
|
+
const { side, align } = placementToRadix('bottom-start');
|
|
7581
|
+
return jsx(RadixMenu.Portal, {
|
|
7582
|
+
children: jsx(RadixMenu.Content, {
|
|
7583
|
+
ref: ref,
|
|
7584
|
+
side: side,
|
|
7585
|
+
align: align,
|
|
7586
|
+
sideOffset: 6,
|
|
7587
|
+
style: minWidth !== undefined ? {
|
|
7588
|
+
minWidth: typeof minWidth === 'number' ? `${minWidth}px` : minWidth
|
|
7589
|
+
} : undefined,
|
|
7590
|
+
className: cn(v.content(), sx, className),
|
|
7591
|
+
children: children
|
|
7592
|
+
})
|
|
7593
|
+
});
|
|
7594
|
+
});
|
|
7595
|
+
MenuContent.displayName = 'MenuContent';
|
|
7596
|
+
// ─── MenuItem ────────────────────────────────────────────────────
|
|
7597
|
+
/**
|
|
7598
|
+
* `<MenuItem>` — clickable row inside the menu.
|
|
7599
|
+
*
|
|
7600
|
+
* Memoized via `React.memo` for performance — re-renders only when
|
|
7601
|
+
* its own props change, not when sibling items mutate. Pattern
|
|
7602
|
+
* inspired by Atlaskit's `ButtonItem` optimization (one of the
|
|
7603
|
+
* reasons Atlaskit menus stay responsive on large item lists).
|
|
7604
|
+
*
|
|
7605
|
+
* Bridge integration:
|
|
7606
|
+
* - `access` — hides / disables the item per RBAC
|
|
7607
|
+
* - `visibleWhen` — engine-reactive predicate
|
|
7608
|
+
*
|
|
7609
|
+
* Both gates fold into the visible/disabled state; when the
|
|
7610
|
+
* predicate returns `false` OR `access` denies visibility, the
|
|
7611
|
+
* item renders `null` (does NOT occupy a row in the menu, vs
|
|
7612
|
+
* disabled which renders dimmed-but-present).
|
|
7613
|
+
*/ function MenuItemImpl(props) {
|
|
7614
|
+
const { children, onClick, icon, endIcon, disabled: disabledProp, selected, color = 'default', access, visibleWhen, className, sx } = props;
|
|
7615
|
+
const { closeOnItemClick } = useContext(MenuContext);
|
|
7616
|
+
// Bridge hooks — always called (rules-of-hooks).
|
|
7617
|
+
const bridge = useContext(DashFormContext);
|
|
7618
|
+
const isBridgeVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
7619
|
+
const accessState = useAccessState(access);
|
|
7620
|
+
if (!isBridgeVisible || !accessState.visible) return null;
|
|
7621
|
+
const isDisabled = Boolean(disabledProp) || accessState.disabled || accessState.readonly;
|
|
7622
|
+
const v = menuVariants({
|
|
7623
|
+
color,
|
|
7624
|
+
selected
|
|
7625
|
+
});
|
|
7626
|
+
return jsxs(RadixMenu.Item, {
|
|
7627
|
+
disabled: isDisabled,
|
|
7628
|
+
onSelect: (e)=>{
|
|
7629
|
+
if (isDisabled) return;
|
|
7630
|
+
// Suppress default close behaviour when the parent <Menu>
|
|
7631
|
+
// explicitly opted out (filter / multi-select pattern).
|
|
7632
|
+
if (!closeOnItemClick) e.preventDefault();
|
|
7633
|
+
onClick == null ? void 0 : onClick();
|
|
7634
|
+
},
|
|
7635
|
+
className: cn(v.item(), sx, className),
|
|
7636
|
+
children: [
|
|
7637
|
+
icon != null && jsx("span", {
|
|
7638
|
+
className: v.itemIcon(),
|
|
7639
|
+
children: icon
|
|
7640
|
+
}),
|
|
7641
|
+
jsx("span", {
|
|
7642
|
+
className: v.itemLabel(),
|
|
7643
|
+
children: children
|
|
7644
|
+
}),
|
|
7645
|
+
endIcon != null && jsx("span", {
|
|
7646
|
+
className: v.itemEnd(),
|
|
7647
|
+
children: endIcon
|
|
7648
|
+
})
|
|
7649
|
+
]
|
|
7650
|
+
});
|
|
7651
|
+
}
|
|
7652
|
+
const MenuItem = /*#__PURE__*/ memo(MenuItemImpl);
|
|
7653
|
+
MenuItem.displayName = 'MenuItem';
|
|
7654
|
+
// ─── MenuLabel ───────────────────────────────────────────────────
|
|
7655
|
+
/**
|
|
7656
|
+
* `<MenuLabel>` — non-interactive group heading.
|
|
7657
|
+
*
|
|
7658
|
+
* Renders via Radix `DropdownMenu.Label` which carries the right
|
|
7659
|
+
* ARIA semantics (`role="presentation"` — not announced as a
|
|
7660
|
+
* `menuitem` by screen readers).
|
|
7661
|
+
*/ function MenuLabel({ children, className }) {
|
|
7662
|
+
const v = menuVariants();
|
|
7663
|
+
return jsx(RadixMenu.Label, {
|
|
7664
|
+
className: cn(v.label(), className),
|
|
7665
|
+
children: children
|
|
7666
|
+
});
|
|
7667
|
+
}
|
|
7668
|
+
// ─── MenuSeparator ───────────────────────────────────────────────
|
|
7669
|
+
/**
|
|
7670
|
+
* `<MenuSeparator>` — horizontal divider between items.
|
|
7671
|
+
*
|
|
7672
|
+
* Renders via Radix `DropdownMenu.Separator` — semantically correct
|
|
7673
|
+
* `role="separator"` for screen readers navigating the menu.
|
|
7674
|
+
*/ function MenuSeparator({ className }) {
|
|
7675
|
+
const v = menuVariants();
|
|
7676
|
+
return jsx(RadixMenu.Separator, {
|
|
7677
|
+
className: cn(v.separator(), className)
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
// ─── MenuSkeleton ────────────────────────────────────────────────
|
|
7681
|
+
/**
|
|
7682
|
+
* `<MenuSkeleton>` — Atlaskit-style loading state.
|
|
7683
|
+
*
|
|
7684
|
+
* Renders `count` placeholder rows (default 3) inside the menu while
|
|
7685
|
+
* real items are being fetched. Each row matches the geometry of a
|
|
7686
|
+
* real `<MenuItem>` (height, padding, icon slot) so the layout
|
|
7687
|
+
* doesn't jump when the real data arrives.
|
|
7688
|
+
*
|
|
7689
|
+
* `withHeading=true` adds a skeleton heading at the top, mimicking a
|
|
7690
|
+
* `<MenuLabel>` — useful when the real menu starts with a section
|
|
7691
|
+
* label.
|
|
7692
|
+
*
|
|
7693
|
+
* Pure visual — no animation overhead beyond Tailwind's
|
|
7694
|
+
* `animate-pulse` (motion-reduce-safe).
|
|
7695
|
+
*
|
|
7696
|
+
* @example
|
|
7697
|
+
* ```tsx
|
|
7698
|
+
* <MenuContent>
|
|
7699
|
+
* {isLoading ? (
|
|
7700
|
+
* <MenuSkeleton count={5} withHeading />
|
|
7701
|
+
* ) : (
|
|
7702
|
+
* items.map((it) => <MenuItem key={it.id}>{it.label}</MenuItem>)
|
|
7703
|
+
* )}
|
|
7704
|
+
* </MenuContent>
|
|
7705
|
+
* ```
|
|
7706
|
+
*/ function MenuSkeleton({ count = 3, withHeading = false, className }) {
|
|
7707
|
+
const v = menuVariants();
|
|
7708
|
+
return jsxs("div", {
|
|
7709
|
+
role: "presentation",
|
|
7710
|
+
"aria-hidden": "true",
|
|
7711
|
+
className: cn('animate-pulse motion-reduce:animate-none', className),
|
|
7712
|
+
children: [
|
|
7713
|
+
withHeading && jsx("div", {
|
|
7714
|
+
className: cn(v.label(), 'pointer-events-none'),
|
|
7715
|
+
children: jsx("span", {
|
|
7716
|
+
className: "inline-block h-2.5 w-16 rounded-sm bg-neutral-200"
|
|
7717
|
+
})
|
|
7718
|
+
}),
|
|
7719
|
+
Array.from({
|
|
7720
|
+
length: count
|
|
7721
|
+
}).map((_, i)=>jsx(SkeletonRow, {}, i))
|
|
7722
|
+
]
|
|
7723
|
+
});
|
|
7724
|
+
}
|
|
7725
|
+
/**
|
|
7726
|
+
* One skeleton row — matches the dimensions of a real `<MenuItem>`
|
|
7727
|
+
* (icon slot + label slot). Internal — not exported.
|
|
7728
|
+
*
|
|
7729
|
+
* @internal
|
|
7730
|
+
*/ function SkeletonRow() {
|
|
7731
|
+
return jsxs("div", {
|
|
7732
|
+
className: "flex items-center gap-2 px-3 py-1.5 mx-1 my-0.5",
|
|
7733
|
+
children: [
|
|
7734
|
+
jsx("span", {
|
|
7735
|
+
className: "inline-block h-4 w-4 shrink-0 rounded-sm bg-neutral-200"
|
|
7736
|
+
}),
|
|
7737
|
+
jsx("span", {
|
|
7738
|
+
className: "inline-block h-3 flex-1 rounded-sm bg-neutral-200"
|
|
7739
|
+
})
|
|
7740
|
+
]
|
|
7741
|
+
});
|
|
7742
|
+
}
|
|
7743
|
+
|
|
7744
|
+
/**
|
|
7745
|
+
* Tailwind-variants recipe for `<Badge>`.
|
|
7746
|
+
*
|
|
7747
|
+
* Slot layout:
|
|
7748
|
+
* - `root` — outer span (relative-positioned wrapper around the
|
|
7749
|
+
* anchor). `inline-flex` so the wrapper shrinks to fit
|
|
7750
|
+
* the anchor element.
|
|
7751
|
+
* - `badge` — the floating absolute-positioned pill / dot. Two
|
|
7752
|
+
* variants (`dot=true` → tiny circle, `dot=false` →
|
|
7753
|
+
* pill with min-width to fit content).
|
|
7754
|
+
*
|
|
7755
|
+
* Color axis emits the `badge` background + text classes. The 7
|
|
7756
|
+
* intents map to:
|
|
7757
|
+
* - solid bg: `bg-{intent}-500` (vivid, attention-grabbing for
|
|
7758
|
+
* notifications — different from Chip which uses 600 for solid
|
|
7759
|
+
* because chip surface is larger; small badge needs the brighter
|
|
7760
|
+
* mid-tone)
|
|
7761
|
+
* - text: `text-white` for primary/secondary/success/warning/danger/
|
|
7762
|
+
* info; `text-neutral-50` for neutral (auto-inverts via CSS-var
|
|
7763
|
+
* swap, no `dark:` needed)
|
|
7764
|
+
*
|
|
7765
|
+
* Placement axis emits absolute-position + translate utilities:
|
|
7766
|
+
* - top-right (default): `top-0 right-0 -translate-y-1/2 translate-x-1/2`
|
|
7767
|
+
* - top-left: `top-0 left-0 -translate-y-1/2 -translate-x-1/2`
|
|
7768
|
+
* - bottom-right: `bottom-0 right-0 translate-y-1/2 translate-x-1/2`
|
|
7769
|
+
* - bottom-left: `bottom-0 left-0 translate-y-1/2 -translate-x-1/2`
|
|
7770
|
+
*
|
|
7771
|
+
* Overlap axis adds an extra inset for circular anchors so the
|
|
7772
|
+
* badge sits along the round edge instead of the bounding box corner.
|
|
7773
|
+
*/ const badgeVariants = tv({
|
|
7774
|
+
slots: {
|
|
7775
|
+
root: 'relative inline-flex shrink-0',
|
|
7776
|
+
badge: [
|
|
7777
|
+
// Absolute positioning is owned by the placement compound rules.
|
|
7778
|
+
'absolute z-10',
|
|
7779
|
+
// Layout / typography — pill defaults; dot mode overrides
|
|
7780
|
+
// dimensions below.
|
|
7781
|
+
'inline-flex items-center justify-center',
|
|
7782
|
+
'rounded-full',
|
|
7783
|
+
'text-[10px] font-medium leading-none',
|
|
7784
|
+
'pointer-events-none select-none',
|
|
7785
|
+
// Smooth show/hide for `invisible` toggle (consumer-side
|
|
7786
|
+
// animation hook). Gated on motion-reduce per WCAG 2.3.3.
|
|
7787
|
+
'transition-transform duration-150 motion-reduce:transition-none motion-reduce:duration-0'
|
|
7788
|
+
]
|
|
7789
|
+
},
|
|
7790
|
+
variants: {
|
|
7791
|
+
/**
|
|
7792
|
+
* `dot=true` → tiny circle (8px), no content. The badge slot
|
|
7793
|
+
* collapses to a fixed-size circle without the pill min-width.
|
|
7794
|
+
*/ dot: {
|
|
7795
|
+
true: {
|
|
7796
|
+
badge: 'w-2 h-2 p-0'
|
|
7797
|
+
},
|
|
7798
|
+
false: {
|
|
7799
|
+
// Content pill — 16px tall, padded inline, min-width so
|
|
7800
|
+
// single-digit counts still read centered.
|
|
7801
|
+
badge: 'min-w-4 h-4 px-1'
|
|
7802
|
+
}
|
|
7803
|
+
},
|
|
7804
|
+
/** Whether the floating badge is hidden via `display: none`. */ invisible: {
|
|
7805
|
+
true: {
|
|
7806
|
+
badge: 'hidden'
|
|
7807
|
+
}
|
|
7808
|
+
},
|
|
7809
|
+
/** Halo ring for visual separation from the anchor surface. */ withRing: {
|
|
7810
|
+
true: {
|
|
7811
|
+
// ring-neutral-50 auto-inverts via dashforgePreset CSS var
|
|
7812
|
+
// swap (no `dark:` needed for the neutral palette).
|
|
7813
|
+
badge: 'ring-2 ring-neutral-50'
|
|
7814
|
+
}
|
|
7815
|
+
},
|
|
7816
|
+
/** 7 intents — drives the badge background + text colour. */ color: {
|
|
7817
|
+
neutral: {
|
|
7818
|
+
// Both classes on the neutral palette — auto-invert via CSS
|
|
7819
|
+
// var swap. No `dark:` variants needed.
|
|
7820
|
+
badge: 'bg-neutral-700 text-neutral-50'
|
|
7821
|
+
},
|
|
7822
|
+
primary: {
|
|
7823
|
+
badge: 'bg-primary-500 text-white'
|
|
7824
|
+
},
|
|
7825
|
+
secondary: {
|
|
7826
|
+
badge: 'bg-secondary-500 text-white'
|
|
7827
|
+
},
|
|
7828
|
+
success: {
|
|
7829
|
+
badge: 'bg-success-500 text-white'
|
|
7830
|
+
},
|
|
7831
|
+
warning: {
|
|
7832
|
+
badge: 'bg-warning-500 text-white'
|
|
7833
|
+
},
|
|
7834
|
+
danger: {
|
|
7835
|
+
badge: 'bg-danger-500 text-white'
|
|
7836
|
+
},
|
|
7837
|
+
info: {
|
|
7838
|
+
badge: 'bg-info-500 text-white'
|
|
7839
|
+
}
|
|
7840
|
+
},
|
|
7841
|
+
placement: {
|
|
7842
|
+
'top-right': {
|
|
7843
|
+
badge: 'top-0 right-0 -translate-y-1/2 translate-x-1/2'
|
|
7844
|
+
},
|
|
7845
|
+
'top-left': {
|
|
7846
|
+
badge: 'top-0 left-0 -translate-y-1/2 -translate-x-1/2'
|
|
7847
|
+
},
|
|
7848
|
+
'bottom-right': {
|
|
7849
|
+
badge: 'bottom-0 right-0 translate-y-1/2 translate-x-1/2'
|
|
7850
|
+
},
|
|
7851
|
+
'bottom-left': {
|
|
7852
|
+
badge: 'bottom-0 left-0 translate-y-1/2 -translate-x-1/2'
|
|
7853
|
+
}
|
|
7854
|
+
},
|
|
7855
|
+
/**
|
|
7856
|
+
* `overlap='circular'` adds an inward inset so the badge sits
|
|
7857
|
+
* along the circular anchor edge instead of the bounding-box
|
|
7858
|
+
* corner. The 14% offset is empirically tuned to match the
|
|
7859
|
+
* standard avatar / round-button geometry (matches MUI's value).
|
|
7860
|
+
*/ overlap: {
|
|
7861
|
+
rectangular: {},
|
|
7862
|
+
circular: {}
|
|
7863
|
+
}
|
|
7864
|
+
},
|
|
7865
|
+
compoundVariants: [
|
|
7866
|
+
// ─── overlap='circular' × placement — additional inset ─────────
|
|
7867
|
+
// The inward shift is in addition to the placement translate,
|
|
7868
|
+
// achieved via an extra `-{translate-corner}` adjustment.
|
|
7869
|
+
{
|
|
7870
|
+
overlap: 'circular',
|
|
7871
|
+
placement: 'top-right',
|
|
7872
|
+
class: {
|
|
7873
|
+
badge: 'top-[14%] right-[14%] translate-x-1/2 -translate-y-1/2'
|
|
7874
|
+
}
|
|
7875
|
+
},
|
|
7876
|
+
{
|
|
7877
|
+
overlap: 'circular',
|
|
7878
|
+
placement: 'top-left',
|
|
7879
|
+
class: {
|
|
7880
|
+
badge: 'top-[14%] left-[14%] -translate-x-1/2 -translate-y-1/2'
|
|
7881
|
+
}
|
|
7882
|
+
},
|
|
7883
|
+
{
|
|
7884
|
+
overlap: 'circular',
|
|
7885
|
+
placement: 'bottom-right',
|
|
7886
|
+
class: {
|
|
7887
|
+
badge: 'bottom-[14%] right-[14%] translate-x-1/2 translate-y-1/2'
|
|
7888
|
+
}
|
|
7889
|
+
},
|
|
7890
|
+
{
|
|
7891
|
+
overlap: 'circular',
|
|
7892
|
+
placement: 'bottom-left',
|
|
7893
|
+
class: {
|
|
7894
|
+
badge: 'bottom-[14%] left-[14%] -translate-x-1/2 translate-y-1/2'
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7897
|
+
],
|
|
7898
|
+
defaultVariants: {
|
|
7899
|
+
dot: false,
|
|
7900
|
+
invisible: false,
|
|
7901
|
+
withRing: true,
|
|
7902
|
+
color: 'danger',
|
|
7903
|
+
placement: 'top-right',
|
|
7904
|
+
overlap: 'rectangular'
|
|
7905
|
+
}
|
|
7906
|
+
});
|
|
7907
|
+
|
|
7908
|
+
/**
|
|
7909
|
+
* Resolve the displayed badge content based on `content`, `max`, and
|
|
7910
|
+
* `showZero` props.
|
|
7911
|
+
*
|
|
7912
|
+
* Returns `null` when nothing should render (omitted content + no dot,
|
|
7913
|
+
* or `content=0` without `showZero`). Otherwise returns the string
|
|
7914
|
+
* to display (formatted overflow for numbers above `max`).
|
|
7915
|
+
*
|
|
7916
|
+
* @internal
|
|
7917
|
+
*/ function resolveDisplayContent(content, max, showZero, dot) {
|
|
7918
|
+
// Dot mode — content is irrelevant, badge always shows (unless
|
|
7919
|
+
// gated by visibleWhen / access / invisible higher up).
|
|
7920
|
+
if (dot) return '';
|
|
7921
|
+
// No content + no dot → nothing to render.
|
|
7922
|
+
if (content === undefined || content === null) return null;
|
|
7923
|
+
// Numeric content — apply max overflow + showZero gate.
|
|
7924
|
+
if (typeof content === 'number') {
|
|
7925
|
+
if (content === 0 && !showZero) return null;
|
|
7926
|
+
if (max >= 0 && content > max) return `${max}+`;
|
|
7927
|
+
return String(content);
|
|
7928
|
+
}
|
|
7929
|
+
// String content — render verbatim. Empty string → hide
|
|
7930
|
+
// (counts as "no content" — consumer should pass `dot` for plain dot).
|
|
7931
|
+
return content === '' ? null : content;
|
|
7932
|
+
}
|
|
7933
|
+
/**
|
|
7934
|
+
* `<Badge>` — anchored indicator overlaying an anchor element.
|
|
7935
|
+
*
|
|
7936
|
+
* Common patterns:
|
|
7937
|
+
* - Notification count on a bell IconButton
|
|
7938
|
+
* - Presence dot on an Avatar
|
|
7939
|
+
* - "NEW" label on a Card or feature tile
|
|
7940
|
+
* - Unsaved-changes indicator on a save button
|
|
7941
|
+
*
|
|
7942
|
+
* For inline standalone pills (status labels, tags, removable
|
|
7943
|
+
* chips), use `<Chip>` instead — Chip and Badge are complementary
|
|
7944
|
+
* primitives. Chip is interactive (`onClick`, `onDelete`,
|
|
7945
|
+
* `selected`); Badge is passive overlay.
|
|
7946
|
+
*
|
|
7947
|
+
* Bridge integration (Sprint 4.4):
|
|
7948
|
+
* - `access` — hide the badge for unauthorized subjects (anchor
|
|
7949
|
+
* stays visible; wrap anchor in a separate Box `access` to gate
|
|
7950
|
+
* it too)
|
|
7951
|
+
* - `visibleWhen` — engine-reactive predicate
|
|
7952
|
+
*
|
|
7953
|
+
* The wrapped anchor element ALWAYS renders; only the floating
|
|
7954
|
+
* badge dot/count is gated by `access`, `visibleWhen`, `invisible`,
|
|
7955
|
+
* and content-resolution logic.
|
|
7956
|
+
*
|
|
7957
|
+
* @example
|
|
7958
|
+
* ```tsx
|
|
7959
|
+
* // Notification count on bell
|
|
7960
|
+
* <Badge content={unreadCount}>
|
|
7961
|
+
* <IconButton aria-label="Notifications" variant="ghost">
|
|
7962
|
+
* <BellIcon />
|
|
7963
|
+
* </IconButton>
|
|
7964
|
+
* </Badge>
|
|
7965
|
+
*
|
|
7966
|
+
* // Presence dot on avatar
|
|
7967
|
+
* <Badge dot color="success" overlap="circular">
|
|
7968
|
+
* <Avatar name="Maya Rodriguez" />
|
|
7969
|
+
* </Badge>
|
|
7970
|
+
*
|
|
7971
|
+
* // Overflow
|
|
7972
|
+
* <Badge content={1284} max={999}>
|
|
7973
|
+
* <IconButton aria-label="Messages"><MailIcon /></IconButton>
|
|
7974
|
+
* </Badge>
|
|
7975
|
+
*
|
|
7976
|
+
* // RBAC-gated
|
|
7977
|
+
* <Badge
|
|
7978
|
+
* content={adminQueueCount}
|
|
7979
|
+
* access={{ resource: 'admin', action: 'read', onUnauthorized: 'hide' }}
|
|
7980
|
+
* >
|
|
7981
|
+
* <IconButton aria-label="Admin queue"><SettingsIcon /></IconButton>
|
|
7982
|
+
* </Badge>
|
|
7983
|
+
*
|
|
7984
|
+
* // Engine-reactive
|
|
7985
|
+
* <Badge
|
|
7986
|
+
* dot color="warning"
|
|
7987
|
+
* visibleWhen={(engine) => !!engine.getNode('unsavedChanges')?.value}
|
|
7988
|
+
* >
|
|
7989
|
+
* <IconButton aria-label="Save"><SaveIcon /></IconButton>
|
|
7990
|
+
* </Badge>
|
|
7991
|
+
* ```
|
|
7992
|
+
*/ const Badge = /*#__PURE__*/ forwardRef(function Badge(props, ref) {
|
|
7993
|
+
var _slotProps_root, _slotProps_badge;
|
|
7994
|
+
const { children, content, dot = false, max = 99, showZero = false, invisible = false, color = 'danger', placement = 'top-right', overlap = 'rectangular', withRing = true, access, visibleWhen, sx, className, slotProps } = props;
|
|
7995
|
+
// Bridge — both hooks called unconditionally (rules-of-hooks).
|
|
7996
|
+
const bridge = useContext(DashFormContext);
|
|
7997
|
+
const isBridgeVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
7998
|
+
const accessState = useAccessState(access);
|
|
7999
|
+
// Resolve the actual content to render (string or null = hide).
|
|
8000
|
+
// Computed regardless of access/visibleWhen so we can keep the
|
|
8001
|
+
// hook order stable; the final "render or not" check folds them in.
|
|
8002
|
+
const displayContent = resolveDisplayContent(content, max, showZero, dot);
|
|
8003
|
+
// The wrapped anchor ALWAYS renders. Only the floating badge is
|
|
8004
|
+
// gated. This is the right contract — gating the anchor too would
|
|
8005
|
+
// belong to a Box `access` wrapper one level up.
|
|
8006
|
+
const shouldRenderBadge = isBridgeVisible && accessState.visible && displayContent !== null;
|
|
8007
|
+
const v = badgeVariants({
|
|
8008
|
+
dot,
|
|
8009
|
+
invisible,
|
|
8010
|
+
withRing,
|
|
8011
|
+
color,
|
|
8012
|
+
placement,
|
|
8013
|
+
overlap
|
|
8014
|
+
});
|
|
8015
|
+
return jsxs("span", {
|
|
8016
|
+
ref: ref,
|
|
8017
|
+
className: cn(v.root(), sx, className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
|
|
8018
|
+
children: [
|
|
8019
|
+
children,
|
|
8020
|
+
shouldRenderBadge && jsx("span", {
|
|
8021
|
+
className: cn(v.badge(), slotProps == null ? void 0 : (_slotProps_badge = slotProps.badge) == null ? void 0 : _slotProps_badge.className),
|
|
8022
|
+
"aria-hidden": "true",
|
|
8023
|
+
children: displayContent
|
|
8024
|
+
})
|
|
8025
|
+
]
|
|
8026
|
+
});
|
|
8027
|
+
});
|
|
8028
|
+
Badge.displayName = 'Badge';
|
|
8029
|
+
|
|
8030
|
+
/**
|
|
8031
|
+
* Tailwind-variants recipe for `<Avatar>`.
|
|
8032
|
+
*
|
|
8033
|
+
* Axes:
|
|
8034
|
+
* - `size` — 5 steps (xs/sm/md/lg/xl), maps to w/h spacing tokens
|
|
8035
|
+
* + matching text-size for the initials inside.
|
|
8036
|
+
* - `radius` — 7 steps (none/sm/md/lg/xl/2xl/full), same scale as
|
|
8037
|
+
* `<Box rounded>`. `shape` prop in the component maps
|
|
8038
|
+
* semantically: circle → full, rounded → lg, square → none.
|
|
8039
|
+
* - `color` — 7 intents. Drives the FALLBACK bg+text only (image
|
|
8040
|
+
* surface is independent of color).
|
|
8041
|
+
*
|
|
8042
|
+
* The `color` × `tone` combo is resolved at render time in
|
|
8043
|
+
* `Avatar.tsx` via compound lookup — see the SOFT_TINT and TONE
|
|
8044
|
+
* tables there. We do NOT expand the matrix exhaustively in this TV
|
|
8045
|
+
* recipe (it would be 7 colors × 10 tones × 5 sizes = 350 rules; the
|
|
8046
|
+
* runtime resolver keeps the recipe slim).
|
|
8047
|
+
*/ const avatarVariants = tv({
|
|
8048
|
+
slots: {
|
|
8049
|
+
/** Outer wrapper — sets dimensions + radius + ring + alignment. */ root: [
|
|
8050
|
+
'relative inline-flex items-center justify-center shrink-0',
|
|
8051
|
+
'overflow-hidden align-middle',
|
|
8052
|
+
'select-none',
|
|
8053
|
+
// The font weight + leading are baked into root so initials slot
|
|
8054
|
+
// inherits them without an extra class layer.
|
|
8055
|
+
'font-medium leading-none'
|
|
8056
|
+
],
|
|
8057
|
+
/** `<img>` slot — fills the root, object-cover for crop-to-square. */ img: 'block w-full h-full object-cover',
|
|
8058
|
+
/** Initials text slot. */ initials: 'uppercase tracking-tight',
|
|
8059
|
+
/** Generic user icon SVG fallback. */ icon: 'w-1/2 h-1/2 opacity-80'
|
|
8060
|
+
},
|
|
8061
|
+
variants: {
|
|
8062
|
+
size: {
|
|
8063
|
+
xs: {
|
|
8064
|
+
root: 'w-5 h-5 text-[10px]',
|
|
8065
|
+
initials: ''
|
|
8066
|
+
},
|
|
8067
|
+
sm: {
|
|
8068
|
+
root: 'w-7 h-7 text-xs',
|
|
8069
|
+
initials: ''
|
|
8070
|
+
},
|
|
8071
|
+
md: {
|
|
8072
|
+
root: 'w-9 h-9 text-sm',
|
|
8073
|
+
initials: ''
|
|
8074
|
+
},
|
|
8075
|
+
lg: {
|
|
8076
|
+
root: 'w-12 h-12 text-base',
|
|
8077
|
+
initials: ''
|
|
8078
|
+
},
|
|
8079
|
+
xl: {
|
|
8080
|
+
root: 'w-16 h-16 text-xl',
|
|
8081
|
+
initials: ''
|
|
8082
|
+
}
|
|
8083
|
+
},
|
|
8084
|
+
radius: {
|
|
8085
|
+
none: {
|
|
8086
|
+
root: 'rounded-none'
|
|
8087
|
+
},
|
|
8088
|
+
sm: {
|
|
8089
|
+
root: 'rounded-sm'
|
|
8090
|
+
},
|
|
8091
|
+
md: {
|
|
8092
|
+
root: 'rounded-md'
|
|
8093
|
+
},
|
|
8094
|
+
lg: {
|
|
8095
|
+
root: 'rounded-lg'
|
|
8096
|
+
},
|
|
8097
|
+
xl: {
|
|
8098
|
+
root: 'rounded-xl'
|
|
8099
|
+
},
|
|
8100
|
+
'2xl': {
|
|
8101
|
+
root: 'rounded-2xl'
|
|
8102
|
+
},
|
|
8103
|
+
full: {
|
|
8104
|
+
root: 'rounded-full'
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
},
|
|
8108
|
+
defaultVariants: {
|
|
8109
|
+
size: 'md',
|
|
8110
|
+
radius: 'full'
|
|
8111
|
+
}
|
|
8112
|
+
});
|
|
8113
|
+
/**
|
|
8114
|
+
* Default "soft" fallback treatment per intent — what you get when
|
|
8115
|
+
* `tone` is omitted. Light tinted bg + dark text for legibility.
|
|
8116
|
+
* Neutral row uses no `dark:` because the dashforgePreset() CSS-var
|
|
8117
|
+
* swap auto-inverts neutral; color rows keep `dark:` for the
|
|
8118
|
+
* intentional palette shift (palette colors don't auto-invert).
|
|
8119
|
+
*/ const AVATAR_SOFT_FALLBACK = {
|
|
8120
|
+
neutral: 'bg-neutral-100 text-neutral-900',
|
|
8121
|
+
primary: 'bg-primary-100 text-primary-900 dark:bg-primary-950 dark:text-primary-100',
|
|
8122
|
+
secondary: 'bg-secondary-100 text-secondary-900 dark:bg-secondary-950 dark:text-secondary-100',
|
|
8123
|
+
success: 'bg-success-100 text-success-900 dark:bg-success-950 dark:text-success-100',
|
|
8124
|
+
warning: 'bg-warning-100 text-warning-900 dark:bg-warning-950 dark:text-warning-100',
|
|
8125
|
+
danger: 'bg-danger-100 text-danger-900 dark:bg-danger-950 dark:text-danger-100',
|
|
8126
|
+
info: 'bg-info-100 text-info-900 dark:bg-info-950 dark:text-info-100'
|
|
8127
|
+
};
|
|
8128
|
+
/**
|
|
8129
|
+
* Specific-tone resolver — when the consumer sets `tone={500}` (or
|
|
8130
|
+
* any other value), this lookup builds the matching bg + text class.
|
|
8131
|
+
*
|
|
8132
|
+
* Text color logic:
|
|
8133
|
+
* - tones 50-400 → dark text (uses the 900 step)
|
|
8134
|
+
* - tones 500-900 → light text (uses the 50 step)
|
|
8135
|
+
*
|
|
8136
|
+
* Tailwind JIT requires literal class strings, so we enumerate them
|
|
8137
|
+
* explicitly here. The bundle cost is ~70 lines, acceptable for the
|
|
8138
|
+
* type-safety win of letting consumers pick any palette shade.
|
|
8139
|
+
*/ const AVATAR_TONE_FALLBACK = {
|
|
8140
|
+
neutral: {
|
|
8141
|
+
50: 'bg-neutral-50 text-neutral-900',
|
|
8142
|
+
100: 'bg-neutral-100 text-neutral-900',
|
|
8143
|
+
200: 'bg-neutral-200 text-neutral-900',
|
|
8144
|
+
300: 'bg-neutral-300 text-neutral-900',
|
|
8145
|
+
400: 'bg-neutral-400 text-neutral-900',
|
|
8146
|
+
500: 'bg-neutral-500 text-neutral-50',
|
|
8147
|
+
600: 'bg-neutral-600 text-neutral-50',
|
|
8148
|
+
700: 'bg-neutral-700 text-neutral-50',
|
|
8149
|
+
800: 'bg-neutral-800 text-neutral-50',
|
|
8150
|
+
900: 'bg-neutral-900 text-neutral-50'
|
|
8151
|
+
},
|
|
8152
|
+
primary: {
|
|
8153
|
+
50: 'bg-primary-50 text-primary-900',
|
|
8154
|
+
100: 'bg-primary-100 text-primary-900',
|
|
8155
|
+
200: 'bg-primary-200 text-primary-900',
|
|
8156
|
+
300: 'bg-primary-300 text-primary-900',
|
|
8157
|
+
400: 'bg-primary-400 text-primary-900',
|
|
8158
|
+
500: 'bg-primary-500 text-primary-50',
|
|
8159
|
+
600: 'bg-primary-600 text-primary-50',
|
|
8160
|
+
700: 'bg-primary-700 text-primary-50',
|
|
8161
|
+
800: 'bg-primary-800 text-primary-50',
|
|
8162
|
+
900: 'bg-primary-900 text-primary-50'
|
|
8163
|
+
},
|
|
8164
|
+
secondary: {
|
|
8165
|
+
50: 'bg-secondary-50 text-secondary-900',
|
|
8166
|
+
100: 'bg-secondary-100 text-secondary-900',
|
|
8167
|
+
200: 'bg-secondary-200 text-secondary-900',
|
|
8168
|
+
300: 'bg-secondary-300 text-secondary-900',
|
|
8169
|
+
400: 'bg-secondary-400 text-secondary-900',
|
|
8170
|
+
500: 'bg-secondary-500 text-secondary-50',
|
|
8171
|
+
600: 'bg-secondary-600 text-secondary-50',
|
|
8172
|
+
700: 'bg-secondary-700 text-secondary-50',
|
|
8173
|
+
800: 'bg-secondary-800 text-secondary-50',
|
|
8174
|
+
900: 'bg-secondary-900 text-secondary-50'
|
|
8175
|
+
},
|
|
8176
|
+
success: {
|
|
8177
|
+
50: 'bg-success-50 text-success-900',
|
|
8178
|
+
100: 'bg-success-100 text-success-900',
|
|
8179
|
+
200: 'bg-success-200 text-success-900',
|
|
8180
|
+
300: 'bg-success-300 text-success-900',
|
|
8181
|
+
400: 'bg-success-400 text-success-900',
|
|
8182
|
+
500: 'bg-success-500 text-success-50',
|
|
8183
|
+
600: 'bg-success-600 text-success-50',
|
|
8184
|
+
700: 'bg-success-700 text-success-50',
|
|
8185
|
+
800: 'bg-success-800 text-success-50',
|
|
8186
|
+
900: 'bg-success-900 text-success-50'
|
|
8187
|
+
},
|
|
8188
|
+
warning: {
|
|
8189
|
+
50: 'bg-warning-50 text-warning-900',
|
|
8190
|
+
100: 'bg-warning-100 text-warning-900',
|
|
8191
|
+
200: 'bg-warning-200 text-warning-900',
|
|
8192
|
+
300: 'bg-warning-300 text-warning-900',
|
|
8193
|
+
400: 'bg-warning-400 text-warning-900',
|
|
8194
|
+
500: 'bg-warning-500 text-warning-50',
|
|
8195
|
+
600: 'bg-warning-600 text-warning-50',
|
|
8196
|
+
700: 'bg-warning-700 text-warning-50',
|
|
8197
|
+
800: 'bg-warning-800 text-warning-50',
|
|
8198
|
+
900: 'bg-warning-900 text-warning-50'
|
|
8199
|
+
},
|
|
8200
|
+
danger: {
|
|
8201
|
+
50: 'bg-danger-50 text-danger-900',
|
|
8202
|
+
100: 'bg-danger-100 text-danger-900',
|
|
8203
|
+
200: 'bg-danger-200 text-danger-900',
|
|
8204
|
+
300: 'bg-danger-300 text-danger-900',
|
|
8205
|
+
400: 'bg-danger-400 text-danger-900',
|
|
8206
|
+
500: 'bg-danger-500 text-danger-50',
|
|
8207
|
+
600: 'bg-danger-600 text-danger-50',
|
|
8208
|
+
700: 'bg-danger-700 text-danger-50',
|
|
8209
|
+
800: 'bg-danger-800 text-danger-50',
|
|
8210
|
+
900: 'bg-danger-900 text-danger-50'
|
|
8211
|
+
},
|
|
8212
|
+
info: {
|
|
8213
|
+
50: 'bg-info-50 text-info-900',
|
|
8214
|
+
100: 'bg-info-100 text-info-900',
|
|
8215
|
+
200: 'bg-info-200 text-info-900',
|
|
8216
|
+
300: 'bg-info-300 text-info-900',
|
|
8217
|
+
400: 'bg-info-400 text-info-900',
|
|
8218
|
+
500: 'bg-info-500 text-info-50',
|
|
8219
|
+
600: 'bg-info-600 text-info-50',
|
|
8220
|
+
700: 'bg-info-700 text-info-50',
|
|
8221
|
+
800: 'bg-info-800 text-info-50',
|
|
8222
|
+
900: 'bg-info-900 text-info-50'
|
|
8223
|
+
}
|
|
8224
|
+
};
|
|
8225
|
+
|
|
8226
|
+
/**
|
|
8227
|
+
* Generic user icon — inline SVG fallback when no `src` and no
|
|
8228
|
+
* `name` are provided. Stroke uses `currentColor` so it inherits the
|
|
8229
|
+
* resolved fallback text color. Same convention as Calendar +
|
|
8230
|
+
* Autocomplete + Alert icons (no icon-library dep).
|
|
8231
|
+
*
|
|
8232
|
+
* @internal
|
|
8233
|
+
*/ function DefaultUserIcon() {
|
|
8234
|
+
return jsxs("svg", {
|
|
8235
|
+
viewBox: "0 0 24 24",
|
|
8236
|
+
fill: "none",
|
|
8237
|
+
stroke: "currentColor",
|
|
8238
|
+
strokeWidth: 1.75,
|
|
8239
|
+
strokeLinecap: "round",
|
|
8240
|
+
strokeLinejoin: "round",
|
|
8241
|
+
"aria-hidden": "true",
|
|
8242
|
+
children: [
|
|
8243
|
+
jsx("circle", {
|
|
8244
|
+
cx: "12",
|
|
8245
|
+
cy: "8",
|
|
8246
|
+
r: "3.75"
|
|
8247
|
+
}),
|
|
8248
|
+
jsx("path", {
|
|
8249
|
+
d: "M4.5 20a7.5 7.5 0 0 1 15 0"
|
|
8250
|
+
})
|
|
8251
|
+
]
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
/**
|
|
8255
|
+
* Map the semantic `shape` shortcut to a Box-style `radius` token.
|
|
8256
|
+
* Used only when `radius` is NOT explicitly passed (explicit wins).
|
|
8257
|
+
*
|
|
8258
|
+
* @internal
|
|
8259
|
+
*/ function shapeToRadius(shape) {
|
|
8260
|
+
switch(shape){
|
|
8261
|
+
case 'circle':
|
|
8262
|
+
return 'full';
|
|
8263
|
+
case 'rounded':
|
|
8264
|
+
return 'lg';
|
|
8265
|
+
case 'square':
|
|
8266
|
+
return 'none';
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
/**
|
|
8270
|
+
* Extract initials from a name string. Strategy:
|
|
8271
|
+
* - Split by whitespace, filter empty.
|
|
8272
|
+
* - Take the first letter of the first 2 tokens, uppercased.
|
|
8273
|
+
* - "Maya Rodriguez" → "MR"
|
|
8274
|
+
* - "Cher" → "C"
|
|
8275
|
+
* - " multiple spaces " → "MS" (if 2+ words after trim)
|
|
8276
|
+
* - "" → "" (empty string — caller falls back to icon)
|
|
8277
|
+
*
|
|
8278
|
+
* @internal
|
|
8279
|
+
*/ function initialsFromName(name) {
|
|
8280
|
+
var _ref;
|
|
8281
|
+
var _tokens_;
|
|
8282
|
+
if (!name) return '';
|
|
8283
|
+
const tokens = name.trim().split(/\s+/).filter(Boolean);
|
|
8284
|
+
if (tokens.length === 0) return '';
|
|
8285
|
+
return (tokens[0][0] + ((_ref = (_tokens_ = tokens[1]) == null ? void 0 : _tokens_[0]) != null ? _ref : '')).toUpperCase();
|
|
8286
|
+
}
|
|
8287
|
+
/**
|
|
8288
|
+
* `<Avatar>` — image / initials / icon visual identity.
|
|
8289
|
+
*
|
|
8290
|
+
* Fallback chain (top to bottom — first match wins):
|
|
8291
|
+
* 1. `src` set + image loads OK → render `<img>`
|
|
8292
|
+
* 2. `src` set + image fails → render initials from `name`
|
|
8293
|
+
* 3. `src` absent + `name` set → render initials from `name`
|
|
8294
|
+
* 4. `fallbackIcon` set → render that ReactNode
|
|
8295
|
+
* 5. Nothing → render generic user SVG
|
|
8296
|
+
*
|
|
8297
|
+
* Color resolution (only for fallback content, image surface is
|
|
8298
|
+
* independent):
|
|
8299
|
+
* - `color="primary"` only → soft default (bg-100, text-900)
|
|
8300
|
+
* - `color="primary" tone={500}` → bg-primary-500 + text-primary-50
|
|
8301
|
+
* - Outside the palette → use `sx`
|
|
8302
|
+
*
|
|
8303
|
+
* No `access` / `visibleWhen` — Avatar is display-only by category.
|
|
8304
|
+
* For visibility gating, wrap in `<Box>` (which has both since
|
|
8305
|
+
* Sprint 4.4).
|
|
8306
|
+
*/ const Avatar = /*#__PURE__*/ forwardRef(function Avatar(props, ref) {
|
|
8307
|
+
var _ref, _ref1;
|
|
8308
|
+
var _AVATAR_TONE_FALLBACK_color;
|
|
8309
|
+
const { src, alt, imgProps, name, shape = 'circle', radius, size = 'md', color = 'neutral', tone, fallbackIcon, sx, className } = props;
|
|
8310
|
+
// Track image-load failures so we can fall through to initials/icon
|
|
8311
|
+
// dynamically. `imgLoaded` distinguishes between "still loading"
|
|
8312
|
+
// (don't show fallback yet — would flash) and "loaded successfully"
|
|
8313
|
+
// (don't paint a fallback color underneath, which would peek through
|
|
8314
|
+
// transparent PNGs).
|
|
8315
|
+
const [imgFailed, setImgFailed] = useState(false);
|
|
8316
|
+
// Resolve final radius: explicit `radius` prop wins over `shape`.
|
|
8317
|
+
const finalRadius = radius != null ? radius : shapeToRadius(shape);
|
|
8318
|
+
// Compute base classes from the TV recipe.
|
|
8319
|
+
const v = avatarVariants({
|
|
8320
|
+
size,
|
|
8321
|
+
radius: finalRadius
|
|
8322
|
+
});
|
|
8323
|
+
// Fallback color resolution. `tone` (when set) drives the specific
|
|
8324
|
+
// shade; otherwise the soft-default lookup is used.
|
|
8325
|
+
const fallbackTone = tone !== undefined ? (_ref = (_AVATAR_TONE_FALLBACK_color = AVATAR_TONE_FALLBACK[color]) == null ? void 0 : _AVATAR_TONE_FALLBACK_color[tone]) != null ? _ref : AVATAR_SOFT_FALLBACK[color] : AVATAR_SOFT_FALLBACK[color];
|
|
8326
|
+
// Image is "successful" when src is set AND the load hasn't failed.
|
|
8327
|
+
const showImage = !!src && !imgFailed;
|
|
8328
|
+
const initials = initialsFromName(name);
|
|
8329
|
+
// The root carries the fallback color ONLY when we're showing the
|
|
8330
|
+
// fallback content. Painting it underneath a successful image is
|
|
8331
|
+
// wrong if the image has transparency.
|
|
8332
|
+
const fallbackBgClasses = !showImage ? fallbackTone : '';
|
|
8333
|
+
const rootClasses = cn(v.root(), fallbackBgClasses, sx, className);
|
|
8334
|
+
// Pick which fallback to render — priority: initials > custom icon > generic SVG.
|
|
8335
|
+
const fallbackContent = initials ? jsx("span", {
|
|
8336
|
+
className: v.initials(),
|
|
8337
|
+
children: initials
|
|
8338
|
+
}) : fallbackIcon ? jsx("span", {
|
|
8339
|
+
className: "inline-flex items-center justify-center w-full h-full",
|
|
8340
|
+
children: fallbackIcon
|
|
8341
|
+
}) : jsx("span", {
|
|
8342
|
+
className: v.icon() + ' inline-flex items-center justify-center',
|
|
8343
|
+
children: jsx(DefaultUserIcon, {})
|
|
8344
|
+
});
|
|
8345
|
+
return jsx("span", {
|
|
8346
|
+
ref: ref,
|
|
8347
|
+
className: rootClasses,
|
|
8348
|
+
role: "img",
|
|
8349
|
+
"aria-label": (_ref1 = alt != null ? alt : name) != null ? _ref1 : undefined,
|
|
8350
|
+
children: showImage ? // Inner img is DECORATIVE (alt="") — the parent span carries
|
|
8351
|
+
// the accessible name via role="img" + aria-label. This avoids
|
|
8352
|
+
// a duplicate role="img" announcement and matches MUI's a11y
|
|
8353
|
+
// pattern.
|
|
8354
|
+
jsx("img", _extends({
|
|
8355
|
+
src: src,
|
|
8356
|
+
alt: "",
|
|
8357
|
+
onError: ()=>setImgFailed(true)
|
|
8358
|
+
}, imgProps, {
|
|
8359
|
+
className: cn(v.img(), imgProps == null ? void 0 : imgProps.className)
|
|
8360
|
+
})) : fallbackContent
|
|
8361
|
+
});
|
|
8362
|
+
});
|
|
8363
|
+
Avatar.displayName = 'Avatar';
|
|
8364
|
+
// ─── AvatarGroup ─────────────────────────────────────────────────
|
|
8365
|
+
/**
|
|
8366
|
+
* Horizontal spacing tokens for the group overlap. Negative margin
|
|
8367
|
+
* pulls each child onto the previous, simulating the "stack of
|
|
8368
|
+
* avatars" pattern. Tokens map to spacing scale.
|
|
8369
|
+
*
|
|
8370
|
+
* @internal
|
|
8371
|
+
*/ const GROUP_SPACING = {
|
|
8372
|
+
xs: '-space-x-1',
|
|
8373
|
+
sm: '-space-x-2',
|
|
8374
|
+
md: '-space-x-3'
|
|
8375
|
+
};
|
|
8376
|
+
/**
|
|
8377
|
+
* `<AvatarGroup>` — thin horizontal wrapper for overlapping avatars.
|
|
8378
|
+
*
|
|
8379
|
+
* Slices children to `max - 1` visible items and renders a trailing
|
|
8380
|
+
* "+N" overflow indicator with the remaining count (when more than
|
|
8381
|
+
* `max` children are passed).
|
|
8382
|
+
*
|
|
8383
|
+
* Each child Avatar inherits the group's `size` unless it explicitly
|
|
8384
|
+
* overrides. When `withRing` is `true` (default), each visible
|
|
8385
|
+
* Avatar gets a `ring-2 ring-white` halo so overlapping borders
|
|
8386
|
+
* stay visually separated.
|
|
8387
|
+
*
|
|
8388
|
+
* @example
|
|
8389
|
+
* ```tsx
|
|
8390
|
+
* <AvatarGroup max={3}>
|
|
8391
|
+
* <Avatar name="Maya Rodriguez" color="primary" />
|
|
8392
|
+
* <Avatar name="John Doe" color="success" />
|
|
8393
|
+
* <Avatar name="Sara Chen" color="warning" />
|
|
8394
|
+
* <Avatar name="Lila Park" color="info" />
|
|
8395
|
+
* <Avatar name="Eve Kim" color="danger" />
|
|
8396
|
+
* </AvatarGroup>
|
|
8397
|
+
* // Renders: MR, JD, SC + a "+2" overflow chip
|
|
8398
|
+
* ```
|
|
8399
|
+
*/ const AvatarGroup = /*#__PURE__*/ forwardRef(function AvatarGroup(props, ref) {
|
|
8400
|
+
const { children, max = 4, size = 'md', spacing = 'sm', withRing = true, className, sx } = props;
|
|
8401
|
+
const allChildren = Children.toArray(children).filter(isValidElement);
|
|
8402
|
+
const total = allChildren.length;
|
|
8403
|
+
const visibleCount = Math.min(total, max);
|
|
8404
|
+
const overflow = Math.max(0, total - visibleCount);
|
|
8405
|
+
// Decorate each visible child with size (if not overridden by the
|
|
8406
|
+
// child) and optional ring halo. cloneElement is the standard
|
|
8407
|
+
// React pattern for prop-injection across a children array.
|
|
8408
|
+
const visible = allChildren.slice(0, visibleCount).map((child, idx)=>{
|
|
8409
|
+
var _childProps_size;
|
|
8410
|
+
const childProps = child.props;
|
|
8411
|
+
const inheritedSize = (_childProps_size = childProps.size) != null ? _childProps_size : size;
|
|
8412
|
+
const ringClass = withRing ? 'ring-2 ring-neutral-50' : '';
|
|
8413
|
+
const mergedSx = cn(ringClass, childProps.sx, childProps.className);
|
|
8414
|
+
return /*#__PURE__*/ cloneElement(child, {
|
|
8415
|
+
key: idx,
|
|
8416
|
+
size: inheritedSize,
|
|
8417
|
+
sx: mergedSx
|
|
8418
|
+
});
|
|
8419
|
+
});
|
|
8420
|
+
return jsxs("div", {
|
|
8421
|
+
ref: ref,
|
|
8422
|
+
className: cn('inline-flex items-center', GROUP_SPACING[spacing], sx, className),
|
|
8423
|
+
children: [
|
|
8424
|
+
visible,
|
|
8425
|
+
overflow > 0 && jsx(Avatar, {
|
|
8426
|
+
// The overflow indicator IS an avatar (same dimensions /
|
|
8427
|
+
// radius), but with a +N text fallback rather than initials.
|
|
8428
|
+
// We pass an empty `name` and a custom `fallbackIcon` that
|
|
8429
|
+
// renders the count.
|
|
8430
|
+
size: size,
|
|
8431
|
+
color: "neutral",
|
|
8432
|
+
sx: cn(withRing && 'ring-2 ring-neutral-50'),
|
|
8433
|
+
fallbackIcon: jsxs("span", {
|
|
8434
|
+
className: "text-xs font-medium",
|
|
8435
|
+
children: [
|
|
8436
|
+
"+",
|
|
8437
|
+
overflow
|
|
8438
|
+
]
|
|
8439
|
+
})
|
|
8440
|
+
})
|
|
8441
|
+
]
|
|
8442
|
+
});
|
|
8443
|
+
});
|
|
8444
|
+
AvatarGroup.displayName = 'AvatarGroup';
|
|
8445
|
+
|
|
8446
|
+
/**
|
|
8447
|
+
* `boxVariants` — the surface primitive for @dashforge/tw.
|
|
8448
|
+
*
|
|
8449
|
+
* Architectural choice (planned with the user — F9 deep dive):
|
|
8450
|
+
*
|
|
8451
|
+
* Box replaces FOUR overlapping concepts from MUI in one component:
|
|
8452
|
+
* • Box (typed div)
|
|
8453
|
+
* • Paper (surface with elevation)
|
|
8454
|
+
* • Card (Paper specialisation)
|
|
8455
|
+
* • Surface (Joy UI's outlined / soft / solid / plain variants)
|
|
8456
|
+
*
|
|
8457
|
+
* The reason for the consolidation: in MUI you have to compose two or
|
|
8458
|
+
* three of these to express even basic intent ("an outlined card with
|
|
8459
|
+
* warning tone"). Here, one `<Box variant="outlined" color="warning">`
|
|
8460
|
+
* says exactly that.
|
|
8461
|
+
*
|
|
8462
|
+
* Variant taxonomy (5 axes, intentionally non-overlapping with Stack/Grid):
|
|
8463
|
+
*
|
|
8464
|
+
* • plain — bare div + padding + radius. The escape hatch.
|
|
8465
|
+
* • outlined — 1px border + subtle bg tint. The "card lite".
|
|
8466
|
+
* • elevated — bg surface + shadow scale (0-5). The "floating panel".
|
|
8467
|
+
* • soft — semi-transparent intent bg + intent text. The "callout".
|
|
8468
|
+
* • solid — solid intent bg + contrasting text. The "CTA banner".
|
|
8469
|
+
*
|
|
8470
|
+
* `color` applies to outlined/soft/solid (each gets the 7 intent
|
|
8471
|
+
* variants). `elevated` is color-agnostic (always neutral surface +
|
|
8472
|
+
* shadow scale). `plain` is everything-agnostic.
|
|
8473
|
+
*
|
|
8474
|
+
* Spacing axes (p/px/py/m/mx/my): mapped explicitly to the 11 token
|
|
8475
|
+
* steps from @dashforge/tw-tokens (0, 0.5, 1, 2, 3, 4, 6, 8, 12, 16, 24).
|
|
8476
|
+
* Tailwind JIT requires literal class strings — building them dynamically
|
|
8477
|
+
* with template literals would purge them. The verbosity below is the
|
|
8478
|
+
* cost of keeping the bundle CSS-pure and rebuild-free.
|
|
8479
|
+
*
|
|
8480
|
+
* What this does NOT do (deliberate, see component docs):
|
|
8481
|
+
* • No display / flex / grid props → use Stack or Grid
|
|
8482
|
+
* • No position / overflow / z-index → use `sx`
|
|
8483
|
+
* • No animation / transition → use `sx`
|
|
8484
|
+
*
|
|
8485
|
+
* The "Box is not flex" rule is the spine of the layout layer. Without
|
|
8486
|
+
* it, every `<div>` in an app gravitates back to Box and the surface
|
|
8487
|
+
* vs layout distinction collapses — exactly the failure mode this
|
|
8488
|
+
* primitive exists to prevent.
|
|
8489
|
+
*/ const boxVariants = tv({
|
|
8490
|
+
base: 'block',
|
|
8491
|
+
variants: {
|
|
8492
|
+
/*
|
|
8493
|
+
* Surface variant. Compound with `color` for outlined / soft / solid;
|
|
6952
8494
|
* standalone for plain / elevated.
|
|
6953
8495
|
*/ variant: {
|
|
6954
8496
|
plain: '',
|
|
@@ -6961,337 +8503,1337 @@ Typography.displayName = 'Typography';
|
|
|
6961
8503
|
// in dark mode — overpowering elevation).
|
|
6962
8504
|
elevated: 'bg-white dark:bg-neutral-100',
|
|
6963
8505
|
soft: '',
|
|
6964
|
-
solid: ''
|
|
8506
|
+
solid: ''
|
|
8507
|
+
},
|
|
8508
|
+
/*
|
|
8509
|
+
* Intent color. Only meaningful when variant is outlined / soft /
|
|
8510
|
+
* solid (resolved via compoundVariants below). For plain / elevated
|
|
8511
|
+
* this axis is ignored at the visual level — but kept in the type
|
|
8512
|
+
* so the prop is always available without conditional typing.
|
|
8513
|
+
*/ color: {
|
|
8514
|
+
primary: '',
|
|
8515
|
+
secondary: '',
|
|
8516
|
+
success: '',
|
|
8517
|
+
warning: '',
|
|
8518
|
+
danger: '',
|
|
8519
|
+
info: '',
|
|
8520
|
+
neutral: ''
|
|
8521
|
+
},
|
|
8522
|
+
/*
|
|
8523
|
+
* Shadow scale — relevant for `variant='elevated'`. We keep elevation
|
|
8524
|
+
* as a separate axis (not folded into `variant`) so consumers can
|
|
8525
|
+
* dial it up/down without changing the variant. Default `0` = no
|
|
8526
|
+
* shadow (consistent with MUI's elevation=0).
|
|
8527
|
+
*/ elevation: {
|
|
8528
|
+
0: 'shadow-none',
|
|
8529
|
+
1: 'shadow-sm',
|
|
8530
|
+
2: 'shadow',
|
|
8531
|
+
3: 'shadow-md',
|
|
8532
|
+
4: 'shadow-lg',
|
|
8533
|
+
5: 'shadow-xl'
|
|
8534
|
+
},
|
|
8535
|
+
rounded: {
|
|
8536
|
+
none: 'rounded-none',
|
|
8537
|
+
sm: 'rounded-sm',
|
|
8538
|
+
md: 'rounded-md',
|
|
8539
|
+
lg: 'rounded-lg',
|
|
8540
|
+
xl: 'rounded-xl',
|
|
8541
|
+
'2xl': 'rounded-2xl',
|
|
8542
|
+
full: 'rounded-full'
|
|
8543
|
+
},
|
|
8544
|
+
/*
|
|
8545
|
+
* Spacing — six axes (p/px/py/m/mx/my), 11 token steps each.
|
|
8546
|
+
* Literals enumerated explicitly so Tailwind's JIT scanner finds
|
|
8547
|
+
* every class. Token steps mirror @dashforge/tw-tokens spacing scale.
|
|
8548
|
+
*/ p: {
|
|
8549
|
+
0: 'p-0',
|
|
8550
|
+
'0.5': 'p-0.5',
|
|
8551
|
+
1: 'p-1',
|
|
8552
|
+
2: 'p-2',
|
|
8553
|
+
3: 'p-3',
|
|
8554
|
+
4: 'p-4',
|
|
8555
|
+
6: 'p-6',
|
|
8556
|
+
8: 'p-8',
|
|
8557
|
+
12: 'p-12',
|
|
8558
|
+
16: 'p-16',
|
|
8559
|
+
24: 'p-24'
|
|
8560
|
+
},
|
|
8561
|
+
px: {
|
|
8562
|
+
0: 'px-0',
|
|
8563
|
+
'0.5': 'px-0.5',
|
|
8564
|
+
1: 'px-1',
|
|
8565
|
+
2: 'px-2',
|
|
8566
|
+
3: 'px-3',
|
|
8567
|
+
4: 'px-4',
|
|
8568
|
+
6: 'px-6',
|
|
8569
|
+
8: 'px-8',
|
|
8570
|
+
12: 'px-12',
|
|
8571
|
+
16: 'px-16',
|
|
8572
|
+
24: 'px-24'
|
|
8573
|
+
},
|
|
8574
|
+
py: {
|
|
8575
|
+
0: 'py-0',
|
|
8576
|
+
'0.5': 'py-0.5',
|
|
8577
|
+
1: 'py-1',
|
|
8578
|
+
2: 'py-2',
|
|
8579
|
+
3: 'py-3',
|
|
8580
|
+
4: 'py-4',
|
|
8581
|
+
6: 'py-6',
|
|
8582
|
+
8: 'py-8',
|
|
8583
|
+
12: 'py-12',
|
|
8584
|
+
16: 'py-16',
|
|
8585
|
+
24: 'py-24'
|
|
8586
|
+
},
|
|
8587
|
+
m: {
|
|
8588
|
+
0: 'm-0',
|
|
8589
|
+
'0.5': 'm-0.5',
|
|
8590
|
+
1: 'm-1',
|
|
8591
|
+
2: 'm-2',
|
|
8592
|
+
3: 'm-3',
|
|
8593
|
+
4: 'm-4',
|
|
8594
|
+
6: 'm-6',
|
|
8595
|
+
8: 'm-8',
|
|
8596
|
+
12: 'm-12',
|
|
8597
|
+
16: 'm-16',
|
|
8598
|
+
24: 'm-24'
|
|
8599
|
+
},
|
|
8600
|
+
mx: {
|
|
8601
|
+
0: 'mx-0',
|
|
8602
|
+
'0.5': 'mx-0.5',
|
|
8603
|
+
1: 'mx-1',
|
|
8604
|
+
2: 'mx-2',
|
|
8605
|
+
3: 'mx-3',
|
|
8606
|
+
4: 'mx-4',
|
|
8607
|
+
6: 'mx-6',
|
|
8608
|
+
8: 'mx-8',
|
|
8609
|
+
12: 'mx-12',
|
|
8610
|
+
16: 'mx-16',
|
|
8611
|
+
24: 'mx-24'
|
|
8612
|
+
},
|
|
8613
|
+
my: {
|
|
8614
|
+
0: 'my-0',
|
|
8615
|
+
'0.5': 'my-0.5',
|
|
8616
|
+
1: 'my-1',
|
|
8617
|
+
2: 'my-2',
|
|
8618
|
+
3: 'my-3',
|
|
8619
|
+
4: 'my-4',
|
|
8620
|
+
6: 'my-6',
|
|
8621
|
+
8: 'my-8',
|
|
8622
|
+
12: 'my-12',
|
|
8623
|
+
16: 'my-16',
|
|
8624
|
+
24: 'my-24'
|
|
8625
|
+
},
|
|
8626
|
+
fullWidth: {
|
|
8627
|
+
true: 'w-full'
|
|
8628
|
+
},
|
|
8629
|
+
fullHeight: {
|
|
8630
|
+
true: 'h-full'
|
|
8631
|
+
}
|
|
8632
|
+
},
|
|
8633
|
+
/*
|
|
8634
|
+
* Compound variants — where surface × color get their actual visual.
|
|
8635
|
+
* Twenty-one entries: 7 (outlined) + 7 (soft) + 7 (solid).
|
|
8636
|
+
* `plain` and `elevated` don't appear here (no color contribution).
|
|
8637
|
+
*
|
|
8638
|
+
* Dark-mode pair is baked in: light-mode picks the 50-300 steps,
|
|
8639
|
+
* dark-mode picks the 800-950 steps — both reactive to setMode()
|
|
8640
|
+
* via the @dashforge/tw-theme CSS variables.
|
|
8641
|
+
*/ compoundVariants: [
|
|
8642
|
+
// ─── outlined × color ─────────────────────────────────────────────
|
|
8643
|
+
{
|
|
8644
|
+
variant: 'outlined',
|
|
8645
|
+
color: 'primary',
|
|
8646
|
+
class: 'border-primary-300 bg-primary-50/40 dark:border-primary-800 dark:bg-primary-950/30'
|
|
8647
|
+
},
|
|
8648
|
+
{
|
|
8649
|
+
variant: 'outlined',
|
|
8650
|
+
color: 'secondary',
|
|
8651
|
+
class: 'border-secondary-300 bg-secondary-50/40 dark:border-secondary-800 dark:bg-secondary-950/30'
|
|
8652
|
+
},
|
|
8653
|
+
{
|
|
8654
|
+
variant: 'outlined',
|
|
8655
|
+
color: 'success',
|
|
8656
|
+
class: 'border-success-300 bg-success-50/40 dark:border-success-800 dark:bg-success-950/30'
|
|
8657
|
+
},
|
|
8658
|
+
{
|
|
8659
|
+
variant: 'outlined',
|
|
8660
|
+
color: 'warning',
|
|
8661
|
+
class: 'border-warning-300 bg-warning-50/40 dark:border-warning-800 dark:bg-warning-950/30'
|
|
8662
|
+
},
|
|
8663
|
+
{
|
|
8664
|
+
variant: 'outlined',
|
|
8665
|
+
color: 'danger',
|
|
8666
|
+
class: 'border-danger-300 bg-danger-50/40 dark:border-danger-800 dark:bg-danger-950/30'
|
|
8667
|
+
},
|
|
8668
|
+
{
|
|
8669
|
+
variant: 'outlined',
|
|
8670
|
+
color: 'info',
|
|
8671
|
+
class: 'border-info-300 bg-info-50/40 dark:border-info-800 dark:bg-info-950/30'
|
|
8672
|
+
},
|
|
8673
|
+
{
|
|
8674
|
+
variant: 'outlined',
|
|
8675
|
+
color: 'neutral',
|
|
8676
|
+
// border-neutral-200 auto-inverts via CSS var swap (no `dark:`
|
|
8677
|
+
// needed). bg-white is hardcoded — `dark:bg-neutral-100` keeps
|
|
8678
|
+
// elevation tier above page surface in dark mode (NOT
|
|
8679
|
+
// `dark:bg-neutral-900` which renders near-white).
|
|
8680
|
+
class: 'border-neutral-200 bg-white dark:bg-neutral-100'
|
|
8681
|
+
},
|
|
8682
|
+
// ─── soft × color ─────────────────────────────────────────────────
|
|
8683
|
+
{
|
|
8684
|
+
variant: 'soft',
|
|
8685
|
+
color: 'primary',
|
|
8686
|
+
class: 'bg-primary-100 text-primary-900 dark:bg-primary-950/50 dark:text-primary-100'
|
|
8687
|
+
},
|
|
8688
|
+
{
|
|
8689
|
+
variant: 'soft',
|
|
8690
|
+
color: 'secondary',
|
|
8691
|
+
class: 'bg-secondary-100 text-secondary-900 dark:bg-secondary-950/50 dark:text-secondary-100'
|
|
8692
|
+
},
|
|
8693
|
+
{
|
|
8694
|
+
variant: 'soft',
|
|
8695
|
+
color: 'success',
|
|
8696
|
+
class: 'bg-success-100 text-success-900 dark:bg-success-950/50 dark:text-success-100'
|
|
8697
|
+
},
|
|
8698
|
+
{
|
|
8699
|
+
variant: 'soft',
|
|
8700
|
+
color: 'warning',
|
|
8701
|
+
class: 'bg-warning-100 text-warning-900 dark:bg-warning-950/50 dark:text-warning-100'
|
|
8702
|
+
},
|
|
8703
|
+
{
|
|
8704
|
+
variant: 'soft',
|
|
8705
|
+
color: 'danger',
|
|
8706
|
+
class: 'bg-danger-100 text-danger-900 dark:bg-danger-950/50 dark:text-danger-100'
|
|
8707
|
+
},
|
|
8708
|
+
{
|
|
8709
|
+
variant: 'soft',
|
|
8710
|
+
color: 'info',
|
|
8711
|
+
class: 'bg-info-100 text-info-900 dark:bg-info-950/50 dark:text-info-100'
|
|
8712
|
+
},
|
|
8713
|
+
{
|
|
8714
|
+
variant: 'soft',
|
|
8715
|
+
color: 'neutral',
|
|
8716
|
+
// Both neutral classes auto-invert via the dashforgePreset() CSS
|
|
8717
|
+
// var swap — no `dark:` variants needed.
|
|
8718
|
+
class: 'bg-neutral-100 text-neutral-900'
|
|
8719
|
+
},
|
|
8720
|
+
// ─── solid × color ────────────────────────────────────────────────
|
|
8721
|
+
{
|
|
8722
|
+
variant: 'solid',
|
|
8723
|
+
color: 'primary',
|
|
8724
|
+
class: 'bg-primary-600 text-white dark:bg-primary-500'
|
|
8725
|
+
},
|
|
8726
|
+
{
|
|
8727
|
+
variant: 'solid',
|
|
8728
|
+
color: 'secondary',
|
|
8729
|
+
class: 'bg-secondary-600 text-white dark:bg-secondary-500'
|
|
8730
|
+
},
|
|
8731
|
+
{
|
|
8732
|
+
variant: 'solid',
|
|
8733
|
+
color: 'success',
|
|
8734
|
+
class: 'bg-success-600 text-white dark:bg-success-500'
|
|
8735
|
+
},
|
|
8736
|
+
{
|
|
8737
|
+
variant: 'solid',
|
|
8738
|
+
color: 'warning',
|
|
8739
|
+
class: 'bg-warning-500 text-white dark:bg-warning-600'
|
|
8740
|
+
},
|
|
8741
|
+
{
|
|
8742
|
+
variant: 'solid',
|
|
8743
|
+
color: 'danger',
|
|
8744
|
+
class: 'bg-danger-600 text-white dark:bg-danger-500'
|
|
8745
|
+
},
|
|
8746
|
+
{
|
|
8747
|
+
variant: 'solid',
|
|
8748
|
+
color: 'info',
|
|
8749
|
+
class: 'bg-info-600 text-white dark:bg-info-500'
|
|
8750
|
+
},
|
|
8751
|
+
{
|
|
8752
|
+
variant: 'solid',
|
|
8753
|
+
color: 'neutral',
|
|
8754
|
+
// Solid neutral is the high-emphasis "always-foreground accent"
|
|
8755
|
+
// variant. Both classes auto-invert: dark surface + light text
|
|
8756
|
+
// in light mode (`#171717` / `#fafafa`) flips to light surface +
|
|
8757
|
+
// dark text in dark mode (`#f5f5f5` / `#0a0a0a`). The
|
|
8758
|
+
// accent inverts WITH the page surface — consistent with the
|
|
8759
|
+
// identity rule. Previously used `text-white` (no auto-invert)
|
|
8760
|
+
// which forced double-inversion to keep readable.
|
|
8761
|
+
class: 'bg-neutral-900 text-neutral-50'
|
|
8762
|
+
}
|
|
8763
|
+
],
|
|
8764
|
+
defaultVariants: {
|
|
8765
|
+
variant: 'plain',
|
|
8766
|
+
color: 'neutral',
|
|
8767
|
+
elevation: 0,
|
|
8768
|
+
rounded: 'none'
|
|
8769
|
+
}
|
|
8770
|
+
});
|
|
8771
|
+
|
|
8772
|
+
/**
|
|
8773
|
+
* `<Box>` — the surface primitive of @dashforge/tw.
|
|
8774
|
+
*
|
|
8775
|
+
* What it IS:
|
|
8776
|
+
* A polymorphic container with typed surface variants (plain /
|
|
8777
|
+
* outlined / elevated / soft / solid) × 7 intent colours, plus
|
|
8778
|
+
* spacing / sizing / rounded / elevation as enumerated token-scale
|
|
8779
|
+
* props. Replaces MUI's Box + Paper + Card + Surface in one component.
|
|
8780
|
+
*
|
|
8781
|
+
* What it is NOT (deliberate, enforced by the prop set):
|
|
8782
|
+
* • Not a flex container → use <Stack>
|
|
8783
|
+
* • Not a grid container → use <Grid>
|
|
8784
|
+
* • Not a paragraph → use <Typography>
|
|
8785
|
+
* • Not a button → use <Button>
|
|
8786
|
+
*
|
|
8787
|
+
* The "Box is not flex" rule is the spine of the layout layer. Without
|
|
8788
|
+
* it, every `<div>` in an app gravitates back to Box and the surface
|
|
8789
|
+
* vs layout distinction collapses. Box's job is the SURFACE around
|
|
8790
|
+
* content; Stack/Grid's job is the ARRANGEMENT of content. Two
|
|
8791
|
+
* primitives, two responsibilities.
|
|
8792
|
+
*
|
|
8793
|
+
* When BOTH `as` and `asChild` are passed, `asChild` wins. Same rule
|
|
8794
|
+
* as Typography — documented to avoid the dimension-bloat of a
|
|
8795
|
+
* discriminated union over an 11-axis props type.
|
|
8796
|
+
*
|
|
8797
|
+
* Layering:
|
|
8798
|
+
* • Sits BENEATH every visual chrome — every card, every panel,
|
|
8799
|
+
* every section background.
|
|
8800
|
+
* • Composes ON TOP of @dashforge/tw-tokens (color + spacing + radius
|
|
8801
|
+
* + shadow scales) via the dashforgePreset CSS variables.
|
|
8802
|
+
* • Pairs with Typography for text content, Stack/Grid for layout
|
|
8803
|
+
* children inside it.
|
|
8804
|
+
*/ const Box = /*#__PURE__*/ forwardRef(function Box(props, ref) {
|
|
8805
|
+
const { variant, color, elevation, rounded, p, px, py, m, mx, my, fullWidth, fullHeight, as, asChild = false, sx, visibleWhen, access, children } = props, rest = _object_without_properties_loose(props, [
|
|
8806
|
+
"variant",
|
|
8807
|
+
"color",
|
|
8808
|
+
"elevation",
|
|
8809
|
+
"rounded",
|
|
8810
|
+
"p",
|
|
8811
|
+
"px",
|
|
8812
|
+
"py",
|
|
8813
|
+
"m",
|
|
8814
|
+
"mx",
|
|
8815
|
+
"my",
|
|
8816
|
+
"fullWidth",
|
|
8817
|
+
"fullHeight",
|
|
8818
|
+
"as",
|
|
8819
|
+
"asChild",
|
|
8820
|
+
"sx",
|
|
8821
|
+
"visibleWhen",
|
|
8822
|
+
"access",
|
|
8823
|
+
"children"
|
|
8824
|
+
]);
|
|
8825
|
+
// Bridge — hooks called unconditionally (rules-of-hooks).
|
|
8826
|
+
// `useEngineVisibility` subscribes to engine state inside a
|
|
8827
|
+
// `<DashForm>` and re-evaluates the predicate reactively; outside
|
|
8828
|
+
// a form, it evaluates the predicate as a plain closure.
|
|
8829
|
+
// `useAccessState` consults the @dashforge/rbac policy engine.
|
|
8830
|
+
// Both default to no-op when the corresponding prop is omitted.
|
|
8831
|
+
const bridge = useContext(DashFormContext);
|
|
8832
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
8833
|
+
const accessState = useAccessState(access);
|
|
8834
|
+
// Early return — predicate false OR RBAC denies visibility.
|
|
8835
|
+
if (!isVisible || !accessState.visible) return null;
|
|
8836
|
+
const classes = cn(boxVariants({
|
|
8837
|
+
variant,
|
|
8838
|
+
color,
|
|
8839
|
+
elevation,
|
|
8840
|
+
rounded,
|
|
8841
|
+
p,
|
|
8842
|
+
px,
|
|
8843
|
+
py,
|
|
8844
|
+
m,
|
|
8845
|
+
mx,
|
|
8846
|
+
my,
|
|
8847
|
+
fullWidth,
|
|
8848
|
+
fullHeight
|
|
8849
|
+
}), // RBAC-disabled / readonly surfaces dim visually + carry
|
|
8850
|
+
// semantic ARIA attributes (assigned below) so descendants and
|
|
8851
|
+
// assistive tech can react.
|
|
8852
|
+
accessState.disabled && 'opacity-60', accessState.readonly && 'opacity-80', sx);
|
|
8853
|
+
const ariaProps = {
|
|
8854
|
+
'aria-disabled': accessState.disabled || undefined,
|
|
8855
|
+
'aria-readonly': accessState.readonly || undefined,
|
|
8856
|
+
'data-disabled': accessState.disabled || undefined,
|
|
8857
|
+
'data-readonly': accessState.readonly || undefined
|
|
8858
|
+
};
|
|
8859
|
+
if (asChild) {
|
|
8860
|
+
return jsx(Slot, _extends({
|
|
8861
|
+
ref: ref,
|
|
8862
|
+
className: classes
|
|
8863
|
+
}, ariaProps, rest, {
|
|
8864
|
+
children: children
|
|
8865
|
+
}));
|
|
8866
|
+
}
|
|
8867
|
+
const Tag = as != null ? as : 'div';
|
|
8868
|
+
return jsx(Tag, _extends({
|
|
8869
|
+
ref: ref,
|
|
8870
|
+
className: classes
|
|
8871
|
+
}, ariaProps, rest, {
|
|
8872
|
+
children: children
|
|
8873
|
+
}));
|
|
8874
|
+
});
|
|
8875
|
+
Box.displayName = 'Box';
|
|
8876
|
+
|
|
8877
|
+
/**
|
|
8878
|
+
* `<Card>` — opinionated surface preset over `<Box>`.
|
|
8879
|
+
*
|
|
8880
|
+
* Thin alias that pre-fills card-shaped Box defaults (`outlined`,
|
|
8881
|
+
* `rounded='lg'`, `elevation=1`, no padding). Reads as `<Card>` in
|
|
8882
|
+
* consumer code while reusing every Box affordance: surface variants,
|
|
8883
|
+
* 7 intent colors, spacing scale, polymorphism, and the bridge
|
|
8884
|
+
* integration (`access` + `visibleWhen`) retrofitted to Box in 1.1.0.
|
|
8885
|
+
*
|
|
8886
|
+
* Restricted variant axis: only `'outlined' | 'elevated' | 'plain'`.
|
|
8887
|
+
* `'soft'` and `'solid'` are deliberately excluded — they belong to
|
|
8888
|
+
* Box (as `<Alert>`-shaped callouts) and to banners, not to cards.
|
|
8889
|
+
*
|
|
8890
|
+
* @example
|
|
8891
|
+
* ```tsx
|
|
8892
|
+
* // Most common — outlined card with content padding via CardContent
|
|
8893
|
+
* <Card>
|
|
8894
|
+
* <CardContent>
|
|
8895
|
+
* <Typography variant="h3">Title</Typography>
|
|
8896
|
+
* <Typography variant="body">Body</Typography>
|
|
8897
|
+
* </CardContent>
|
|
8898
|
+
* </Card>
|
|
8899
|
+
*
|
|
8900
|
+
* // Elevated, clickable, gated by RBAC (inherits access from Box)
|
|
8901
|
+
* <Card
|
|
8902
|
+
* variant="elevated"
|
|
8903
|
+
* elevation={2}
|
|
8904
|
+
* access={{ resource: 'billing', action: 'read', onUnauthorized: 'hide' }}
|
|
8905
|
+
* >
|
|
8906
|
+
* <CardActionArea onClick={openBilling}>
|
|
8907
|
+
* <CardContent>
|
|
8908
|
+
* <Typography variant="h3">Revenue</Typography>
|
|
8909
|
+
* </CardContent>
|
|
8910
|
+
* </CardActionArea>
|
|
8911
|
+
* </Card>
|
|
8912
|
+
* ```
|
|
8913
|
+
*/ const Card = /*#__PURE__*/ forwardRef(function Card(props, ref) {
|
|
8914
|
+
const { variant = 'outlined', rounded = 'lg', elevation = 1, p = 0, children } = props, rest = _object_without_properties_loose(props, [
|
|
8915
|
+
"variant",
|
|
8916
|
+
"rounded",
|
|
8917
|
+
"elevation",
|
|
8918
|
+
"p",
|
|
8919
|
+
"children"
|
|
8920
|
+
]);
|
|
8921
|
+
return jsx(Box, _extends({
|
|
8922
|
+
ref: ref,
|
|
8923
|
+
variant: variant,
|
|
8924
|
+
rounded: rounded,
|
|
8925
|
+
elevation: elevation,
|
|
8926
|
+
p: p
|
|
8927
|
+
}, rest, {
|
|
8928
|
+
children: children
|
|
8929
|
+
}));
|
|
8930
|
+
});
|
|
8931
|
+
Card.displayName = 'Card';
|
|
8932
|
+
/**
|
|
8933
|
+
* `<CardContent>` — padded inner section of a Card.
|
|
8934
|
+
*
|
|
8935
|
+
* Semantic alias for an inner `<Box p={4}>`. The default padding step
|
|
8936
|
+
* (4) maps to `--df-tw-spacing-4` (1rem) via the dashforgePreset,
|
|
8937
|
+
* matching common card-content paddings across MUI / shadcn / Chakra.
|
|
8938
|
+
* Override `p` to dial up/down per the spacing token scale.
|
|
8939
|
+
*
|
|
8940
|
+
* @example
|
|
8941
|
+
* ```tsx
|
|
8942
|
+
* <Card>
|
|
8943
|
+
* <CardContent p={6}> {/* bigger inner padding *}/}
|
|
8944
|
+
* <Typography variant="h3">Spacious card</Typography>
|
|
8945
|
+
* </CardContent>
|
|
8946
|
+
* </Card>
|
|
8947
|
+
* ```
|
|
8948
|
+
*/ function CardContent({ p = 4, children, className, sx }) {
|
|
8949
|
+
return jsx(Box, {
|
|
8950
|
+
p: p,
|
|
8951
|
+
sx: cn(className, sx),
|
|
8952
|
+
children: children
|
|
8953
|
+
});
|
|
8954
|
+
}
|
|
8955
|
+
CardContent.displayName = 'CardContent';
|
|
8956
|
+
/**
|
|
8957
|
+
* `<CardActionArea>` — clickable wrapper for Card content.
|
|
8958
|
+
*
|
|
8959
|
+
* Adds focus ring + hover tint + Radix Slot polymorphism. Renders as
|
|
8960
|
+
* `<button type="button">` by default, or as the immediate child via
|
|
8961
|
+
* `asChild` (typical pattern: wrap a router `<Link>`).
|
|
8962
|
+
*
|
|
8963
|
+
* Bridge integration is local — CardActionArea is an action trigger
|
|
8964
|
+
* (not a surface), so it carries `access` + `visibleWhen` directly
|
|
8965
|
+
* (rather than relying on the wrapping Card's props). This lets the
|
|
8966
|
+
* consumer differentiate "the card is visible but not clickable" from
|
|
8967
|
+
* "the card is not visible at all".
|
|
8968
|
+
*
|
|
8969
|
+
* @example
|
|
8970
|
+
* ```tsx
|
|
8971
|
+
* // Card-as-CTA
|
|
8972
|
+
* <Card>
|
|
8973
|
+
* <CardActionArea onClick={navigate}>
|
|
8974
|
+
* <CardContent>
|
|
8975
|
+
* <Typography variant="h3">Open dashboard</Typography>
|
|
8976
|
+
* <Typography variant="body">Hover to see the focus ring.</Typography>
|
|
8977
|
+
* </CardContent>
|
|
8978
|
+
* </CardActionArea>
|
|
8979
|
+
* </Card>
|
|
8980
|
+
*
|
|
8981
|
+
* // Polymorphic — wrap a router Link
|
|
8982
|
+
* <Card>
|
|
8983
|
+
* <CardActionArea asChild>
|
|
8984
|
+
* <Link to="/billing">
|
|
8985
|
+
* <CardContent>
|
|
8986
|
+
* <Typography variant="h3">Billing</Typography>
|
|
8987
|
+
* </CardContent>
|
|
8988
|
+
* </Link>
|
|
8989
|
+
* </CardActionArea>
|
|
8990
|
+
* </Card>
|
|
8991
|
+
*
|
|
8992
|
+
* // Selectable card (filter / option pattern)
|
|
8993
|
+
* <CardActionArea
|
|
8994
|
+
* selected={selected === 'monthly'}
|
|
8995
|
+
* onClick={() => setSelected('monthly')}
|
|
8996
|
+
* >
|
|
8997
|
+
* <CardContent>Monthly plan</CardContent>
|
|
8998
|
+
* </CardActionArea>
|
|
8999
|
+
* ```
|
|
9000
|
+
*/ const CardActionArea = /*#__PURE__*/ forwardRef(function CardActionArea(props, ref) {
|
|
9001
|
+
const { children, asChild, selected, access, visibleWhen, disabled, sx, className, onClick } = props, rest = _object_without_properties_loose(props, [
|
|
9002
|
+
"children",
|
|
9003
|
+
"asChild",
|
|
9004
|
+
"selected",
|
|
9005
|
+
"access",
|
|
9006
|
+
"visibleWhen",
|
|
9007
|
+
"disabled",
|
|
9008
|
+
"sx",
|
|
9009
|
+
"className",
|
|
9010
|
+
"onClick"
|
|
9011
|
+
]);
|
|
9012
|
+
// Bridge — both hooks called unconditionally (rules-of-hooks).
|
|
9013
|
+
const bridge = useContext(DashFormContext);
|
|
9014
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
9015
|
+
const accessState = useAccessState(access);
|
|
9016
|
+
if (!isVisible || !accessState.visible) return null;
|
|
9017
|
+
const isDisabled = Boolean(disabled) || accessState.disabled || accessState.readonly;
|
|
9018
|
+
// The action area must SPAN the parent Card so the entire surface
|
|
9019
|
+
// is clickable. Includes focus ring, hover, and a subtle pressed
|
|
9020
|
+
// tint via `data-pressed`. Token-driven via dashforgePreset.
|
|
9021
|
+
const classes = cn('block w-full text-left', 'outline-none rounded-[inherit]', 'transition-colors', // Hover — gentle bg tint on top of the card surface. Uses
|
|
9022
|
+
// currentColor with 5% alpha so it composes with any Card
|
|
9023
|
+
// variant/color without hardcoding.
|
|
9024
|
+
'hover:bg-current/5', // Focus ring — keyboard-navigable, primary tone, inset slightly
|
|
9025
|
+
// so it traces the card's inherited radius.
|
|
9026
|
+
'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1', // Selected — adds a primary tone ring + soft tint so the card
|
|
9027
|
+
// visually reads as "active option" without changing the parent
|
|
9028
|
+
// Card's variant.
|
|
9029
|
+
selected && 'ring-2 ring-primary-500 ring-offset-1 bg-primary-50/60 dark:bg-primary-950/40', // Disabled — dims content and suppresses clicks.
|
|
9030
|
+
isDisabled && 'opacity-50 cursor-not-allowed pointer-events-none', sx, className);
|
|
9031
|
+
const handleClick = (e)=>{
|
|
9032
|
+
if (isDisabled) return;
|
|
9033
|
+
onClick == null ? void 0 : onClick(e);
|
|
9034
|
+
};
|
|
9035
|
+
if (asChild) {
|
|
9036
|
+
return jsx(Slot, {
|
|
9037
|
+
ref: ref,
|
|
9038
|
+
className: classes,
|
|
9039
|
+
"data-disabled": isDisabled || undefined,
|
|
9040
|
+
"aria-disabled": isDisabled || undefined,
|
|
9041
|
+
"aria-pressed": selected ? true : undefined,
|
|
9042
|
+
onClick: handleClick,
|
|
9043
|
+
children: children
|
|
9044
|
+
});
|
|
9045
|
+
}
|
|
9046
|
+
return jsx("button", _extends({
|
|
9047
|
+
ref: ref,
|
|
9048
|
+
type: "button",
|
|
9049
|
+
className: classes,
|
|
9050
|
+
disabled: isDisabled,
|
|
9051
|
+
"aria-pressed": selected ? true : undefined,
|
|
9052
|
+
onClick: handleClick
|
|
9053
|
+
}, rest, {
|
|
9054
|
+
children: children
|
|
9055
|
+
}));
|
|
9056
|
+
});
|
|
9057
|
+
CardActionArea.displayName = 'CardActionArea';
|
|
9058
|
+
|
|
9059
|
+
/**
|
|
9060
|
+
* Tailwind-variants recipe for `<Chip>` — promoted from the internal
|
|
9061
|
+
* `Table/cells/RenderChip` (Sprint 4.4). The visual treatment is
|
|
9062
|
+
* preserved byte-identical: the same 3 × 7 compound matrix
|
|
9063
|
+
* (variant × color) emitted by RenderChip is now the single source
|
|
9064
|
+
* of truth, consumed by both the public `<Chip>` and (via thin
|
|
9065
|
+
* wrapper) the legacy `RenderChip` internal.
|
|
9066
|
+
*
|
|
9067
|
+
* Slots (only `root` slot used by the public Chip; sub-slots
|
|
9068
|
+
* surfaced via separate inline classes in `Chip.tsx`):
|
|
9069
|
+
* - `root` — outer chip surface
|
|
9070
|
+
*
|
|
9071
|
+
* Variants axis (3 × 7, plus orthogonal `size`):
|
|
9072
|
+
* - `color`: 7 intents (`neutral / primary / secondary / success /
|
|
9073
|
+
* warning / danger / info`)
|
|
9074
|
+
* - `variant`: `soft` (default) / `solid` / `outline`
|
|
9075
|
+
* - `size`: `sm` (default) / `md`
|
|
9076
|
+
*
|
|
9077
|
+
* Token-driven: every Tailwind utility resolves through
|
|
9078
|
+
* `dashforgePreset()` → CSS variables. No hardcoded hex.
|
|
9079
|
+
*
|
|
9080
|
+
* `dark:` variants on color palettes (lines 49-77 of legacy
|
|
9081
|
+
* RenderChip, preserved here) are INTENTIONAL — color palettes do not
|
|
9082
|
+
* auto-invert via the CSS-var swap (only the `neutral` palette does),
|
|
9083
|
+
* so the `dark:` shift is a deliberate design choice (darker bg +
|
|
9084
|
+
* lighter text in dark mode for visual cohesion). `dark:` on the
|
|
9085
|
+
* `neutral` row is AVOIDED — that would double-invert.
|
|
9086
|
+
*/ const chipVariants = tv({
|
|
9087
|
+
base: [
|
|
9088
|
+
'inline-flex items-center gap-1 rounded-full font-medium',
|
|
9089
|
+
'whitespace-nowrap',
|
|
9090
|
+
// Smooth transitions for clickable / selected state changes.
|
|
9091
|
+
'transition-colors',
|
|
9092
|
+
'motion-reduce:transition-none'
|
|
9093
|
+
],
|
|
9094
|
+
variants: {
|
|
9095
|
+
color: {
|
|
9096
|
+
neutral: '',
|
|
9097
|
+
primary: '',
|
|
9098
|
+
secondary: '',
|
|
9099
|
+
success: '',
|
|
9100
|
+
warning: '',
|
|
9101
|
+
danger: '',
|
|
9102
|
+
info: ''
|
|
9103
|
+
},
|
|
9104
|
+
variant: {
|
|
9105
|
+
soft: '',
|
|
9106
|
+
solid: '',
|
|
9107
|
+
outline: ''
|
|
9108
|
+
},
|
|
9109
|
+
size: {
|
|
9110
|
+
sm: 'h-5 px-2 text-xs',
|
|
9111
|
+
md: 'h-6 px-2.5 text-xs'
|
|
9112
|
+
},
|
|
9113
|
+
/**
|
|
9114
|
+
* Clickable state — adds focus ring + hover transition. Applied
|
|
9115
|
+
* when the chip renders as `<button>` (i.e. `onClick` or
|
|
9116
|
+
* explicit `clickable` is set).
|
|
9117
|
+
*/ clickable: {
|
|
9118
|
+
true: [
|
|
9119
|
+
'cursor-pointer select-none',
|
|
9120
|
+
'outline-none focus-visible:ring-2 focus-visible:ring-offset-1'
|
|
9121
|
+
]
|
|
9122
|
+
},
|
|
9123
|
+
/**
|
|
9124
|
+
* Selected state — used on filter chips to indicate active
|
|
9125
|
+
* selection. Visually shifts the chip to a heavier emphasis
|
|
9126
|
+
* (one step up the elevation ladder of its current variant).
|
|
9127
|
+
* Compound rules below apply the actual shift per (variant, color)
|
|
9128
|
+
* cell.
|
|
9129
|
+
*/ selected: {
|
|
9130
|
+
true: 'ring-2 ring-offset-1'
|
|
9131
|
+
},
|
|
9132
|
+
/** Disabled state for clickable chips. */ disabled: {
|
|
9133
|
+
true: 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
9134
|
+
}
|
|
9135
|
+
},
|
|
9136
|
+
compoundVariants: [
|
|
9137
|
+
// ─── soft × color ──────────────────────────────────────────────
|
|
9138
|
+
// Subtle bg + readable text. Neutral row uses NO `dark:` (auto-
|
|
9139
|
+
// inverts via the dashforgePreset CSS-var swap). Color rows KEEP
|
|
9140
|
+
// `dark:` (color palettes don't auto-invert).
|
|
9141
|
+
{
|
|
9142
|
+
variant: 'soft',
|
|
9143
|
+
color: 'neutral',
|
|
9144
|
+
class: 'bg-neutral-100 text-neutral-700'
|
|
9145
|
+
},
|
|
9146
|
+
{
|
|
9147
|
+
variant: 'soft',
|
|
9148
|
+
color: 'primary',
|
|
9149
|
+
class: 'bg-primary-100 dark:bg-primary-950 text-primary-700 dark:text-primary-300'
|
|
9150
|
+
},
|
|
9151
|
+
{
|
|
9152
|
+
variant: 'soft',
|
|
9153
|
+
color: 'secondary',
|
|
9154
|
+
class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300'
|
|
9155
|
+
},
|
|
9156
|
+
{
|
|
9157
|
+
variant: 'soft',
|
|
9158
|
+
color: 'success',
|
|
9159
|
+
class: 'bg-success-100 dark:bg-success-950 text-success-700 dark:text-success-300'
|
|
9160
|
+
},
|
|
9161
|
+
{
|
|
9162
|
+
variant: 'soft',
|
|
9163
|
+
color: 'warning',
|
|
9164
|
+
class: 'bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-300'
|
|
9165
|
+
},
|
|
9166
|
+
{
|
|
9167
|
+
variant: 'soft',
|
|
9168
|
+
color: 'danger',
|
|
9169
|
+
class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300'
|
|
9170
|
+
},
|
|
9171
|
+
{
|
|
9172
|
+
variant: 'soft',
|
|
9173
|
+
color: 'info',
|
|
9174
|
+
class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300'
|
|
9175
|
+
},
|
|
9176
|
+
// ─── solid × color ─────────────────────────────────────────────
|
|
9177
|
+
// Full bg + contrasting text. Solid neutral uses neutral-700/50
|
|
9178
|
+
// (both on neutral palette → auto-invert together).
|
|
9179
|
+
// `text-white` is deliberately NOT used: white does not auto-invert,
|
|
9180
|
+
// which would flip the contrast in dark mode for the neutral row.
|
|
9181
|
+
{
|
|
9182
|
+
variant: 'solid',
|
|
9183
|
+
color: 'neutral',
|
|
9184
|
+
class: 'bg-neutral-700 text-neutral-50'
|
|
6965
9185
|
},
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
* so the prop is always available without conditional typing.
|
|
6971
|
-
*/ color: {
|
|
6972
|
-
primary: '',
|
|
6973
|
-
secondary: '',
|
|
6974
|
-
success: '',
|
|
6975
|
-
warning: '',
|
|
6976
|
-
danger: '',
|
|
6977
|
-
info: '',
|
|
6978
|
-
neutral: ''
|
|
9186
|
+
{
|
|
9187
|
+
variant: 'solid',
|
|
9188
|
+
color: 'primary',
|
|
9189
|
+
class: 'bg-primary-500 text-white'
|
|
6979
9190
|
},
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
* shadow (consistent with MUI's elevation=0).
|
|
6985
|
-
*/ elevation: {
|
|
6986
|
-
0: 'shadow-none',
|
|
6987
|
-
1: 'shadow-sm',
|
|
6988
|
-
2: 'shadow',
|
|
6989
|
-
3: 'shadow-md',
|
|
6990
|
-
4: 'shadow-lg',
|
|
6991
|
-
5: 'shadow-xl'
|
|
9191
|
+
{
|
|
9192
|
+
variant: 'solid',
|
|
9193
|
+
color: 'secondary',
|
|
9194
|
+
class: 'bg-secondary-500 text-white'
|
|
6992
9195
|
},
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
lg: 'rounded-lg',
|
|
6998
|
-
xl: 'rounded-xl',
|
|
6999
|
-
'2xl': 'rounded-2xl',
|
|
7000
|
-
full: 'rounded-full'
|
|
9196
|
+
{
|
|
9197
|
+
variant: 'solid',
|
|
9198
|
+
color: 'success',
|
|
9199
|
+
class: 'bg-success-500 text-white'
|
|
7001
9200
|
},
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
*/ p: {
|
|
7007
|
-
0: 'p-0',
|
|
7008
|
-
'0.5': 'p-0.5',
|
|
7009
|
-
1: 'p-1',
|
|
7010
|
-
2: 'p-2',
|
|
7011
|
-
3: 'p-3',
|
|
7012
|
-
4: 'p-4',
|
|
7013
|
-
6: 'p-6',
|
|
7014
|
-
8: 'p-8',
|
|
7015
|
-
12: 'p-12',
|
|
7016
|
-
16: 'p-16',
|
|
7017
|
-
24: 'p-24'
|
|
9201
|
+
{
|
|
9202
|
+
variant: 'solid',
|
|
9203
|
+
color: 'warning',
|
|
9204
|
+
class: 'bg-warning-500 text-white'
|
|
7018
9205
|
},
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
2: 'px-2',
|
|
7024
|
-
3: 'px-3',
|
|
7025
|
-
4: 'px-4',
|
|
7026
|
-
6: 'px-6',
|
|
7027
|
-
8: 'px-8',
|
|
7028
|
-
12: 'px-12',
|
|
7029
|
-
16: 'px-16',
|
|
7030
|
-
24: 'px-24'
|
|
9206
|
+
{
|
|
9207
|
+
variant: 'solid',
|
|
9208
|
+
color: 'danger',
|
|
9209
|
+
class: 'bg-danger-500 text-white'
|
|
7031
9210
|
},
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
2: 'py-2',
|
|
7037
|
-
3: 'py-3',
|
|
7038
|
-
4: 'py-4',
|
|
7039
|
-
6: 'py-6',
|
|
7040
|
-
8: 'py-8',
|
|
7041
|
-
12: 'py-12',
|
|
7042
|
-
16: 'py-16',
|
|
7043
|
-
24: 'py-24'
|
|
9211
|
+
{
|
|
9212
|
+
variant: 'solid',
|
|
9213
|
+
color: 'info',
|
|
9214
|
+
class: 'bg-info-500 text-white'
|
|
7044
9215
|
},
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
8: 'm-8',
|
|
7054
|
-
12: 'm-12',
|
|
7055
|
-
16: 'm-16',
|
|
7056
|
-
24: 'm-24'
|
|
9216
|
+
// ─── outline × color ───────────────────────────────────────────
|
|
9217
|
+
// Border + colored text. Outline neutral: both auto-invert via
|
|
9218
|
+
// CSS-var swap. Color rows: `dark:text-*-300` for legibility in
|
|
9219
|
+
// dark mode on a transparent background.
|
|
9220
|
+
{
|
|
9221
|
+
variant: 'outline',
|
|
9222
|
+
color: 'neutral',
|
|
9223
|
+
class: 'border border-neutral-300 text-neutral-700'
|
|
7057
9224
|
},
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
2: 'mx-2',
|
|
7063
|
-
3: 'mx-3',
|
|
7064
|
-
4: 'mx-4',
|
|
7065
|
-
6: 'mx-6',
|
|
7066
|
-
8: 'mx-8',
|
|
7067
|
-
12: 'mx-12',
|
|
7068
|
-
16: 'mx-16',
|
|
7069
|
-
24: 'mx-24'
|
|
9225
|
+
{
|
|
9226
|
+
variant: 'outline',
|
|
9227
|
+
color: 'primary',
|
|
9228
|
+
class: 'border border-primary-500 text-primary-700 dark:text-primary-300'
|
|
7070
9229
|
},
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
2: 'my-2',
|
|
7076
|
-
3: 'my-3',
|
|
7077
|
-
4: 'my-4',
|
|
7078
|
-
6: 'my-6',
|
|
7079
|
-
8: 'my-8',
|
|
7080
|
-
12: 'my-12',
|
|
7081
|
-
16: 'my-16',
|
|
7082
|
-
24: 'my-24'
|
|
9230
|
+
{
|
|
9231
|
+
variant: 'outline',
|
|
9232
|
+
color: 'secondary',
|
|
9233
|
+
class: 'border border-secondary-500 text-secondary-700 dark:text-secondary-300'
|
|
7083
9234
|
},
|
|
7084
|
-
|
|
7085
|
-
|
|
9235
|
+
{
|
|
9236
|
+
variant: 'outline',
|
|
9237
|
+
color: 'success',
|
|
9238
|
+
class: 'border border-success-500 text-success-700 dark:text-success-300'
|
|
7086
9239
|
},
|
|
7087
|
-
fullHeight: {
|
|
7088
|
-
true: 'h-full'
|
|
7089
|
-
}
|
|
7090
|
-
},
|
|
7091
|
-
/*
|
|
7092
|
-
* Compound variants — where surface × color get their actual visual.
|
|
7093
|
-
* Twenty-one entries: 7 (outlined) + 7 (soft) + 7 (solid).
|
|
7094
|
-
* `plain` and `elevated` don't appear here (no color contribution).
|
|
7095
|
-
*
|
|
7096
|
-
* Dark-mode pair is baked in: light-mode picks the 50-300 steps,
|
|
7097
|
-
* dark-mode picks the 800-950 steps — both reactive to setMode()
|
|
7098
|
-
* via the @dashforge/tw-theme CSS variables.
|
|
7099
|
-
*/ compoundVariants: [
|
|
7100
|
-
// ─── outlined × color ─────────────────────────────────────────────
|
|
7101
9240
|
{
|
|
7102
|
-
variant: '
|
|
9241
|
+
variant: 'outline',
|
|
9242
|
+
color: 'warning',
|
|
9243
|
+
class: 'border border-warning-500 text-warning-700 dark:text-warning-300'
|
|
9244
|
+
},
|
|
9245
|
+
{
|
|
9246
|
+
variant: 'outline',
|
|
9247
|
+
color: 'danger',
|
|
9248
|
+
class: 'border border-danger-500 text-danger-700 dark:text-danger-300'
|
|
9249
|
+
},
|
|
9250
|
+
{
|
|
9251
|
+
variant: 'outline',
|
|
9252
|
+
color: 'info',
|
|
9253
|
+
class: 'border border-info-500 text-info-700 dark:text-info-300'
|
|
9254
|
+
},
|
|
9255
|
+
// ─── clickable × color hover (subtle bg shift) ─────────────────
|
|
9256
|
+
// Hover state on clickable chips. Each (variant, color) hover
|
|
9257
|
+
// emits a one-tier deeper bg tint, working across both modes via
|
|
9258
|
+
// the same auto-invert logic as the base compound.
|
|
9259
|
+
{
|
|
9260
|
+
clickable: true,
|
|
9261
|
+
variant: 'soft',
|
|
9262
|
+
color: 'neutral',
|
|
9263
|
+
class: 'hover:bg-neutral-200'
|
|
9264
|
+
},
|
|
9265
|
+
{
|
|
9266
|
+
clickable: true,
|
|
9267
|
+
variant: 'soft',
|
|
7103
9268
|
color: 'primary',
|
|
7104
|
-
class: '
|
|
9269
|
+
class: 'hover:bg-primary-200 dark:hover:bg-primary-900'
|
|
7105
9270
|
},
|
|
7106
9271
|
{
|
|
7107
|
-
|
|
9272
|
+
clickable: true,
|
|
9273
|
+
variant: 'soft',
|
|
7108
9274
|
color: 'secondary',
|
|
7109
|
-
class: '
|
|
9275
|
+
class: 'hover:bg-secondary-200 dark:hover:bg-secondary-900'
|
|
7110
9276
|
},
|
|
7111
9277
|
{
|
|
7112
|
-
|
|
9278
|
+
clickable: true,
|
|
9279
|
+
variant: 'soft',
|
|
7113
9280
|
color: 'success',
|
|
7114
|
-
class: '
|
|
9281
|
+
class: 'hover:bg-success-200 dark:hover:bg-success-900'
|
|
7115
9282
|
},
|
|
7116
9283
|
{
|
|
7117
|
-
|
|
9284
|
+
clickable: true,
|
|
9285
|
+
variant: 'soft',
|
|
7118
9286
|
color: 'warning',
|
|
7119
|
-
class: '
|
|
9287
|
+
class: 'hover:bg-warning-200 dark:hover:bg-warning-900'
|
|
9288
|
+
},
|
|
9289
|
+
{
|
|
9290
|
+
clickable: true,
|
|
9291
|
+
variant: 'soft',
|
|
9292
|
+
color: 'danger',
|
|
9293
|
+
class: 'hover:bg-danger-200 dark:hover:bg-danger-900'
|
|
9294
|
+
},
|
|
9295
|
+
{
|
|
9296
|
+
clickable: true,
|
|
9297
|
+
variant: 'soft',
|
|
9298
|
+
color: 'info',
|
|
9299
|
+
class: 'hover:bg-info-200 dark:hover:bg-info-900'
|
|
9300
|
+
},
|
|
9301
|
+
// Solid hover → one tone darker
|
|
9302
|
+
{
|
|
9303
|
+
clickable: true,
|
|
9304
|
+
variant: 'solid',
|
|
9305
|
+
color: 'neutral',
|
|
9306
|
+
class: 'hover:bg-neutral-800'
|
|
9307
|
+
},
|
|
9308
|
+
{
|
|
9309
|
+
clickable: true,
|
|
9310
|
+
variant: 'solid',
|
|
9311
|
+
color: 'primary',
|
|
9312
|
+
class: 'hover:bg-primary-600'
|
|
9313
|
+
},
|
|
9314
|
+
{
|
|
9315
|
+
clickable: true,
|
|
9316
|
+
variant: 'solid',
|
|
9317
|
+
color: 'secondary',
|
|
9318
|
+
class: 'hover:bg-secondary-600'
|
|
9319
|
+
},
|
|
9320
|
+
{
|
|
9321
|
+
clickable: true,
|
|
9322
|
+
variant: 'solid',
|
|
9323
|
+
color: 'success',
|
|
9324
|
+
class: 'hover:bg-success-600'
|
|
9325
|
+
},
|
|
9326
|
+
{
|
|
9327
|
+
clickable: true,
|
|
9328
|
+
variant: 'solid',
|
|
9329
|
+
color: 'warning',
|
|
9330
|
+
class: 'hover:bg-warning-600'
|
|
9331
|
+
},
|
|
9332
|
+
{
|
|
9333
|
+
clickable: true,
|
|
9334
|
+
variant: 'solid',
|
|
9335
|
+
color: 'danger',
|
|
9336
|
+
class: 'hover:bg-danger-600'
|
|
9337
|
+
},
|
|
9338
|
+
{
|
|
9339
|
+
clickable: true,
|
|
9340
|
+
variant: 'solid',
|
|
9341
|
+
color: 'info',
|
|
9342
|
+
class: 'hover:bg-info-600'
|
|
9343
|
+
},
|
|
9344
|
+
// Outline hover → tinted bg appears (transparent → light tint)
|
|
9345
|
+
{
|
|
9346
|
+
clickable: true,
|
|
9347
|
+
variant: 'outline',
|
|
9348
|
+
color: 'neutral',
|
|
9349
|
+
class: 'hover:bg-neutral-100'
|
|
9350
|
+
},
|
|
9351
|
+
{
|
|
9352
|
+
clickable: true,
|
|
9353
|
+
variant: 'outline',
|
|
9354
|
+
color: 'primary',
|
|
9355
|
+
class: 'hover:bg-primary-50 dark:hover:bg-primary-950'
|
|
9356
|
+
},
|
|
9357
|
+
{
|
|
9358
|
+
clickable: true,
|
|
9359
|
+
variant: 'outline',
|
|
9360
|
+
color: 'secondary',
|
|
9361
|
+
class: 'hover:bg-secondary-50 dark:hover:bg-secondary-950'
|
|
9362
|
+
},
|
|
9363
|
+
{
|
|
9364
|
+
clickable: true,
|
|
9365
|
+
variant: 'outline',
|
|
9366
|
+
color: 'success',
|
|
9367
|
+
class: 'hover:bg-success-50 dark:hover:bg-success-950'
|
|
9368
|
+
},
|
|
9369
|
+
{
|
|
9370
|
+
clickable: true,
|
|
9371
|
+
variant: 'outline',
|
|
9372
|
+
color: 'warning',
|
|
9373
|
+
class: 'hover:bg-warning-50 dark:hover:bg-warning-950'
|
|
7120
9374
|
},
|
|
7121
9375
|
{
|
|
7122
|
-
|
|
9376
|
+
clickable: true,
|
|
9377
|
+
variant: 'outline',
|
|
7123
9378
|
color: 'danger',
|
|
7124
|
-
class: '
|
|
9379
|
+
class: 'hover:bg-danger-50 dark:hover:bg-danger-950'
|
|
7125
9380
|
},
|
|
7126
9381
|
{
|
|
7127
|
-
|
|
9382
|
+
clickable: true,
|
|
9383
|
+
variant: 'outline',
|
|
7128
9384
|
color: 'info',
|
|
7129
|
-
class: '
|
|
9385
|
+
class: 'hover:bg-info-50 dark:hover:bg-info-950'
|
|
7130
9386
|
},
|
|
9387
|
+
// ─── selected × color (focus ring tone) ────────────────────────
|
|
9388
|
+
// When selected, the ring color matches the chip's intent.
|
|
7131
9389
|
{
|
|
7132
|
-
|
|
9390
|
+
selected: true,
|
|
7133
9391
|
color: 'neutral',
|
|
7134
|
-
|
|
7135
|
-
// needed). bg-white is hardcoded — `dark:bg-neutral-100` keeps
|
|
7136
|
-
// elevation tier above page surface in dark mode (NOT
|
|
7137
|
-
// `dark:bg-neutral-900` which renders near-white).
|
|
7138
|
-
class: 'border-neutral-200 bg-white dark:bg-neutral-100'
|
|
9392
|
+
class: 'ring-neutral-400'
|
|
7139
9393
|
},
|
|
7140
|
-
// ─── soft × color ─────────────────────────────────────────────────
|
|
7141
9394
|
{
|
|
7142
|
-
|
|
9395
|
+
selected: true,
|
|
7143
9396
|
color: 'primary',
|
|
7144
|
-
class: '
|
|
9397
|
+
class: 'ring-primary-500'
|
|
7145
9398
|
},
|
|
7146
9399
|
{
|
|
7147
|
-
|
|
9400
|
+
selected: true,
|
|
7148
9401
|
color: 'secondary',
|
|
7149
|
-
class: '
|
|
9402
|
+
class: 'ring-secondary-500'
|
|
7150
9403
|
},
|
|
7151
9404
|
{
|
|
7152
|
-
|
|
9405
|
+
selected: true,
|
|
7153
9406
|
color: 'success',
|
|
7154
|
-
class: '
|
|
9407
|
+
class: 'ring-success-500'
|
|
7155
9408
|
},
|
|
7156
9409
|
{
|
|
7157
|
-
|
|
9410
|
+
selected: true,
|
|
7158
9411
|
color: 'warning',
|
|
7159
|
-
class: '
|
|
9412
|
+
class: 'ring-warning-500'
|
|
7160
9413
|
},
|
|
7161
9414
|
{
|
|
7162
|
-
|
|
9415
|
+
selected: true,
|
|
7163
9416
|
color: 'danger',
|
|
7164
|
-
class: '
|
|
9417
|
+
class: 'ring-danger-500'
|
|
7165
9418
|
},
|
|
7166
9419
|
{
|
|
7167
|
-
|
|
9420
|
+
selected: true,
|
|
7168
9421
|
color: 'info',
|
|
7169
|
-
class: '
|
|
9422
|
+
class: 'ring-info-500'
|
|
7170
9423
|
},
|
|
9424
|
+
// ─── clickable × color (focus ring tone) ───────────────────────
|
|
9425
|
+
// Tab-focused ring tone matches the chip color (same intent as
|
|
9426
|
+
// selected but only when keyboard-focused, via focus-visible).
|
|
7171
9427
|
{
|
|
7172
|
-
|
|
9428
|
+
clickable: true,
|
|
7173
9429
|
color: 'neutral',
|
|
7174
|
-
|
|
7175
|
-
// var swap — no `dark:` variants needed.
|
|
7176
|
-
class: 'bg-neutral-100 text-neutral-900'
|
|
9430
|
+
class: 'focus-visible:ring-neutral-400'
|
|
7177
9431
|
},
|
|
7178
|
-
// ─── solid × color ────────────────────────────────────────────────
|
|
7179
9432
|
{
|
|
7180
|
-
|
|
9433
|
+
clickable: true,
|
|
7181
9434
|
color: 'primary',
|
|
7182
|
-
class: '
|
|
9435
|
+
class: 'focus-visible:ring-primary-500'
|
|
7183
9436
|
},
|
|
7184
9437
|
{
|
|
7185
|
-
|
|
9438
|
+
clickable: true,
|
|
7186
9439
|
color: 'secondary',
|
|
7187
|
-
class: '
|
|
9440
|
+
class: 'focus-visible:ring-secondary-500'
|
|
7188
9441
|
},
|
|
7189
9442
|
{
|
|
7190
|
-
|
|
9443
|
+
clickable: true,
|
|
7191
9444
|
color: 'success',
|
|
7192
|
-
class: '
|
|
9445
|
+
class: 'focus-visible:ring-success-500'
|
|
7193
9446
|
},
|
|
7194
9447
|
{
|
|
7195
|
-
|
|
9448
|
+
clickable: true,
|
|
7196
9449
|
color: 'warning',
|
|
7197
|
-
class: '
|
|
9450
|
+
class: 'focus-visible:ring-warning-500'
|
|
7198
9451
|
},
|
|
7199
9452
|
{
|
|
7200
|
-
|
|
9453
|
+
clickable: true,
|
|
7201
9454
|
color: 'danger',
|
|
7202
|
-
class: '
|
|
9455
|
+
class: 'focus-visible:ring-danger-500'
|
|
7203
9456
|
},
|
|
7204
9457
|
{
|
|
7205
|
-
|
|
9458
|
+
clickable: true,
|
|
7206
9459
|
color: 'info',
|
|
7207
|
-
class: '
|
|
7208
|
-
},
|
|
7209
|
-
{
|
|
7210
|
-
variant: 'solid',
|
|
7211
|
-
color: 'neutral',
|
|
7212
|
-
// Solid neutral is the high-emphasis "always-foreground accent"
|
|
7213
|
-
// variant. Both classes auto-invert: dark surface + light text
|
|
7214
|
-
// in light mode (`#171717` / `#fafafa`) flips to light surface +
|
|
7215
|
-
// dark text in dark mode (`#f5f5f5` / `#0a0a0a`). The
|
|
7216
|
-
// accent inverts WITH the page surface — consistent with the
|
|
7217
|
-
// identity rule. Previously used `text-white` (no auto-invert)
|
|
7218
|
-
// which forced double-inversion to keep readable.
|
|
7219
|
-
class: 'bg-neutral-900 text-neutral-50'
|
|
9460
|
+
class: 'focus-visible:ring-info-500'
|
|
7220
9461
|
}
|
|
7221
9462
|
],
|
|
7222
9463
|
defaultVariants: {
|
|
7223
|
-
variant: 'plain',
|
|
7224
9464
|
color: 'neutral',
|
|
7225
|
-
|
|
7226
|
-
|
|
9465
|
+
variant: 'soft',
|
|
9466
|
+
size: 'sm'
|
|
9467
|
+
}
|
|
9468
|
+
});
|
|
9469
|
+
|
|
9470
|
+
/**
|
|
9471
|
+
* Default delete-button glyph — inline stroke SVG. Inherits the
|
|
9472
|
+
* chip's text colour via `currentColor`. No icon-library dep.
|
|
9473
|
+
*
|
|
9474
|
+
* @internal
|
|
9475
|
+
*/ function DefaultDeleteIcon() {
|
|
9476
|
+
return jsx("svg", {
|
|
9477
|
+
width: "0.875em",
|
|
9478
|
+
height: "0.875em",
|
|
9479
|
+
viewBox: "0 0 14 14",
|
|
9480
|
+
fill: "none",
|
|
9481
|
+
stroke: "currentColor",
|
|
9482
|
+
strokeWidth: 1.75,
|
|
9483
|
+
strokeLinecap: "round",
|
|
9484
|
+
strokeLinejoin: "round",
|
|
9485
|
+
"aria-hidden": "true",
|
|
9486
|
+
children: jsx("path", {
|
|
9487
|
+
d: "m4 4 6 6M10 4l-6 6"
|
|
9488
|
+
})
|
|
9489
|
+
});
|
|
9490
|
+
}
|
|
9491
|
+
/**
|
|
9492
|
+
* Dashforge TW Chip — status / filter / tag pill.
|
|
9493
|
+
*
|
|
9494
|
+
* Promoted from the internal `Table/cells/RenderChip` (Sprint 4.4):
|
|
9495
|
+
* preserves the 3 × 7 variant × color matrix byte-identical and adds
|
|
9496
|
+
* the public API (label, icon, avatar, clickable, onClick, onDelete,
|
|
9497
|
+
* selected, access).
|
|
9498
|
+
*
|
|
9499
|
+
* **Rendering modes** (chosen at runtime from props):
|
|
9500
|
+
* - **Static** (default) → `<span>`. Pure display.
|
|
9501
|
+
* - **Clickable** (`clickable` true OR `onClick` provided) →
|
|
9502
|
+
* `<button type="button">` with focus ring + hover state.
|
|
9503
|
+
* - **Deletable** (`onDelete` provided) → root + trailing `<button>`
|
|
9504
|
+
* for the delete glyph. Static or clickable root depending on
|
|
9505
|
+
* whether `onClick` is also set.
|
|
9506
|
+
*
|
|
9507
|
+
* **Variant axis** is `soft | solid | outline` (Dashforge chip
|
|
9508
|
+
* vocabulary, default `'soft'`). Distinct from Button's
|
|
9509
|
+
* (`solid | outline | ghost | link`) and Alert's
|
|
9510
|
+
* (`standard | filled | outlined`) by design — see CHANGELOG.
|
|
9511
|
+
*
|
|
9512
|
+
* **A11y**:
|
|
9513
|
+
* - Static chip → no role (it's a `<span>`).
|
|
9514
|
+
* - Clickable chip → native `<button>` semantics.
|
|
9515
|
+
* - `selected` → `aria-pressed="true"` (so it announces as a toggle
|
|
9516
|
+
* button in pressed state).
|
|
9517
|
+
* - Delete button → its own `<button>` with `aria-label`
|
|
9518
|
+
* (defaults to `'Remove'`).
|
|
9519
|
+
*
|
|
9520
|
+
* @example
|
|
9521
|
+
* ```tsx
|
|
9522
|
+
* // Static status chip
|
|
9523
|
+
* <Chip label="Active" color="success" />
|
|
9524
|
+
*
|
|
9525
|
+
* // With icon
|
|
9526
|
+
* import { CheckIcon } from 'lucide-react';
|
|
9527
|
+
* <Chip label="Verified" color="success" icon={<CheckIcon size={14} />} />
|
|
9528
|
+
*
|
|
9529
|
+
* // Filter chip (clickable + toggle via selected)
|
|
9530
|
+
* <Chip
|
|
9531
|
+
* label="Last 7 days"
|
|
9532
|
+
* color="primary"
|
|
9533
|
+
* clickable
|
|
9534
|
+
* selected={range === '7d'}
|
|
9535
|
+
* onClick={() => setRange('7d')}
|
|
9536
|
+
* />
|
|
9537
|
+
*
|
|
9538
|
+
* // Removable tag
|
|
9539
|
+
* <Chip
|
|
9540
|
+
* label="design-system"
|
|
9541
|
+
* color="info"
|
|
9542
|
+
* variant="outline"
|
|
9543
|
+
* onDelete={() => removeTag('design-system')}
|
|
9544
|
+
* />
|
|
9545
|
+
*
|
|
9546
|
+
* // RBAC-gated admin chip
|
|
9547
|
+
* <Chip
|
|
9548
|
+
* label="Admin"
|
|
9549
|
+
* color="danger"
|
|
9550
|
+
* variant="solid"
|
|
9551
|
+
* access={{ resource: 'role', action: 'read', onUnauthorized: 'hide' }}
|
|
9552
|
+
* />
|
|
9553
|
+
* ```
|
|
9554
|
+
*/ const Chip = /*#__PURE__*/ forwardRef(function Chip(props, ref) {
|
|
9555
|
+
const { label, icon, avatar, variant, color, size, clickable: clickableProp, onClick, onDelete, deleteIcon, deleteLabel = 'Remove', selected, disabled: disabledProp, visibleWhen, access, sx, className } = props;
|
|
9556
|
+
// Bridge — both hooks called unconditionally (rules-of-hooks).
|
|
9557
|
+
// Inside a `<DashForm>`, `useEngineVisibility` subscribes to engine
|
|
9558
|
+
// state and re-evaluates the predicate on changes; outside a form,
|
|
9559
|
+
// the predicate is called with `null` engine (consumer captures
|
|
9560
|
+
// external state in the closure).
|
|
9561
|
+
const bridge = useContext(DashFormContext);
|
|
9562
|
+
const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
|
|
9563
|
+
const accessState = useAccessState(access);
|
|
9564
|
+
if (!isVisible || !accessState.visible) return null;
|
|
9565
|
+
// Effective interactivity / disabled — mirrors Button semantics.
|
|
9566
|
+
const isClickable = Boolean(clickableProp != null ? clickableProp : onClick);
|
|
9567
|
+
const isDisabled = Boolean(disabledProp) || accessState.disabled || accessState.readonly;
|
|
9568
|
+
const classes = cn(chipVariants({
|
|
9569
|
+
color,
|
|
9570
|
+
variant,
|
|
9571
|
+
size,
|
|
9572
|
+
clickable: isClickable || undefined,
|
|
9573
|
+
selected: selected || undefined,
|
|
9574
|
+
disabled: isDisabled || undefined
|
|
9575
|
+
}), sx, className);
|
|
9576
|
+
// ─── Inner content — leading slot + label + delete ────────────
|
|
9577
|
+
// Avatar wins over icon when both provided (MUI parity).
|
|
9578
|
+
// Label is rendered as a direct text node (no wrapping span) so
|
|
9579
|
+
// the chip DOM stays minimal and consumers that target the root
|
|
9580
|
+
// via `getByText(label)` continue working. The root's
|
|
9581
|
+
// `inline-flex items-center` provides vertical alignment with
|
|
9582
|
+
// any leading / trailing slots.
|
|
9583
|
+
const leading = avatar != null ? avatar : icon;
|
|
9584
|
+
const innerContent = jsxs(Fragment$1, {
|
|
9585
|
+
children: [
|
|
9586
|
+
leading != null && jsx("span", {
|
|
9587
|
+
className: "-ml-0.5 inline-flex items-center shrink-0",
|
|
9588
|
+
children: leading
|
|
9589
|
+
}),
|
|
9590
|
+
label,
|
|
9591
|
+
onDelete != null && jsx("button", {
|
|
9592
|
+
type: "button",
|
|
9593
|
+
"aria-label": deleteLabel,
|
|
9594
|
+
onClick: (e)=>{
|
|
9595
|
+
// Stop propagation so the chip's own onClick doesn't fire
|
|
9596
|
+
// when the user clicks the × — MUI matches this behaviour.
|
|
9597
|
+
e.stopPropagation();
|
|
9598
|
+
if (!isDisabled) onDelete(e);
|
|
9599
|
+
},
|
|
9600
|
+
disabled: isDisabled,
|
|
9601
|
+
className: cn('ml-0.5 -mr-1 inline-flex items-center justify-center', 'h-4 w-4 rounded-full opacity-70 hover:opacity-100', 'outline-none focus-visible:ring-2 focus-visible:ring-current', 'transition-opacity'),
|
|
9602
|
+
children: deleteIcon != null ? deleteIcon : jsx(DefaultDeleteIcon, {})
|
|
9603
|
+
})
|
|
9604
|
+
]
|
|
9605
|
+
});
|
|
9606
|
+
// ─── Interactive root (clickable) ─────────────────────────────
|
|
9607
|
+
if (isClickable) {
|
|
9608
|
+
return jsx("button", {
|
|
9609
|
+
ref: ref,
|
|
9610
|
+
type: "button",
|
|
9611
|
+
className: classes,
|
|
9612
|
+
disabled: isDisabled,
|
|
9613
|
+
"aria-pressed": selected ? true : undefined,
|
|
9614
|
+
onClick: (e)=>{
|
|
9615
|
+
if (isDisabled) return;
|
|
9616
|
+
onClick == null ? void 0 : onClick(e);
|
|
9617
|
+
},
|
|
9618
|
+
onKeyDown: (e)=>{
|
|
9619
|
+
},
|
|
9620
|
+
children: innerContent
|
|
9621
|
+
});
|
|
9622
|
+
}
|
|
9623
|
+
// ─── Static root (display only) ───────────────────────────────
|
|
9624
|
+
return jsx("span", {
|
|
9625
|
+
ref: ref,
|
|
9626
|
+
className: classes,
|
|
9627
|
+
"aria-disabled": isDisabled || undefined,
|
|
9628
|
+
children: innerContent
|
|
9629
|
+
});
|
|
9630
|
+
});
|
|
9631
|
+
|
|
9632
|
+
/**
|
|
9633
|
+
* `typographyVariants` — the full type scale for the @dashforge/tw library.
|
|
9634
|
+
*
|
|
9635
|
+
* Mirrors the MUI Typography variant set (h1–h6 · subtitle1/2 · body1/2 ·
|
|
9636
|
+
* caption · overline) so the mental model carries over for developers
|
|
9637
|
+
* moving between the two ecosystems. Each variant baseline maps to a
|
|
9638
|
+
* Tailwind utility chain that resolves through the @dashforge/tw-tokens
|
|
9639
|
+
* scale (so the visual stays in sync with the rest of the system when the
|
|
9640
|
+
* token theme is patched).
|
|
9641
|
+
*
|
|
9642
|
+
* Variant axes are intentionally ORTHOGONAL — `variant` chooses the type
|
|
9643
|
+
* scale, `weight` overrides the variant's default weight (useful for "h2
|
|
9644
|
+
* but lighter"), `color` picks the intent, `align` picks the axis. They
|
|
9645
|
+
* never collide, so consumers can mix them freely.
|
|
9646
|
+
*
|
|
9647
|
+
* Two boolean flags (`truncate`, `noWrap`) encode the most common one-line
|
|
9648
|
+
* patterns; `gutterBottom` adds the conventional bottom margin used when a
|
|
9649
|
+
* heading precedes a paragraph block (mirror of MUI's same flag).
|
|
9650
|
+
*/ const typographyVariants = tv({
|
|
9651
|
+
base: 'text-inherit',
|
|
9652
|
+
variants: {
|
|
9653
|
+
/*
|
|
9654
|
+
* `variant` is the type-scale axis.
|
|
9655
|
+
*
|
|
9656
|
+
* The default font-weight is baked into each variant (headings come
|
|
9657
|
+
* with semibold/bold by default). Consumers override per-instance via
|
|
9658
|
+
* the `weight` axis below — when set, `weight` wins because it's
|
|
9659
|
+
* declared later in the cn() chain and tailwind-merge resolves the
|
|
9660
|
+
* last `font-*` to win.
|
|
9661
|
+
*/ variant: {
|
|
9662
|
+
h1: 'text-5xl font-bold leading-[1.05] tracking-[-0.025em]',
|
|
9663
|
+
h2: 'text-4xl font-bold leading-[1.1] tracking-[-0.022em]',
|
|
9664
|
+
h3: 'text-3xl font-semibold leading-[1.15] tracking-[-0.02em]',
|
|
9665
|
+
h4: 'text-2xl font-semibold leading-snug tracking-[-0.015em]',
|
|
9666
|
+
h5: 'text-xl font-semibold leading-snug',
|
|
9667
|
+
h6: 'text-lg font-semibold leading-normal',
|
|
9668
|
+
subtitle1: 'text-base font-medium leading-relaxed',
|
|
9669
|
+
subtitle2: 'text-sm font-medium leading-relaxed',
|
|
9670
|
+
body1: 'text-base font-normal leading-relaxed',
|
|
9671
|
+
body2: 'text-sm font-normal leading-relaxed',
|
|
9672
|
+
caption: 'text-xs font-normal leading-normal',
|
|
9673
|
+
overline: 'text-xs font-semibold uppercase tracking-[0.12em] leading-normal'
|
|
9674
|
+
},
|
|
9675
|
+
/*
|
|
9676
|
+
* `color` is the intent axis. Pairs with the @dashforge/tw-theme
|
|
9677
|
+
* reactive colour vars so the choice survives theme patches and dark
|
|
9678
|
+
* mode flips. `inherit` is the escape hatch — used inside a Box that
|
|
9679
|
+
* has set its own color (e.g. `<Box variant="solid" color="primary">`
|
|
9680
|
+
* paints white text).
|
|
9681
|
+
*/ color: {
|
|
9682
|
+
// `inherit` is the explicit escape hatch — use it on a
|
|
9683
|
+
// `<Typography>` inside a coloured surface (e.g.
|
|
9684
|
+
// `<Box variant="solid" color="primary">` paints white text)
|
|
9685
|
+
// so the text picks up the surface colour. It is NO LONGER the
|
|
9686
|
+
// default: a bare `<Typography>` would otherwise inherit the
|
|
9687
|
+
// consuming app's arbitrary root colour (often OS-`prefers-
|
|
9688
|
+
// color-scheme` driven), which diverges from the Dashforge
|
|
9689
|
+
// `data-dash-tw-theme` mode and breaks colour consistency in
|
|
9690
|
+
// dark mode. The default is now `neutral` — see `defaultVariants`.
|
|
9691
|
+
inherit: 'text-inherit',
|
|
9692
|
+
primary: 'text-primary-700 dark:text-primary-400',
|
|
9693
|
+
secondary: 'text-secondary-700 dark:text-secondary-400',
|
|
9694
|
+
success: 'text-success-700 dark:text-success-400',
|
|
9695
|
+
warning: 'text-warning-700 dark:text-warning-400',
|
|
9696
|
+
danger: 'text-danger-700 dark:text-danger-400',
|
|
9697
|
+
info: 'text-info-700 dark:text-info-400',
|
|
9698
|
+
// Neutral palette auto-inverts via the dashforgePreset() CSS-var
|
|
9699
|
+
// swap — no `dark:` variants. `text-neutral-900` resolves to
|
|
9700
|
+
// dark text in light mode and light text in dark mode; same
|
|
9701
|
+
// applies to `text-neutral-600` (muted).
|
|
9702
|
+
neutral: 'text-neutral-900',
|
|
9703
|
+
muted: 'text-neutral-600'
|
|
9704
|
+
},
|
|
9705
|
+
/*
|
|
9706
|
+
* `weight` overrides the variant's default weight. When unset, the
|
|
9707
|
+
* variant's own weight wins. When set, this axis appears LATER in the
|
|
9708
|
+
* cn() chain so tailwind-merge resolves to this value.
|
|
9709
|
+
*/ weight: {
|
|
9710
|
+
normal: 'font-normal',
|
|
9711
|
+
medium: 'font-medium',
|
|
9712
|
+
semibold: 'font-semibold',
|
|
9713
|
+
bold: 'font-bold',
|
|
9714
|
+
extrabold: 'font-extrabold'
|
|
9715
|
+
},
|
|
9716
|
+
align: {
|
|
9717
|
+
left: 'text-left',
|
|
9718
|
+
center: 'text-center',
|
|
9719
|
+
right: 'text-right',
|
|
9720
|
+
justify: 'text-justify'
|
|
9721
|
+
},
|
|
9722
|
+
/*
|
|
9723
|
+
* `truncate` collapses to a one-line ellipsis. `noWrap` is the looser
|
|
9724
|
+
* sibling — keeps the text on one line but lets it overflow without
|
|
9725
|
+
* the `…`. Mutually-exclusive intent-wise; if both are passed,
|
|
9726
|
+
* `truncate` wins (later in the cn() chain).
|
|
9727
|
+
*/ truncate: {
|
|
9728
|
+
true: 'truncate'
|
|
9729
|
+
},
|
|
9730
|
+
noWrap: {
|
|
9731
|
+
true: 'whitespace-nowrap'
|
|
9732
|
+
},
|
|
9733
|
+
/*
|
|
9734
|
+
* `gutterBottom` adds the conventional bottom margin used when a
|
|
9735
|
+
* heading precedes a paragraph block. Mirror of MUI's same prop —
|
|
9736
|
+
* familiar to developers crossing from the MUI side.
|
|
9737
|
+
*/ gutterBottom: {
|
|
9738
|
+
true: 'mb-3'
|
|
9739
|
+
}
|
|
9740
|
+
},
|
|
9741
|
+
defaultVariants: {
|
|
9742
|
+
variant: 'body1',
|
|
9743
|
+
// `neutral` (not `inherit`): a bare `<Typography>` emits
|
|
9744
|
+
// `text-neutral-900`, which auto-inverts via the dashforgePreset()
|
|
9745
|
+
// CSS-var swap and follows the Dashforge mode. Pass
|
|
9746
|
+
// `color="inherit"` explicitly inside a coloured surface.
|
|
9747
|
+
color: 'neutral',
|
|
9748
|
+
align: 'left'
|
|
7227
9749
|
}
|
|
7228
9750
|
});
|
|
7229
9751
|
|
|
7230
9752
|
/**
|
|
7231
|
-
*
|
|
7232
|
-
*
|
|
7233
|
-
*
|
|
7234
|
-
*
|
|
7235
|
-
*
|
|
7236
|
-
*
|
|
7237
|
-
*
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
9753
|
+
* Default HTML tag per variant. Headings get their semantic level (h1→h1
|
|
9754
|
+
* etc.); subtitle/body get `<p>` (block, paragraph semantics);
|
|
9755
|
+
* caption/overline get `<span>` (inline, no implicit block break).
|
|
9756
|
+
*
|
|
9757
|
+
* Override per-instance via the `as` prop — useful when the semantic
|
|
9758
|
+
* heading level should differ from the visual scale (e.g. a hero
|
|
9759
|
+
* rendered as `<h2>` but visually styled `h1`).
|
|
9760
|
+
*/ const VARIANT_TO_TAG = {
|
|
9761
|
+
h1: 'h1',
|
|
9762
|
+
h2: 'h2',
|
|
9763
|
+
h3: 'h3',
|
|
9764
|
+
h4: 'h4',
|
|
9765
|
+
h5: 'h5',
|
|
9766
|
+
h6: 'h6',
|
|
9767
|
+
subtitle1: 'p',
|
|
9768
|
+
subtitle2: 'p',
|
|
9769
|
+
body1: 'p',
|
|
9770
|
+
body2: 'p',
|
|
9771
|
+
caption: 'span',
|
|
9772
|
+
overline: 'span'
|
|
9773
|
+
};
|
|
9774
|
+
/**
|
|
9775
|
+
* `<Typography>` — semantic typed text, the foundation of every readable
|
|
9776
|
+
* surface in @dashforge/tw.
|
|
7244
9777
|
*
|
|
7245
|
-
*
|
|
7246
|
-
*
|
|
7247
|
-
*
|
|
7248
|
-
*
|
|
7249
|
-
*
|
|
9778
|
+
* Why this exists:
|
|
9779
|
+
* Tailwind ships a typographic scale (`text-xl`, `font-bold`,
|
|
9780
|
+
* `leading-relaxed`) but leaves the SEMANTIC HTML tag and the
|
|
9781
|
+
* intent-coloured palette to the consumer. That's fine for one-off
|
|
9782
|
+
* marketing surfaces, but at app scale it means every `<h2>` and every
|
|
9783
|
+
* body paragraph re-derives its own utility chain — and they drift.
|
|
7250
9784
|
*
|
|
7251
|
-
*
|
|
7252
|
-
*
|
|
7253
|
-
*
|
|
9785
|
+
* Typography moves that decision into a typed prop set: the visual
|
|
9786
|
+
* scale, the intent colour, the alignment, the truncation all live in
|
|
9787
|
+
* `tailwind-variants` and resolve to the same utility chain everywhere
|
|
9788
|
+
* in the app. The default HTML tag is inferred from `variant` so the
|
|
9789
|
+
* semantic layer follows the visual layer; `as` and `asChild` are the
|
|
9790
|
+
* two escape hatches when you need something else.
|
|
7254
9791
|
*
|
|
7255
9792
|
* Layering:
|
|
7256
|
-
* • Sits BENEATH every
|
|
7257
|
-
*
|
|
7258
|
-
* • Composes ON TOP of
|
|
7259
|
-
*
|
|
7260
|
-
*
|
|
7261
|
-
*
|
|
7262
|
-
|
|
7263
|
-
|
|
9793
|
+
* • Sits BENEATH every component that renders text — `<Button>`'s
|
|
9794
|
+
* label, `<TextField>`'s helper text, MDX prose in our own docs.
|
|
9795
|
+
* • Composes ON TOP of `@dashforge/tw-tokens` colour scales (so
|
|
9796
|
+
* `color="primary"` paints `text-primary-700` in light, `-400` in dark,
|
|
9797
|
+
* reactive to `setMode()`).
|
|
9798
|
+
* • Polymorphic via Radix Slot — pairs cleanly with router `<Link>`,
|
|
9799
|
+
* `<button>`, or `<a>` without injecting an extra wrapper element.
|
|
9800
|
+
*
|
|
9801
|
+
* Polymorphism rules — `as` vs `asChild`:
|
|
9802
|
+
* • `as` swaps the rendered tag (we still render the element ourselves).
|
|
9803
|
+
* • `asChild` removes our element entirely — the single React child
|
|
9804
|
+
* becomes the rendered tag, with our className/ref merged onto it.
|
|
9805
|
+
* This is the Radix Slot pattern; use when you need a router Link
|
|
9806
|
+
* to style as a heading.
|
|
9807
|
+
* When BOTH are passed, `asChild` wins. We considered making this a
|
|
9808
|
+
* compile-time error via discriminated unions, but the API surface
|
|
9809
|
+
* already has 8 axes and adding one more dimension to the props type
|
|
9810
|
+
* would inflate IntelliSense suggestions for marginal benefit. The
|
|
9811
|
+
* runtime preference is documented; the test asserts it.
|
|
9812
|
+
*/ const Typography = /*#__PURE__*/ forwardRef(function Typography(props, ref) {
|
|
9813
|
+
var _ref;
|
|
9814
|
+
const { variant = 'body1', color, weight, align, truncate, noWrap, gutterBottom, as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
|
|
7264
9815
|
"variant",
|
|
7265
9816
|
"color",
|
|
7266
|
-
"
|
|
7267
|
-
"
|
|
7268
|
-
"
|
|
7269
|
-
"
|
|
7270
|
-
"
|
|
7271
|
-
"m",
|
|
7272
|
-
"mx",
|
|
7273
|
-
"my",
|
|
7274
|
-
"fullWidth",
|
|
7275
|
-
"fullHeight",
|
|
9817
|
+
"weight",
|
|
9818
|
+
"align",
|
|
9819
|
+
"truncate",
|
|
9820
|
+
"noWrap",
|
|
9821
|
+
"gutterBottom",
|
|
7276
9822
|
"as",
|
|
7277
9823
|
"asChild",
|
|
7278
9824
|
"sx",
|
|
7279
9825
|
"children"
|
|
7280
9826
|
]);
|
|
7281
|
-
const classes = cn(
|
|
9827
|
+
const classes = cn(typographyVariants({
|
|
7282
9828
|
variant,
|
|
7283
9829
|
color,
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
m,
|
|
7290
|
-
mx,
|
|
7291
|
-
my,
|
|
7292
|
-
fullWidth,
|
|
7293
|
-
fullHeight
|
|
9830
|
+
weight,
|
|
9831
|
+
align,
|
|
9832
|
+
truncate,
|
|
9833
|
+
noWrap,
|
|
9834
|
+
gutterBottom
|
|
7294
9835
|
}), sx);
|
|
9836
|
+
// asChild wins over `as` when both are passed (see component header).
|
|
7295
9837
|
if (asChild) {
|
|
7296
9838
|
return jsx(Slot, _extends({
|
|
7297
9839
|
ref: ref,
|
|
@@ -7300,7 +9842,8 @@ Typography.displayName = 'Typography';
|
|
|
7300
9842
|
children: children
|
|
7301
9843
|
}));
|
|
7302
9844
|
}
|
|
7303
|
-
|
|
9845
|
+
// Resolve the rendered tag: explicit `as` > variant default > fallback.
|
|
9846
|
+
const Tag = (_ref = as != null ? as : VARIANT_TO_TAG[variant]) != null ? _ref : 'span';
|
|
7304
9847
|
return jsx(Tag, _extends({
|
|
7305
9848
|
ref: ref,
|
|
7306
9849
|
className: classes
|
|
@@ -7308,7 +9851,7 @@ Typography.displayName = 'Typography';
|
|
|
7308
9851
|
children: children
|
|
7309
9852
|
}));
|
|
7310
9853
|
});
|
|
7311
|
-
|
|
9854
|
+
Typography.displayName = 'Typography';
|
|
7312
9855
|
|
|
7313
9856
|
/**
|
|
7314
9857
|
* `stackVariants` — flex container 1D, the layout primitive of @dashforge/tw.
|
|
@@ -8996,7 +11539,7 @@ const accordionVariants = tv({
|
|
|
8996
11539
|
// surface; the border gives the separation). `hover:bg-neutral-100`
|
|
8997
11540
|
// is the one-tier elevation that works in both modes.
|
|
8998
11541
|
summary: 'text-neutral-600',
|
|
8999
|
-
list: 'inline-flex items-center gap-1',
|
|
11542
|
+
list: 'inline-flex items-center gap-1 list-none pl-0 m-0',
|
|
9000
11543
|
pageButton: [
|
|
9001
11544
|
'inline-flex items-center justify-center rounded-md',
|
|
9002
11545
|
'border border-neutral-300',
|
|
@@ -10775,178 +13318,14 @@ function SearchIcon$1(props) {
|
|
|
10775
13318
|
});
|
|
10776
13319
|
}
|
|
10777
13320
|
|
|
10778
|
-
/**
|
|
10779
|
-
* Internal mini-chip used by the cell renderer library. Not exported
|
|
10780
|
-
* from `@dashforge/tw` as a standalone component (per Sprint 4.1
|
|
10781
|
-
* scope decision — a standalone `<Chip>` may ship in Sprint 5 if
|
|
10782
|
-
* demand emerges).
|
|
10783
|
-
*
|
|
10784
|
-
* Variants:
|
|
10785
|
-
* - `color`: 7 intents (neutral / primary / success / warning /
|
|
10786
|
-
* danger / info / secondary)
|
|
10787
|
-
* - `variant`: `soft` (default) / `solid` / `outline`
|
|
10788
|
-
* - `size`: `sm` / `md`
|
|
10789
|
-
*/ const chipVariants = tv({
|
|
10790
|
-
base: [
|
|
10791
|
-
'inline-flex items-center gap-1 rounded-full font-medium',
|
|
10792
|
-
'whitespace-nowrap'
|
|
10793
|
-
],
|
|
10794
|
-
variants: {
|
|
10795
|
-
color: {
|
|
10796
|
-
neutral: '',
|
|
10797
|
-
primary: '',
|
|
10798
|
-
success: '',
|
|
10799
|
-
warning: '',
|
|
10800
|
-
danger: '',
|
|
10801
|
-
info: '',
|
|
10802
|
-
secondary: ''
|
|
10803
|
-
},
|
|
10804
|
-
variant: {
|
|
10805
|
-
soft: '',
|
|
10806
|
-
solid: '',
|
|
10807
|
-
outline: ''
|
|
10808
|
-
},
|
|
10809
|
-
size: {
|
|
10810
|
-
sm: 'h-5 px-2 text-xs',
|
|
10811
|
-
md: 'h-6 px-2.5 text-xs'
|
|
10812
|
-
}
|
|
10813
|
-
},
|
|
10814
|
-
compoundVariants: [
|
|
10815
|
-
// soft = subtle bg + dark text.
|
|
10816
|
-
// Neutral row uses NO `dark:` variants — the dashforgePreset()
|
|
10817
|
-
// CSS-var swap auto-inverts the neutral palette already.
|
|
10818
|
-
// Color rows below KEEP `dark:` variants because color palettes
|
|
10819
|
-
// don't auto-invert: the `dark:` shift is an intentional design
|
|
10820
|
-
// choice (darker bg + lighter text in dark mode for visual cohesion).
|
|
10821
|
-
{
|
|
10822
|
-
variant: 'soft',
|
|
10823
|
-
color: 'neutral',
|
|
10824
|
-
class: 'bg-neutral-100 text-neutral-700'
|
|
10825
|
-
},
|
|
10826
|
-
{
|
|
10827
|
-
variant: 'soft',
|
|
10828
|
-
color: 'primary',
|
|
10829
|
-
class: 'bg-primary-100 dark:bg-primary-950 text-primary-700 dark:text-primary-300'
|
|
10830
|
-
},
|
|
10831
|
-
{
|
|
10832
|
-
variant: 'soft',
|
|
10833
|
-
color: 'success',
|
|
10834
|
-
class: 'bg-success-100 dark:bg-success-950 text-success-700 dark:text-success-300'
|
|
10835
|
-
},
|
|
10836
|
-
{
|
|
10837
|
-
variant: 'soft',
|
|
10838
|
-
color: 'warning',
|
|
10839
|
-
class: 'bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-300'
|
|
10840
|
-
},
|
|
10841
|
-
{
|
|
10842
|
-
variant: 'soft',
|
|
10843
|
-
color: 'danger',
|
|
10844
|
-
class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300'
|
|
10845
|
-
},
|
|
10846
|
-
{
|
|
10847
|
-
variant: 'soft',
|
|
10848
|
-
color: 'info',
|
|
10849
|
-
class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300'
|
|
10850
|
-
},
|
|
10851
|
-
{
|
|
10852
|
-
variant: 'soft',
|
|
10853
|
-
color: 'secondary',
|
|
10854
|
-
class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300'
|
|
10855
|
-
},
|
|
10856
|
-
// solid = full bg + contrasting text.
|
|
10857
|
-
// solid neutral: both `bg-neutral-700` and `text-neutral-50` sit
|
|
10858
|
-
// on the neutral palette, so they auto-invert together via the
|
|
10859
|
-
// CSS-var swap — bg/text contrast is preserved in BOTH modes.
|
|
10860
|
-
// (`text-white` is deliberately NOT used here: white does not
|
|
10861
|
-
// auto-invert, which would flip the contrast in dark mode.)
|
|
10862
|
-
{
|
|
10863
|
-
variant: 'solid',
|
|
10864
|
-
color: 'neutral',
|
|
10865
|
-
class: 'bg-neutral-700 text-neutral-50'
|
|
10866
|
-
},
|
|
10867
|
-
{
|
|
10868
|
-
variant: 'solid',
|
|
10869
|
-
color: 'primary',
|
|
10870
|
-
class: 'bg-primary-500 text-white'
|
|
10871
|
-
},
|
|
10872
|
-
{
|
|
10873
|
-
variant: 'solid',
|
|
10874
|
-
color: 'success',
|
|
10875
|
-
class: 'bg-success-500 text-white'
|
|
10876
|
-
},
|
|
10877
|
-
{
|
|
10878
|
-
variant: 'solid',
|
|
10879
|
-
color: 'warning',
|
|
10880
|
-
class: 'bg-warning-500 text-white'
|
|
10881
|
-
},
|
|
10882
|
-
{
|
|
10883
|
-
variant: 'solid',
|
|
10884
|
-
color: 'danger',
|
|
10885
|
-
class: 'bg-danger-500 text-white'
|
|
10886
|
-
},
|
|
10887
|
-
{
|
|
10888
|
-
variant: 'solid',
|
|
10889
|
-
color: 'info',
|
|
10890
|
-
class: 'bg-info-500 text-white'
|
|
10891
|
-
},
|
|
10892
|
-
{
|
|
10893
|
-
variant: 'solid',
|
|
10894
|
-
color: 'secondary',
|
|
10895
|
-
class: 'bg-secondary-500 text-white'
|
|
10896
|
-
},
|
|
10897
|
-
// outline = border + colored text
|
|
10898
|
-
// outline neutral: both border + text auto-invert via CSS-var swap.
|
|
10899
|
-
{
|
|
10900
|
-
variant: 'outline',
|
|
10901
|
-
color: 'neutral',
|
|
10902
|
-
class: 'border border-neutral-300 text-neutral-700'
|
|
10903
|
-
},
|
|
10904
|
-
{
|
|
10905
|
-
variant: 'outline',
|
|
10906
|
-
color: 'primary',
|
|
10907
|
-
class: 'border border-primary-500 text-primary-700 dark:text-primary-300'
|
|
10908
|
-
},
|
|
10909
|
-
{
|
|
10910
|
-
variant: 'outline',
|
|
10911
|
-
color: 'success',
|
|
10912
|
-
class: 'border border-success-500 text-success-700 dark:text-success-300'
|
|
10913
|
-
},
|
|
10914
|
-
{
|
|
10915
|
-
variant: 'outline',
|
|
10916
|
-
color: 'warning',
|
|
10917
|
-
class: 'border border-warning-500 text-warning-700 dark:text-warning-300'
|
|
10918
|
-
},
|
|
10919
|
-
{
|
|
10920
|
-
variant: 'outline',
|
|
10921
|
-
color: 'danger',
|
|
10922
|
-
class: 'border border-danger-500 text-danger-700 dark:text-danger-300'
|
|
10923
|
-
},
|
|
10924
|
-
{
|
|
10925
|
-
variant: 'outline',
|
|
10926
|
-
color: 'info',
|
|
10927
|
-
class: 'border border-info-500 text-info-700 dark:text-info-300'
|
|
10928
|
-
},
|
|
10929
|
-
{
|
|
10930
|
-
variant: 'outline',
|
|
10931
|
-
color: 'secondary',
|
|
10932
|
-
class: 'border border-secondary-500 text-secondary-700 dark:text-secondary-300'
|
|
10933
|
-
}
|
|
10934
|
-
],
|
|
10935
|
-
defaultVariants: {
|
|
10936
|
-
color: 'neutral',
|
|
10937
|
-
variant: 'soft',
|
|
10938
|
-
size: 'sm'
|
|
10939
|
-
}
|
|
10940
|
-
});
|
|
10941
13321
|
function RenderChip(props) {
|
|
10942
13322
|
const { children, color, variant, size, className } = props;
|
|
10943
|
-
return jsx(
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
children: children
|
|
13323
|
+
return jsx(Chip, {
|
|
13324
|
+
label: children,
|
|
13325
|
+
color: color,
|
|
13326
|
+
variant: variant,
|
|
13327
|
+
size: size,
|
|
13328
|
+
className: className
|
|
10950
13329
|
});
|
|
10951
13330
|
}
|
|
10952
13331
|
|
|
@@ -10992,7 +13371,7 @@ function RenderChip(props) {
|
|
|
10992
13371
|
side: "bottom",
|
|
10993
13372
|
align: "end",
|
|
10994
13373
|
content: jsx("ul", {
|
|
10995
|
-
className: "flex flex-col gap-0.5 min-w-[140px]",
|
|
13374
|
+
className: "flex flex-col gap-0.5 min-w-[140px] list-none pl-0 m-0",
|
|
10996
13375
|
role: "menu",
|
|
10997
13376
|
children: visibleActions.map((action, idx)=>jsx(RowActionMenuItem, {
|
|
10998
13377
|
action: action,
|
|
@@ -11812,7 +14191,7 @@ function ColumnVisibilityTrigger(props) {
|
|
|
11812
14191
|
]
|
|
11813
14192
|
}),
|
|
11814
14193
|
jsx("ul", {
|
|
11815
|
-
className: "flex flex-col gap-0.5 max-h-72 overflow-auto",
|
|
14194
|
+
className: "flex flex-col gap-0.5 max-h-72 overflow-auto list-none pl-0 m-0",
|
|
11816
14195
|
children: hideableCols.map((col)=>{
|
|
11817
14196
|
const field = col.field;
|
|
11818
14197
|
const hidden = hiddenColumns.includes(field);
|
|
@@ -12904,6 +15283,6 @@ function SearchIcon(props) {
|
|
|
12904
15283
|
*/ // ───── Components ─────
|
|
12905
15284
|
/**
|
|
12906
15285
|
* Package version (synced with `package.json` at publish time).
|
|
12907
|
-
*/ const VERSION = '
|
|
15286
|
+
*/ const VERSION = '1.1.0';
|
|
12908
15287
|
|
|
12909
|
-
export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Calendar, Checkbox, ConfirmDialogProvider, Container, DataGrid, DatePicker, DateRangePicker, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, Skeleton, SnackbarProvider, Stack, Switch, Table, Tabs, TextField, Textarea, TimePicker, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, calendarDayVariants, calendarVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, datePickerVariants, dateRangeDayVariants, dateRangePickerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, timePickerVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
|
|
15288
|
+
export { AVATAR_SOFT_FALLBACK, AVATAR_TONE_FALLBACK, Accordion, Alert, AlertTitle, AppShell, AspectRatio, Autocomplete, Avatar, AvatarGroup, Badge, Box, Breadcrumbs, Button, Calendar, Card, CardActionArea, CardContent, Checkbox, Chip, ConfirmDialogProvider, Container, DataGrid, DatePicker, DateRangePicker, DateTimePicker, Dialog, Divider, Grid, ICON_BUTTON_BASE, ICON_BUTTON_SIZE_OVERRIDES, IconButton, LeftNav, Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuSkeleton, MenuTrigger, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, SPINNER_COLORS, SPINNER_STROKE_WIDTH, SPINNER_TRACK_OPACITY, Skeleton, SnackbarProvider, Spinner, Stack, Switch, Table, Tabs, TextField, Textarea, TimePicker, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, alertVariants, appShellVariants, autocompleteVariants, avatarVariants, badgeVariants, boxVariants, breadcrumbsVariants, buttonVariants, calendarDayVariants, calendarVariants, checkboxVariants, chipVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, datePickerVariants, dateRangeDayVariants, dateRangePickerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, leftNavVariants, menuVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, spinnerVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, timePickerVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
|