@trackunit/react-components 1.13.10 → 1.13.11

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/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
2
  import { objectKeys, uuidv4, parseTailwindArbitraryValue, objectEntries, nonNullable, objectValues, filterByMultiple } from '@trackunit/shared-utils';
3
- import { useRef, useMemo, useEffect, useState, useLayoutEffect, useCallback, createElement, forwardRef, Fragment, memo, useReducer, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
4
- import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, themeFontSize, color } from '@trackunit/ui-design-tokens';
3
+ import { useRef, useMemo, useEffect, useState, useLayoutEffect, useCallback, createElement, forwardRef, Fragment, memo, useId, useReducer, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
4
+ import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, themeContainerSize, themeFontSize, color } from '@trackunit/ui-design-tokens';
5
5
  import { iconNames } from '@trackunit/ui-icons';
6
6
  import IconSpriteMicro from '@trackunit/ui-icons/icons-sprite-micro.svg';
7
7
  import IconSpriteMini from '@trackunit/ui-icons/icons-sprite-mini.svg';
@@ -12,8 +12,8 @@ import { cvaMerge } from '@trackunit/css-class-variance-utilities';
12
12
  import { Slottable, Slot } from '@radix-ui/react-slot';
13
13
  import { Link, useBlocker } from '@tanstack/react-router';
14
14
  import { isEqual, omit } from 'es-toolkit';
15
- import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, FloatingPortal, useMergeRefs as useMergeRefs$1, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
16
15
  import { twMerge } from 'tailwind-merge';
16
+ import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, FloatingPortal, useMergeRefs as useMergeRefs$1, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
17
17
  import { useVirtualizer } from '@tanstack/react-virtual';
18
18
  import { HelmetProvider, Helmet } from 'react-helmet-async';
19
19
  import { Trigger, Content, List as List$1, Root } from '@radix-ui/react-tabs';
@@ -2047,6 +2047,755 @@ const ExternalLink = ({ rel = "noreferrer", target = "_blank", href, className,
2047
2047
  return (jsx("a", { className: cvaExternalLink({ className, color }), "data-testid": dataTestId, href: href, onClick: onClick, rel: rel, target: target, title: title, children: children }));
2048
2048
  };
2049
2049
 
2050
+ // =============================================================================
2051
+ // Builder Implementation
2052
+ // =============================================================================
2053
+ /**
2054
+ * Creates a type-safe grid configuration using a fluent builder pattern.
2055
+ *
2056
+ * The builder provides:
2057
+ * - Full autocomplete for area names in layouts
2058
+ * - Type errors on the correct property (columns, rows, layout)
2059
+ * - Validation that layout cells match defined areas
2060
+ * - Validation that all `when` items appear in the layout
2061
+ *
2062
+ * @example Module-level definition (recommended for static grids)
2063
+ * ```tsx
2064
+ * const cardGrid = createGrid()
2065
+ * .areas(["icon", "info", "tag"])
2066
+ * .layout({ layout: [["info"]] })
2067
+ * .layout({
2068
+ * when: ["icon", "info"],
2069
+ * layout: [["icon", "info"]],
2070
+ * columns: ["48px", "1fr"],
2071
+ * })
2072
+ * .build();
2073
+ *
2074
+ * function MyCard() {
2075
+ * return (
2076
+ * <GridAreas config={cardGrid}>
2077
+ * {(slots) => (<>{slots.icon}...</>)}
2078
+ * </GridAreas>
2079
+ * );
2080
+ * }
2081
+ * ```
2082
+ */
2083
+ function createGrid() {
2084
+ return {
2085
+ areas(areas) {
2086
+ const layouts = [];
2087
+ const builder = {
2088
+ layout(config) {
2089
+ // At runtime, config properties are always valid arrays (error types only exist at compile-time)
2090
+ // We need to cast and copy readonly arrays to mutable arrays for storage
2091
+ // eslint-disable-next-line local-rules/no-typescript-assertion -- Error tuple types only exist at compile-time; runtime values are always valid arrays
2092
+ const runtimeConfig = config;
2093
+ layouts.push({
2094
+ layout: runtimeConfig.layout.map(row => [...row]),
2095
+ rows: runtimeConfig.rows ? [...runtimeConfig.rows] : undefined,
2096
+ columns: runtimeConfig.columns ? [...runtimeConfig.columns] : undefined,
2097
+ when: runtimeConfig.when ? [...runtimeConfig.when] : undefined,
2098
+ breakpoints: runtimeConfig.breakpoints,
2099
+ });
2100
+ return builder;
2101
+ },
2102
+ build() {
2103
+ return {
2104
+ areas: [...areas],
2105
+ layouts,
2106
+ };
2107
+ },
2108
+ };
2109
+ return builder;
2110
+ },
2111
+ };
2112
+ }
2113
+
2114
+ /**
2115
+ * Component for CSS grid with dynamic grid-template-areas.
2116
+ *
2117
+ * Renders the style tag and container div with proper scoping.
2118
+ * Uses a render prop pattern to provide typed slot props for children.
2119
+ *
2120
+ * @example Basic usage with builder pattern and hook
2121
+ * ```tsx
2122
+ * // Define grid config at module level (runs once)
2123
+ * const cardGrid = createGrid()
2124
+ * .areas(['icon', 'info'])
2125
+ * .layout({ layout: [['icon', 'info']] })
2126
+ * .build();
2127
+ *
2128
+ * function MyCard() {
2129
+ * const gridAreas = useGridAreas(cardGrid);
2130
+ *
2131
+ * return (
2132
+ * <GridAreas {...gridAreas} className="gap-4">
2133
+ * {(slots) => (
2134
+ * <>
2135
+ * <Icon {...slots.icon} />
2136
+ * <Info {...slots.info} />
2137
+ * </>
2138
+ * )}
2139
+ * </GridAreas>
2140
+ * );
2141
+ * }
2142
+ * ```
2143
+ * @example Conditional layouts
2144
+ * ```tsx
2145
+ * const cardGrid = createGrid()
2146
+ * .areas(['icon', 'info', 'tag'])
2147
+ * .layout({ layout: [['info']] })
2148
+ * .layout({
2149
+ * when: ['icon', 'info'],
2150
+ * layout: [['icon', 'info']],
2151
+ * columns: ['48px', '1fr'],
2152
+ * })
2153
+ * .build();
2154
+ *
2155
+ * function MyCard({ showIcon }) {
2156
+ * const gridAreas = useGridAreas(cardGrid);
2157
+ *
2158
+ * return (
2159
+ * <GridAreas {...gridAreas}>
2160
+ * {(slots) => (
2161
+ * <>
2162
+ * {showIcon && <Icon {...slots.icon} />}
2163
+ * <Info {...slots.info} />
2164
+ * </>
2165
+ * )}
2166
+ * </GridAreas>
2167
+ * );
2168
+ * }
2169
+ * ```
2170
+ * @example Using asChild to render a custom element
2171
+ * ```tsx
2172
+ * function MyCard() {
2173
+ * const gridAreas = useGridAreas(cardGrid);
2174
+ *
2175
+ * return (
2176
+ * <GridAreas {...gridAreas} asChild className="gap-4">
2177
+ * <section aria-label="Card content">
2178
+ * {(slots) => (
2179
+ * <>
2180
+ * <Icon {...slots.icon} />
2181
+ * <Info {...slots.info} />
2182
+ * </>
2183
+ * )}
2184
+ * </section>
2185
+ * </GridAreas>
2186
+ * );
2187
+ * }
2188
+ * ```
2189
+ */
2190
+ function GridAreas({ slots, css, containerProps, validationRef, className, children, asChild = false, }) {
2191
+ const Comp = asChild ? Slot : "div";
2192
+ return (jsxs(Fragment$1, { children: [jsx("style", { children: css }), jsx(Comp, { ref: validationRef, ...containerProps, className: twMerge("@container grid", className), children: children(slots) })] }));
2193
+ }
2194
+
2195
+ /**
2196
+ * Tailwind CSS default container query breakpoints with @ prefix.
2197
+ *
2198
+ * These are the standard values used by @sm, @md, @lg, etc. in Tailwind container queries.
2199
+ * Values are sourced from themeContainerSize in @trackunit/ui-design-tokens.
2200
+ *
2201
+ * @see https://tailwindcss.com/docs/responsive-design#container-queries
2202
+ */
2203
+ // eslint-disable-next-line local-rules/no-typescript-assertion
2204
+ const CONTAINER_BREAKPOINTS = Object.fromEntries(Object.entries(themeContainerSize).map(([key, value]) => [`@${key}`, value]));
2205
+
2206
+ /**
2207
+ * Converts a 2D layout array to a CSS grid-template-areas string.
2208
+ *
2209
+ * @example
2210
+ * layoutToGridTemplateAreas([['icon', 'tag'], ['info', 'info']])
2211
+ * // Returns: "'icon tag' 'info info'"
2212
+ */
2213
+ function layoutToGridTemplateAreas(layout) {
2214
+ return layout.map(row => `'${row.join(" ")}'`).join(" ");
2215
+ }
2216
+ /**
2217
+ * Converts an array of track sizes to a CSS grid-template-rows/columns string.
2218
+ *
2219
+ * @example
2220
+ * tracksToGridTemplate(['min-content', '1fr', 'auto'])
2221
+ * // Returns: "min-content 1fr auto"
2222
+ */
2223
+ function tracksToGridTemplate(tracks) {
2224
+ return tracks.join(" ");
2225
+ }
2226
+ /**
2227
+ * Generates the selector for an area name.
2228
+ * Uses [data-slot=name] format.
2229
+ */
2230
+ function areaToSelector(area) {
2231
+ return `[data-slot=${area}]`;
2232
+ }
2233
+ /**
2234
+ * Generates a :has() selector for the given conditions.
2235
+ *
2236
+ * Areas in `when` must be present (direct children).
2237
+ * All other areas from the full list are automatically treated as "must NOT be present".
2238
+ *
2239
+ * Uses direct child selector (>) for better performance and to avoid
2240
+ * false positives with nested grids.
2241
+ *
2242
+ * @example
2243
+ * generateHasSelector(['icon', 'tag'], ['icon', 'tag', 'info'])
2244
+ * // Returns: "&:has(> [data-slot=icon]):has(> [data-slot=tag]):not(:has(> [data-slot=info]))"
2245
+ */
2246
+ function generateHasSelector(when, allAreas) {
2247
+ const hasParts = when.map(area => `:has(> ${areaToSelector(area)})`);
2248
+ const notAreas = allAreas.filter(area => !when.includes(area));
2249
+ const notParts = notAreas.map(area => `:not(:has(> ${areaToSelector(area)}))`);
2250
+ return `&${[...hasParts, ...notParts].join("")}`;
2251
+ }
2252
+ /**
2253
+ * Helper to safely get or initialize a nested styles object
2254
+ */
2255
+ function getOrCreateNestedStyles(styles, key) {
2256
+ const existing = styles[key];
2257
+ if (typeof existing === "object") {
2258
+ return existing;
2259
+ }
2260
+ const newObj = {};
2261
+ styles[key] = newObj;
2262
+ return newObj;
2263
+ }
2264
+ /**
2265
+ * Type guard to check if a string is a valid container breakpoint
2266
+ */
2267
+ function isContainerBreakpoint(value) {
2268
+ return value in CONTAINER_BREAKPOINTS;
2269
+ }
2270
+ /**
2271
+ * Creates a typed slots object from an array of area names.
2272
+ *
2273
+ * Each slot contains a data-slot attribute and gridArea style.
2274
+ */
2275
+ function createSlots(areas) {
2276
+ // eslint-disable-next-line local-rules/no-typescript-assertion -- Object.fromEntries loses key type information
2277
+ return Object.fromEntries(areas.map(area => [area, { "data-slot": area, style: { gridArea: area } }]));
2278
+ }
2279
+ /**
2280
+ * Converts a camelCase CSS property name to kebab-case.
2281
+ *
2282
+ * @example
2283
+ * camelToKebab('gridTemplateAreas') // 'grid-template-areas'
2284
+ */
2285
+ function camelToKebab(str) {
2286
+ return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
2287
+ }
2288
+ /**
2289
+ * Converts a NestedStyles object to CSS declarations string.
2290
+ */
2291
+ function stylesToCssDeclarations(styles) {
2292
+ const declarations = [];
2293
+ for (const key of Object.keys(styles)) {
2294
+ const value = styles[key];
2295
+ // Only include non-nested properties (not starting with & or @)
2296
+ if (!key.startsWith("&") && !key.startsWith("@") && value !== undefined && typeof value !== "object") {
2297
+ declarations.push(`${camelToKebab(key)}: ${value};`);
2298
+ }
2299
+ }
2300
+ return declarations.join(" ");
2301
+ }
2302
+ /**
2303
+ * Recursively converts a GridStyles object to a CSS string.
2304
+ */
2305
+ function stylesToCss(styles, parentSelector, indent = "") {
2306
+ const lines = [];
2307
+ // Collect base declarations for this level
2308
+ const baseDeclarations = stylesToCssDeclarations(styles);
2309
+ if (baseDeclarations.length > 0) {
2310
+ lines.push(`${indent}${parentSelector} { ${baseDeclarations} }`);
2311
+ }
2312
+ // Process nested selectors
2313
+ for (const key of Object.keys(styles)) {
2314
+ const value = styles[key];
2315
+ if (typeof value === "object") {
2316
+ if (key.startsWith("&")) {
2317
+ // Pseudo-selector: replace & with parent selector
2318
+ const nestedSelector = key.replace(/^&/, parentSelector);
2319
+ lines.push(stylesToCss(value, nestedSelector, indent));
2320
+ }
2321
+ else if (key.startsWith("@")) {
2322
+ // At-rule (like @container): wrap in the at-rule
2323
+ const nestedCss = stylesToCss(value, parentSelector, indent + " ");
2324
+ lines.push(`${indent}${key} {\n${nestedCss}\n${indent}}`);
2325
+ }
2326
+ }
2327
+ }
2328
+ return lines.join("\n");
2329
+ }
2330
+ /**
2331
+ * Applies layout styles (areas, rows, columns) to a styles object.
2332
+ *
2333
+ * For breakpoints (isBreakpoint=true), rows/columns are always explicitly set
2334
+ * to prevent base layout values from cascading. This makes breakpoints self-contained.
2335
+ */
2336
+ function applyLayoutStyles({ targetStyles, layout, rows, columns, isBreakpoint = false, }) {
2337
+ targetStyles.gridTemplateAreas = layoutToGridTemplateAreas(layout);
2338
+ if (rows !== undefined && rows.length > 0) {
2339
+ targetStyles.gridTemplateRows = tracksToGridTemplate(rows);
2340
+ }
2341
+ else if (isBreakpoint) {
2342
+ // Breakpoints are self-contained - reset to prevent base cascade
2343
+ targetStyles.gridTemplateRows = "none";
2344
+ }
2345
+ if (columns !== undefined && columns.length > 0) {
2346
+ targetStyles.gridTemplateColumns = tracksToGridTemplate(columns);
2347
+ }
2348
+ else if (isBreakpoint) {
2349
+ // Breakpoints are self-contained - reset to prevent base cascade
2350
+ targetStyles.gridTemplateColumns = "none";
2351
+ }
2352
+ }
2353
+ /**
2354
+ * Generates CSS string from a grid configuration and id.
2355
+ *
2356
+ * @param id - Unique identifier for CSS scoping
2357
+ * @param config - Grid configuration from createGrid().build()
2358
+ * @returns {string} CSS string for use in a style tag
2359
+ */
2360
+ function generateCss(id, config) {
2361
+ const { areas, layouts } = config;
2362
+ // Build styles object
2363
+ const styles = {};
2364
+ for (const layoutDef of layouts) {
2365
+ const { layout, rows, columns, when = [], breakpoints = {} } = layoutDef;
2366
+ // No conditions - this is a base layout
2367
+ if (when.length === 0) {
2368
+ applyLayoutStyles({ targetStyles: styles, layout, rows, columns });
2369
+ // Add breakpoint overrides for base layout
2370
+ for (const breakpoint of Object.keys(breakpoints)) {
2371
+ if (isContainerBreakpoint(breakpoint)) {
2372
+ const breakpointConfig = breakpoints[breakpoint];
2373
+ if (breakpointConfig !== undefined) {
2374
+ const containerQuery = `@container (min-width: ${CONTAINER_BREAKPOINTS[breakpoint]})`;
2375
+ const nestedStyles = getOrCreateNestedStyles(styles, containerQuery);
2376
+ applyLayoutStyles({
2377
+ targetStyles: nestedStyles,
2378
+ layout: breakpointConfig.layout,
2379
+ rows: breakpointConfig.rows,
2380
+ columns: breakpointConfig.columns,
2381
+ isBreakpoint: true,
2382
+ });
2383
+ }
2384
+ }
2385
+ }
2386
+ }
2387
+ else {
2388
+ // Conditional layout - generate :has() selector
2389
+ // Areas not in `when` are automatically treated as "must NOT be present"
2390
+ const selector = generateHasSelector(when, areas);
2391
+ const conditionalStyles = getOrCreateNestedStyles(styles, selector);
2392
+ applyLayoutStyles({ targetStyles: conditionalStyles, layout, rows, columns });
2393
+ // Add breakpoint overrides for conditional layout
2394
+ for (const breakpoint of Object.keys(breakpoints)) {
2395
+ if (isContainerBreakpoint(breakpoint)) {
2396
+ const breakpointConfig = breakpoints[breakpoint];
2397
+ if (breakpointConfig !== undefined) {
2398
+ const containerQuery = `@container (min-width: ${CONTAINER_BREAKPOINTS[breakpoint]})`;
2399
+ const containerStyles = getOrCreateNestedStyles(styles, containerQuery);
2400
+ const nestedConditional = getOrCreateNestedStyles(containerStyles, selector);
2401
+ applyLayoutStyles({
2402
+ targetStyles: nestedConditional,
2403
+ layout: breakpointConfig.layout,
2404
+ rows: breakpointConfig.rows,
2405
+ columns: breakpointConfig.columns,
2406
+ isBreakpoint: true,
2407
+ });
2408
+ }
2409
+ }
2410
+ }
2411
+ }
2412
+ }
2413
+ // Generate CSS string with the container selector
2414
+ const containerSelector = `[data-grid-id="${id}"]`;
2415
+ return stylesToCss(styles, containerSelector);
2416
+ }
2417
+
2418
+ /**
2419
+ * Creates a normalized key from a when-clause for comparison.
2420
+ * Sorts the array alphabetically and joins with a delimiter.
2421
+ * Returns empty string for undefined/empty when-clauses.
2422
+ *
2423
+ * @param when - The when-clause array
2424
+ * @returns {string} A normalized string key for comparison
2425
+ */
2426
+ function normalizeWhenClause(when) {
2427
+ if (when === undefined || when.length === 0) {
2428
+ return "";
2429
+ }
2430
+ return [...when].sort().join("\0");
2431
+ }
2432
+ /**
2433
+ * Validates a grid layout's dimensions against rows/columns arrays.
2434
+ *
2435
+ * @param options - Validation options
2436
+ * @param options.layout - The 2D grid layout
2437
+ * @param options.rows - Optional row track sizes
2438
+ * @param options.columns - Optional column track sizes
2439
+ * @param options.context - Context string for error messages
2440
+ * @returns {Array<string>} Array of error messages
2441
+ */
2442
+ function validateLayoutDimensions({ layout, rows, columns, context }) {
2443
+ const errors = [];
2444
+ const firstRow = layout[0];
2445
+ if (firstRow === undefined) {
2446
+ errors.push(`${context}: Layout has no rows.`);
2447
+ return errors;
2448
+ }
2449
+ const rowCount = layout.length;
2450
+ const firstRowLength = firstRow.length;
2451
+ // Check rows array length
2452
+ if (rows !== undefined && rows.length !== rowCount) {
2453
+ errors.push(`${context}: rows array length (${rows.length}) doesn't match layout row count (${rowCount}).`);
2454
+ }
2455
+ // Check columns array length
2456
+ if (columns !== undefined && columns.length !== firstRowLength) {
2457
+ errors.push(`${context}: columns array length (${columns.length}) doesn't match layout column count (${firstRowLength}).`);
2458
+ }
2459
+ // Check for inconsistent column counts across rows
2460
+ for (let i = 1; i < layout.length; i++) {
2461
+ const row = layout[i];
2462
+ if (row === undefined) {
2463
+ continue;
2464
+ }
2465
+ if (row.length !== firstRowLength) {
2466
+ errors.push(`${context}: Row ${i} has ${row.length} columns, but row 0 has ${firstRowLength}. All rows must have the same number of columns.`);
2467
+ }
2468
+ }
2469
+ return errors;
2470
+ }
2471
+ /**
2472
+ * Validates that all cell names in a layout are valid area names.
2473
+ *
2474
+ * @param options - Validation options
2475
+ * @param options.layout - The 2D grid layout
2476
+ * @param options.areas - Valid area names
2477
+ * @param options.context - Context string for error messages
2478
+ * @returns {Array<string>} Array of error messages
2479
+ */
2480
+ function validateLayoutAreaNames({ layout, areas, context }) {
2481
+ const errors = [];
2482
+ const areaSet = new Set(areas);
2483
+ for (let rowIndex = 0; rowIndex < layout.length; rowIndex++) {
2484
+ const row = layout[rowIndex];
2485
+ if (row === undefined) {
2486
+ continue;
2487
+ }
2488
+ for (let colIndex = 0; colIndex < row.length; colIndex++) {
2489
+ const cell = row[colIndex];
2490
+ if (cell === undefined) {
2491
+ continue;
2492
+ }
2493
+ if (cell !== "." && !areaSet.has(cell)) {
2494
+ errors.push(`${context}: Unknown area name "${cell}" at row ${rowIndex}, column ${colIndex}. Valid areas are: ${areas.join(", ")}.`);
2495
+ }
2496
+ }
2497
+ }
2498
+ return errors;
2499
+ }
2500
+ /**
2501
+ * Validates that all items in a 'when' array are valid area names and appear in the layout.
2502
+ *
2503
+ * @param options - Validation options
2504
+ * @param options.when - Optional when-clause array
2505
+ * @param options.layout - The 2D grid layout
2506
+ * @param options.areas - Valid area names
2507
+ * @param options.context - Context string for error messages
2508
+ * @returns {Array<string>} Array of error messages
2509
+ */
2510
+ function validateWhenClause({ when, layout, areas, context }) {
2511
+ if (when === undefined || when.length === 0) {
2512
+ return [];
2513
+ }
2514
+ const errors = [];
2515
+ const areaSet = new Set(areas);
2516
+ const layoutCells = new Set(layout.flat());
2517
+ for (const item of when) {
2518
+ if (!areaSet.has(item)) {
2519
+ errors.push(`${context}: Unknown area "${item}" in 'when' clause. Valid areas are: ${areas.join(", ")}.`);
2520
+ }
2521
+ else if (!layoutCells.has(item)) {
2522
+ errors.push(`${context}: Area "${item}" is in 'when' clause but doesn't appear in the layout. The layout will never match this condition.`);
2523
+ }
2524
+ }
2525
+ return errors;
2526
+ }
2527
+ /**
2528
+ * Validates breakpoint configurations.
2529
+ *
2530
+ * @param options - Validation options
2531
+ * @param options.breakpoints - Optional breakpoint config
2532
+ * @param options.areas - Valid area names
2533
+ * @param options.layoutIndex - Index of the parent layout for error messages
2534
+ * @returns {Array<string>} Array of error messages
2535
+ */
2536
+ function validateBreakpoints({ breakpoints, areas, layoutIndex }) {
2537
+ if (breakpoints === undefined) {
2538
+ return [];
2539
+ }
2540
+ const errors = [];
2541
+ for (const [breakpoint, config] of Object.entries(breakpoints)) {
2542
+ const context = `Layout ${layoutIndex}, breakpoint "${breakpoint}"`;
2543
+ errors.push(...validateLayoutDimensions({ layout: config.layout, rows: config.rows, columns: config.columns, context }));
2544
+ errors.push(...validateLayoutAreaNames({ layout: config.layout, areas, context }));
2545
+ }
2546
+ return errors;
2547
+ }
2548
+ /**
2549
+ * Validates that there are no duplicate 'when' clauses across layouts.
2550
+ * Two when-clauses are considered duplicates if they contain the same set of areas (order doesn't matter).
2551
+ *
2552
+ * @param layouts - Array of layout configurations
2553
+ * @returns {Array<string>} Array of warning messages
2554
+ */
2555
+ function validateDuplicateWhenClauses(layouts) {
2556
+ const warnings = [];
2557
+ const seenWhenClauses = new Map();
2558
+ for (let i = 0; i < layouts.length; i++) {
2559
+ const layoutConfig = layouts[i];
2560
+ if (layoutConfig === undefined) {
2561
+ continue;
2562
+ }
2563
+ const whenKey = normalizeWhenClause(layoutConfig.when);
2564
+ // Skip layouts without when-clauses (base layouts)
2565
+ if (whenKey === "") {
2566
+ continue;
2567
+ }
2568
+ const previousIndex = seenWhenClauses.get(whenKey);
2569
+ if (previousIndex !== undefined) {
2570
+ const whenDisplay = layoutConfig.when?.join(", ") ?? "";
2571
+ warnings.push(`Layout ${i} has the same 'when' clause as Layout ${previousIndex}: [${whenDisplay}]. ` +
2572
+ `Layout ${i} will override Layout ${previousIndex} since it appears later.`);
2573
+ }
2574
+ seenWhenClauses.set(whenKey, i);
2575
+ }
2576
+ return warnings;
2577
+ }
2578
+ /**
2579
+ * Validates a grid configuration and returns an array of error messages.
2580
+ * Returns an empty array if the configuration is valid.
2581
+ *
2582
+ * @param config - The grid configuration to validate
2583
+ * @returns {Array<string>} Array of error message strings
2584
+ */
2585
+ function validateGridConfig(config) {
2586
+ const errors = [];
2587
+ const { areas, layouts } = config;
2588
+ // Validate areas array
2589
+ if (areas.length === 0) {
2590
+ errors.push("Config has no areas defined. Use .areas([...]) to define at least one area.");
2591
+ }
2592
+ // Validate layouts array
2593
+ if (layouts.length === 0) {
2594
+ errors.push("Config has no layouts defined. Use .layout({...}) to define at least one layout.");
2595
+ }
2596
+ // Check for duplicate when-clauses
2597
+ errors.push(...validateDuplicateWhenClauses(layouts));
2598
+ // Validate each layout
2599
+ for (let i = 0; i < layouts.length; i++) {
2600
+ const layoutConfig = layouts[i];
2601
+ if (layoutConfig === undefined) {
2602
+ continue;
2603
+ }
2604
+ const context = `Layout ${i}`;
2605
+ errors.push(...validateLayoutDimensions({
2606
+ layout: layoutConfig.layout,
2607
+ rows: layoutConfig.rows,
2608
+ columns: layoutConfig.columns,
2609
+ context,
2610
+ }));
2611
+ errors.push(...validateLayoutAreaNames({ layout: layoutConfig.layout, areas, context }));
2612
+ errors.push(...validateWhenClause({ when: layoutConfig.when, layout: layoutConfig.layout, areas, context }));
2613
+ errors.push(...validateBreakpoints({ breakpoints: layoutConfig.breakpoints, areas, layoutIndex: i }));
2614
+ }
2615
+ return errors;
2616
+ }
2617
+
2618
+ /**
2619
+ * Extracts the gridArea value from an element's inline style.
2620
+ *
2621
+ * @param element - The element to check
2622
+ * @returns {string | null} The gridArea value or null if not set
2623
+ */
2624
+ function getGridAreaStyle(element) {
2625
+ if (element instanceof HTMLElement) {
2626
+ const gridArea = element.style.gridArea;
2627
+ return gridArea !== "" ? gridArea : null;
2628
+ }
2629
+ return null;
2630
+ }
2631
+ /**
2632
+ * Validates that all direct children of a grid container have proper slot props.
2633
+ * Checks for both data-slot attribute and gridArea style, and validates they match.
2634
+ *
2635
+ * @param container - The grid container element
2636
+ * @param areas - Array of valid area names
2637
+ * @returns {Array<string>} Array of warning message strings
2638
+ */
2639
+ function validateGridSlots(container, areas) {
2640
+ const warnings = [];
2641
+ const areaSet = new Set(areas);
2642
+ const unwiredChildren = [];
2643
+ const unknownSlots = [];
2644
+ const partialSlots = [];
2645
+ const mismatchedSlots = [];
2646
+ let childIndex = 0;
2647
+ for (const child of Array.from(container.children)) {
2648
+ const dataSlot = child.getAttribute("data-slot");
2649
+ const gridArea = getGridAreaStyle(child);
2650
+ const tagName = child.tagName.toLowerCase();
2651
+ const hasDataSlot = dataSlot !== null;
2652
+ const hasGridArea = gridArea !== null;
2653
+ if (!hasDataSlot && !hasGridArea) {
2654
+ // Completely missing slot props
2655
+ unwiredChildren.push({ tagName, index: childIndex, dataSlot, gridArea });
2656
+ }
2657
+ else if (hasDataSlot && !hasGridArea) {
2658
+ // Has data-slot but missing gridArea style
2659
+ partialSlots.push({ tagName, index: childIndex, dataSlot, gridArea });
2660
+ }
2661
+ else if (!hasDataSlot && hasGridArea) {
2662
+ // Has gridArea but missing data-slot
2663
+ partialSlots.push({ tagName, index: childIndex, dataSlot, gridArea });
2664
+ }
2665
+ else if (hasDataSlot && hasGridArea && dataSlot !== gridArea) {
2666
+ // Both present but values don't match
2667
+ mismatchedSlots.push({ tagName, index: childIndex, dataSlot, gridArea });
2668
+ }
2669
+ else if (hasDataSlot && !areaSet.has(dataSlot)) {
2670
+ // Valid slot props but unknown area name
2671
+ unknownSlots.push({ tagName, index: childIndex, dataSlot, gridArea });
2672
+ }
2673
+ childIndex++;
2674
+ }
2675
+ if (unwiredChildren.length > 0) {
2676
+ const details = unwiredChildren.map(({ tagName, index }) => ` - <${tagName}> at index ${index}`).join("\n");
2677
+ warnings.push(`Found ${unwiredChildren.length} child element(s) without slot props:\n${details}\n` +
2678
+ `Hint: Did you forget to spread {...slots.name} on these elements?`);
2679
+ }
2680
+ if (partialSlots.length > 0) {
2681
+ const details = partialSlots
2682
+ .map(({ tagName, index, dataSlot, gridArea }) => {
2683
+ const has = dataSlot !== null ? `data-slot="${dataSlot}"` : `style.gridArea="${gridArea}"`;
2684
+ const missing = dataSlot !== null ? "style.gridArea" : "data-slot";
2685
+ return ` - <${tagName}> at index ${index} has ${has} but missing ${missing}`;
2686
+ })
2687
+ .join("\n");
2688
+ warnings.push(`Found ${partialSlots.length} child element(s) with incomplete slot props:\n${details}\n` +
2689
+ `Hint: Make sure to spread the full slot props {...slots.name} instead of setting attributes manually. And make sure the child takes a style prop.`);
2690
+ }
2691
+ if (mismatchedSlots.length > 0) {
2692
+ const details = mismatchedSlots
2693
+ .map(({ tagName, index, dataSlot, gridArea }) => ` - <${tagName}> at index ${index} has data-slot="${dataSlot}" but style.gridArea="${gridArea}"`)
2694
+ .join("\n");
2695
+ warnings.push(`Found ${mismatchedSlots.length} child element(s) with mismatched slot values:\n${details}\n` +
2696
+ `Hint: The data-slot attribute and style.gridArea should have the same value. Use {...slots.name} to ensure consistency.`);
2697
+ }
2698
+ if (unknownSlots.length > 0) {
2699
+ const details = unknownSlots
2700
+ .map(({ dataSlot, tagName, index }) => ` - <${tagName}> at index ${index} has data-slot="${dataSlot}"`)
2701
+ .join("\n");
2702
+ warnings.push(`Found ${unknownSlots.length} child element(s) with unknown slot names:\n${details}\n` +
2703
+ `Valid slot names are: ${areas.join(", ")}.`);
2704
+ }
2705
+ return warnings;
2706
+ }
2707
+
2708
+ /**
2709
+ * React hook for creating type-safe CSS grid-template-areas with automatic :has() selector generation.
2710
+ *
2711
+ * Automatically generates a unique id using React's useId() hook.
2712
+ * Returns memoized grid configuration including slots, CSS, and container props.
2713
+ *
2714
+ * In development mode, validates the grid configuration and logs warnings for:
2715
+ * - Empty areas or layouts
2716
+ * - Unknown area names in layouts or when clauses
2717
+ * - Mismatched row/column counts
2718
+ * - Missing when items in layouts
2719
+ *
2720
+ * Also returns a validationRef callback that validates DOM children when attached.
2721
+ *
2722
+ * @example Basic usage with builder pattern
2723
+ * ```tsx
2724
+ * const cardGrid = createGrid()
2725
+ * .areas(["icon", "info"])
2726
+ * .layout({ layout: [["icon", "info"]] })
2727
+ * .build();
2728
+ *
2729
+ * function MyCard() {
2730
+ * const gridAreas = useGridAreas(cardGrid);
2731
+ *
2732
+ * return (
2733
+ * <GridAreas {...gridAreas}>
2734
+ * {(slots) => (
2735
+ * <>
2736
+ * <Icon {...slots.icon} />
2737
+ * <Info {...slots.info} />
2738
+ * </>
2739
+ * )}
2740
+ * </GridAreas>
2741
+ * );
2742
+ * }
2743
+ * ```
2744
+ * @example Conditional layouts with row/column sizing
2745
+ * ```tsx
2746
+ * const cardGrid = createGrid()
2747
+ * .areas(["icon", "title", "description"])
2748
+ * .layout({ layout: [["title"]] })
2749
+ * .layout({
2750
+ * when: ["icon", "title", "description"],
2751
+ * layout: [
2752
+ * ["icon", "title"],
2753
+ * ["icon", "description"],
2754
+ * ],
2755
+ * rows: ["auto", "1fr"],
2756
+ * columns: ["48px", "1fr"],
2757
+ * })
2758
+ * .build();
2759
+ *
2760
+ * function Card() {
2761
+ * const gridAreas = useGridAreas(cardGrid);
2762
+ * // ...
2763
+ * }
2764
+ * ```
2765
+ * @param config - Grid configuration from createGrid().build()
2766
+ * @returns {GridAreasResult} Object with slots, css, containerProps, and validationRef to spread on GridAreas
2767
+ */
2768
+ function useGridAreas(config) {
2769
+ const id = useId();
2770
+ // Ref callback for DOM validation (runs once when element mounts)
2771
+ const validationRef = useCallback((element) => {
2772
+ if (process.env.NODE_ENV !== "development" || element === null) {
2773
+ return;
2774
+ }
2775
+ const warnings = validateGridSlots(element, config.areas);
2776
+ for (const warning of warnings) {
2777
+ // eslint-disable-next-line no-console
2778
+ console.warn(`GridAreas: ${warning}`);
2779
+ }
2780
+ }, [config.areas]);
2781
+ return useMemo(() => {
2782
+ // Config validation (dev only)
2783
+ if (process.env.NODE_ENV === "development") {
2784
+ const errors = validateGridConfig(config);
2785
+ for (const error of errors) {
2786
+ // eslint-disable-next-line no-console
2787
+ console.warn(`useGridAreas: ${error}`);
2788
+ }
2789
+ }
2790
+ return {
2791
+ slots: createSlots(config.areas),
2792
+ css: generateCss(id, config),
2793
+ containerProps: { "data-grid-id": id },
2794
+ validationRef,
2795
+ };
2796
+ }, [id, config, validationRef]);
2797
+ }
2798
+
2050
2799
  const cvaHighlight = cvaMerge([
2051
2800
  "inline-flex",
2052
2801
  "justify-center",
@@ -3101,7 +3850,7 @@ const Indicator = ({ "data-testid": dataTestId, icon, label, color = "unknown",
3101
3850
  *
3102
3851
  * @returns tailwind class names on the basis on the provided props.
3103
3852
  */
3104
- const cvaInteractableItem = cvaMerge("", {
3853
+ const cvaInteractableItem = cvaMerge(["transition-colors", "duration-100", "ease-in-out"], {
3105
3854
  variants: {
3106
3855
  cursor: {
3107
3856
  pointer: "cursor-pointer",
@@ -3408,6 +4157,124 @@ const cvaListItem$1 = cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3408
4157
  },
3409
4158
  });
3410
4159
 
4160
+ const cvaSkeleton = cvaMerge([
4161
+ "relative",
4162
+ "overflow-hidden",
4163
+ "rounded-lg",
4164
+ // Gradient background
4165
+ "bg-gradient-to-r",
4166
+ "from-gray-200/80",
4167
+ "via-gray-300/60",
4168
+ "to-gray-200/80",
4169
+ // Pulse animation
4170
+ "animate-pulse",
4171
+ // Shimmer overlay
4172
+ "before:absolute",
4173
+ "before:inset-0",
4174
+ "before:bg-gradient-to-r",
4175
+ "before:from-transparent",
4176
+ "before:via-white/50",
4177
+ "before:to-transparent",
4178
+ "before:opacity-0",
4179
+ "before:animate-pulse",
4180
+ // Smooth transitions for accessibility
4181
+ "transition-all",
4182
+ "duration-300",
4183
+ "ease-in-out",
4184
+ ]);
4185
+
4186
+ const VALID_SIZE_KEYS = [
4187
+ "xs",
4188
+ "sm",
4189
+ "base",
4190
+ "lg",
4191
+ "xl",
4192
+ "2xl",
4193
+ "3xl",
4194
+ "4xl",
4195
+ "5xl",
4196
+ "6xl",
4197
+ "7xl",
4198
+ "8xl",
4199
+ "9xl",
4200
+ ];
4201
+ /**
4202
+ * Extract the size key from a text size string (e.g., "text-base" → "base").
4203
+ *
4204
+ * @param value - The text size string to parse
4205
+ * @returns {fontSizeKeys | null} The extracted size key or null if invalid
4206
+ */
4207
+ const extractSizeKey = (value) => {
4208
+ if (!value.startsWith("text-")) {
4209
+ return null;
4210
+ }
4211
+ const sizeKey = value.replace("text-", "");
4212
+ return VALID_SIZE_KEYS.find(key => key === sizeKey) ?? null;
4213
+ };
4214
+ /**
4215
+ * Calculate the height value based on the height prop and variant.
4216
+ *
4217
+ * @param height - The height value (number, CSS length, or text size key)
4218
+ * @param variant - The skeleton variant ("text" or "block")
4219
+ * @returns {string} The calculated CSS height value
4220
+ */
4221
+ const getHeightValue = (height, variant) => {
4222
+ if (typeof height === "number") {
4223
+ return `${height}px`;
4224
+ }
4225
+ const sizeKey = extractSizeKey(height);
4226
+ if (sizeKey) {
4227
+ // Text variant: use font-size × 0.7 to approximate cap-height
4228
+ // Block variant: use full line-height (for when text size keys are passed to block variant)
4229
+ return variant === "text" ? `calc(var(--font-size-${sizeKey}) * 0.7)` : `var(--line-height-${sizeKey})`;
4230
+ }
4231
+ return height;
4232
+ };
4233
+ /**
4234
+ * Calculate the vertical margin value for text variant to align with text baseline.
4235
+ * Formula: (line-height - cap-height) / 2, where cap-height = font-size × 0.7
4236
+ *
4237
+ * @param height - The height value (number, CSS length, or text size key)
4238
+ * @returns {string} The calculated CSS margin value
4239
+ */
4240
+ const getMarginValue = (height) => {
4241
+ if (typeof height === "string") {
4242
+ const sizeKey = extractSizeKey(height);
4243
+ if (sizeKey) {
4244
+ // margin = (line-height - cap-height) / 2
4245
+ // cap-height = font-size × 0.7
4246
+ // For large text sizes, this may be negative, which matches how actual text extends beyond its line-height box
4247
+ return `calc((var(--line-height-${sizeKey}) - var(--font-size-${sizeKey}) * 0.7) / 2)`;
4248
+ }
4249
+ }
4250
+ return "0";
4251
+ };
4252
+ /**
4253
+ * Display a single placeholder line before data gets loaded to reduce load-time frustration.
4254
+ *
4255
+ * Use `variant="text"` with text-size keys for text placeholders.
4256
+ * Use `variant="block"` with numbers/CSS for images, badges, buttons, and other shape-based elements.
4257
+ * Pass children to create custom skeleton layouts.
4258
+ */
4259
+ const Skeleton = memo((props) => {
4260
+ const { width = "100%", className, "data-testid": dataTestId, children } = props;
4261
+ const variant = props.variant ?? "text";
4262
+ const height = props.height ?? (variant === "text" ? "text-base" : 16);
4263
+ const flexibleWidth = props.flexibleWidth ?? variant === "text";
4264
+ const widthValue = typeof width === "number" ? `${width}px` : width;
4265
+ const isTextVariant = variant === "text";
4266
+ const heightValue = getHeightValue(height, variant);
4267
+ const marginValue = isTextVariant ? getMarginValue(height) : undefined;
4268
+ return (jsx("div", { "aria-label": "Loading", className: cvaSkeleton({ className }), "data-testid": dataTestId, role: "status", style: {
4269
+ width: flexibleWidth ? "100%" : widthValue,
4270
+ maxWidth: flexibleWidth ? widthValue : undefined,
4271
+ height: heightValue,
4272
+ marginTop: marginValue,
4273
+ marginBottom: marginValue,
4274
+ }, children: children }));
4275
+ });
4276
+ Skeleton.displayName = "Skeleton";
4277
+
3411
4278
  const cvaListItem = cvaMerge(["py-3", "px-4", "min-h-14", "w-full", "flex", "justify-between", "items-center"]);
3412
4279
  const cvaMainInformationClass = cvaMerge(["grid", "items-center", "text-sm", "gap-2"], {
3413
4280
  variants: {
@@ -3426,6 +4293,11 @@ const cvaThumbnailContainer = cvaMerge([
3426
4293
  "overflow-hidden",
3427
4294
  "rounded-md",
3428
4295
  ]);
4296
+ const cvaTextContainer = cvaMerge(["grid-rows-min-fr", "grid", "items-center", "text-sm"]);
4297
+ const cvaTitleRow = cvaMerge(["gap-responsive-space-sm", "flex", "w-full", "min-w-0", "items-center", "text-sm"]);
4298
+ const cvaDescriptionRow = cvaMerge(["gap-responsive-space-sm", "flex", "w-full", "min-w-0", "items-center"]);
4299
+ const cvaMetaRow = cvaMerge(["gap-responsive-space-sm", "flex", "w-full", "min-w-0", "items-center", "pt-0.5"]);
4300
+ const cvaDetailsContainer = cvaMerge(["flex", "items-center", "gap-0.5", "text-nowrap", "pl-2"]);
3429
4301
 
3430
4302
  /**
3431
4303
  * Default property values for ListItemSkeleton component.
@@ -3451,7 +4323,7 @@ const ListItemSkeleton = ({ hasThumbnail = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasT
3451
4323
  details: getResponsiveRandomWidthPercentage({ min: 25, max: 45 }),
3452
4324
  };
3453
4325
  }, []);
3454
- return (jsxs("div", { className: cvaListItem({ className: "w-full" }), children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail, className: "w-full" }), children: [hasThumbnail ? (jsx("div", { className: cvaThumbnailContainer({ className: "bg-gray-200" }), children: jsx("div", { className: twMerge("bg-gray-300", thumbnailShape === "circle" ? "rounded-full" : "rounded"), style: { width: 20, height: 20 } }) })) : null, jsxs("div", { className: "grid-rows-min-fr grid w-full items-center gap-1 text-sm", children: [jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.title }), hasDescription ? jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.description }) : null, hasMeta ? jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.meta }) : null] })] }), hasDetails ? (jsx("div", { className: "pl-2 text-sm", children: jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.details }) })) : null] }));
4326
+ return (jsxs("div", { className: cvaListItem({ className: "w-full" }), children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail, className: "w-full" }), children: [hasThumbnail ? (jsx("div", { className: cvaThumbnailContainer({ className: "bg-gray-200" }), children: jsx("div", { className: twMerge("bg-gray-300", thumbnailShape === "circle" ? "rounded-full" : "rounded"), style: { width: 20, height: 20 } }) })) : null, jsxs("div", { className: cvaTextContainer(), children: [jsx("div", { className: cvaTitleRow(), children: jsx(Skeleton, { height: "text-sm", width: lineWidths.title }) }), hasDescription ? (jsx("div", { className: cvaDescriptionRow(), children: jsx(Skeleton, { height: "text-xs", width: lineWidths.description }) })) : null, hasMeta ? (jsx("div", { className: cvaMetaRow(), children: jsx(Skeleton, { height: "text-xs", width: lineWidths.meta }) })) : null] })] }), hasDetails ? (jsx("div", { className: cvaDetailsContainer(), children: jsx(Skeleton, { height: "text-sm", width: lineWidths.details }) })) : null] }));
3455
4327
  };
3456
4328
 
3457
4329
  /**
@@ -3938,13 +4810,13 @@ const ListItem = ({ className, "data-testid": dataTestId, onClick, details, titl
3938
4810
  const interactableItemClass = onClick ? twMerge(baseClass, cvaInteractableItem({ cursor: "pointer" })) : baseClass;
3939
4811
  return (jsxs("li", { className: interactableItemClass, "data-testid": dataTestId, onClick: onClick, ...rest, children: [jsxs("div", { className: cvaMainInformationClass({ hasThumbnail: !!thumbnail }), children: [thumbnail ? (jsx("div", { className: cvaThumbnailContainer({
3940
4812
  className: `text-${thumbnailColor} bg-${thumbnailBackground}`,
3941
- }), children: thumbnail })) : null, jsxs("div", { className: "grid-rows-min-fr grid items-center text-sm", children: [jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center text-sm", children: typeof title === "string" ? (jsx(Text, { className: "truncate", "data-testid": dataTestId ? `${dataTestId}-title` : undefined, weight: "bold", children: title })) : (cloneElement(title, {
4813
+ }), children: thumbnail })) : null, jsxs("div", { className: cvaTextContainer(), children: [jsx("div", { className: cvaTitleRow(), children: typeof title === "string" ? (jsx(Text, { className: "truncate", "data-testid": dataTestId ? `${dataTestId}-title` : undefined, weight: "bold", children: title })) : (cloneElement(title, {
3942
4814
  className: twMerge(title.props.className, "neutral-900 text-sm font-medium truncate"),
3943
4815
  "data-testid": !title.props["data-testid"] && dataTestId ? `${dataTestId}-title` : undefined,
3944
- })) }), description !== undefined && description !== "" ? (jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center", children: typeof description === "string" ? (jsx(Text, { className: "truncate text-xs text-neutral-500", "data-testid": dataTestId ? `${dataTestId}-description` : undefined, weight: "bold", children: description })) : (cloneElement(description, {
4816
+ })) }), description !== undefined && description !== "" ? (jsx("div", { className: cvaDescriptionRow(), children: typeof description === "string" ? (jsx(Text, { className: "truncate text-xs text-neutral-500", "data-testid": dataTestId ? `${dataTestId}-description` : undefined, weight: "bold", children: description })) : (cloneElement(description, {
3945
4817
  className: twMerge(description.props.className, "text-neutral-500 text-xs font-medium truncate"),
3946
4818
  "data-testid": !description.props["data-testid"] && dataTestId ? `${dataTestId}-description` : undefined,
3947
- })) })) : null, meta ? (jsx("div", { className: "gap-responsive-space-sm flex w-full min-w-0 items-center pt-0.5", children: jsx(Text, { className: "truncate text-xs text-neutral-400", "data-testid": dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxs("div", { className: "flex items-center gap-0.5 text-nowrap pl-2", children: [details, onClick ? jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
4819
+ })) })) : null, meta ? (jsx("div", { className: cvaMetaRow(), children: jsx(Text, { className: "truncate text-xs text-neutral-400", "data-testid": dataTestId ? `${dataTestId}-meta` : undefined, weight: "bold", children: meta }) })) : null] })] }), jsxs("div", { className: cvaDetailsContainer(), children: [details, onClick ? jsx(Icon, { color: "neutral", name: "ChevronRight", size: "medium" }) : null] })] }));
3948
4820
  };
3949
4821
 
3950
4822
  // Height constants (in pixels) - based on ListItem.variants.ts styling
@@ -4640,6 +5512,216 @@ const Polygon = ({ points, size, color = "black", opaque = true, className, "dat
4640
5512
  };
4641
5513
  const normalize = ({ value, min, max, size }) => ((value - min) / (max - min)) * size;
4642
5514
 
5515
+ const cvaPreferenceCard = cvaMerge([
5516
+ "rounded-lg",
5517
+ "border",
5518
+ "border-neutral-200",
5519
+ "bg-white",
5520
+ "w-full",
5521
+ "grid",
5522
+ "gap-y-2",
5523
+ "@container",
5524
+ "grid-rows-1",
5525
+ "grid-cols-1",
5526
+ "[&:has([data-slot=input-container])]:grid-cols-min-fr", // When the input container is present, the grid columns are set to min-fr
5527
+ "[&:has([data-slot=input-container]_:focus-visible)]:outline-native",
5528
+ ], {
5529
+ variants: {
5530
+ disabled: {
5531
+ true: "",
5532
+ false: "",
5533
+ },
5534
+ clickable: {
5535
+ true: ["cursor-pointer"],
5536
+ false: "",
5537
+ },
5538
+ },
5539
+ compoundVariants: [
5540
+ {
5541
+ disabled: true,
5542
+ clickable: true,
5543
+ class: "cursor-not-allowed",
5544
+ },
5545
+ ],
5546
+ defaultVariants: {
5547
+ disabled: false,
5548
+ clickable: false,
5549
+ },
5550
+ });
5551
+ const cvaInputContainer = cvaMerge(["flex", "p-4", "rounded-l-lg", "border", "border-transparent", "bg-neutral-50", "border-r-neutral-200"], {
5552
+ variants: {
5553
+ itemPlacement: {
5554
+ center: "items-center",
5555
+ start: "items-start",
5556
+ },
5557
+ },
5558
+ defaultVariants: {
5559
+ itemPlacement: "start",
5560
+ },
5561
+ });
5562
+ const cvaContentContainer = cvaMerge([
5563
+ "grid",
5564
+ "gap-x-3",
5565
+ //* Grid template areas are applied with GridAreas component)
5566
+ ], {
5567
+ variants: {
5568
+ itemPlacement: {
5569
+ center: "items-center",
5570
+ start: "items-start",
5571
+ },
5572
+ },
5573
+ defaultVariants: {
5574
+ itemPlacement: "start",
5575
+ },
5576
+ });
5577
+ const cvaTitleCard = cvaMerge(["min-w-0", "overflow-hidden", "text-ellipsis", "whitespace-nowrap"], {
5578
+ variants: {
5579
+ disabled: {
5580
+ true: "text-neutral-400",
5581
+ false: "text-neutral-900",
5582
+ },
5583
+ },
5584
+ defaultVariants: {
5585
+ disabled: false,
5586
+ },
5587
+ });
5588
+ const cvaDescriptionCard = cvaMerge([], {
5589
+ variants: {
5590
+ disabled: {
5591
+ true: "text-neutral-400",
5592
+ false: "text-neutral-600",
5593
+ },
5594
+ },
5595
+ defaultVariants: {
5596
+ disabled: false,
5597
+ },
5598
+ });
5599
+ const cvaIconBackground = cvaMerge(["flex", "h-8", "w-8", "items-center", "justify-center", "rounded-md", "shrink-0"], {
5600
+ variants: {
5601
+ disabled: {
5602
+ true: "bg-neutral-200",
5603
+ false: "",
5604
+ },
5605
+ },
5606
+ defaultVariants: {
5607
+ disabled: false,
5608
+ },
5609
+ });
5610
+ const cvaContentWrapper = cvaMerge(["grid", "gap-2", "px-4", "py-3"]);
5611
+
5612
+ const CENTER_INPUT_HEIGHT_THRESHOLD = 92;
5613
+ /**
5614
+ * Grid layout configuration for the PreferenceCard content area.
5615
+ * Defined at module level for optimal performance (runs once).
5616
+ */
5617
+ const preferenceCardGrid = createGrid()
5618
+ .areas(["icon", "information", "cardTag"])
5619
+ // Base: just information
5620
+ .layout({ layout: [["information"]] })
5621
+ // When all three: stacked on mobile, inline on @sm
5622
+ .layout({
5623
+ when: ["icon", "information", "cardTag"],
5624
+ layout: [
5625
+ ["icon", "cardTag"],
5626
+ ["information", "information"],
5627
+ ],
5628
+ rows: ["min-content", "1fr"],
5629
+ columns: ["min-content", "auto"],
5630
+ breakpoints: {
5631
+ "@sm": {
5632
+ layout: [["icon", "information", "cardTag"]],
5633
+ columns: ["min-content", "1fr", "auto"],
5634
+ },
5635
+ },
5636
+ })
5637
+ // Icon and information (no cardTag)
5638
+ .layout({
5639
+ when: ["icon", "information"],
5640
+ layout: [["icon", "information"]],
5641
+ columns: ["min-content", "auto"],
5642
+ })
5643
+ // Information and cardTag (no icon)
5644
+ .layout({
5645
+ when: ["cardTag", "information"],
5646
+ layout: [["information", "cardTag"]],
5647
+ })
5648
+ .build();
5649
+ /**
5650
+ * PreferenceCard is a flexible component for displaying add-ons or settings configuration options
5651
+ * with various states and visual treatments.
5652
+ * It is recommended to be primarily used as an input component, as it supports checkboxes, radio buttons and toggles.
5653
+ */
5654
+ const PreferenceCard = ({ title, description, icon, input, titleTag, cardTag, disabled = false, className, "data-testid": dataTestId, children, }) => {
5655
+ const { ref, geometry } = useMeasure();
5656
+ const gridAreas = useGridAreas(preferenceCardGrid);
5657
+ return (jsxs("div", { className: cvaPreferenceCard({
5658
+ disabled,
5659
+ className,
5660
+ }), "data-testid": dataTestId, ref: ref, children: [input ? (jsx("label", { className: twMerge(cvaInputContainer({
5661
+ itemPlacement: geometry && geometry.height < CENTER_INPUT_HEIGHT_THRESHOLD ? "center" : "start",
5662
+ }), cvaInteractableItem({
5663
+ disabled,
5664
+ selection: "auto",
5665
+ focus: "unfocused",
5666
+ })), "data-slot": "input-container", children: cloneElement(input, disabled ? { disabled: true } : {}) })) : null, jsxs("div", { className: cvaContentWrapper(), children: [jsx(GridAreas, { ...gridAreas, className: cvaContentContainer({
5667
+ itemPlacement: geometry && geometry.height < CENTER_INPUT_HEIGHT_THRESHOLD ? "center" : "start",
5668
+ }), children: slots => (jsxs(Fragment$1, { children: [jsx(IconContent, { ...slots.icon, disabled: disabled, icon: icon }), jsx(InformationContent, { ...slots.information, description: description, disabled: disabled, title: title, titleTag: titleTag }), cardTag ? (jsx("div", { ...slots.cardTag, className: "justify-self-end", children: jsx(Tag, { color: disabled ? "unknown" : cardTag.color, size: "medium", children: cardTag.title }) })) : null] })) }), Boolean(children) ? jsx("div", { "data-slot": "children", children: children }) : null] })] }));
5669
+ };
5670
+ const IconContent = ({ icon, disabled, ...rest }) => {
5671
+ if (icon?.type === "icon") {
5672
+ return (jsx("div", { ...rest, className: twMerge(cvaIconBackground({ disabled }), disabled ? "" : icon.containerClassName), children: jsx(Icon, { color: "white", name: icon.name, size: "small", type: "solid" }) }));
5673
+ }
5674
+ if (icon?.type === "image") {
5675
+ return (jsx("div", { ...rest, className: twMerge(cvaIconBackground({ disabled }), disabled ? "" : icon.containerClassName || "bg-gray-100"), children: jsx("img", { alt: icon.alt, className: "aspect-square h-8 w-8 rounded-md border border-neutral-200 object-contain", src: icon.src }) }));
5676
+ }
5677
+ return null;
5678
+ };
5679
+ const InformationContent = ({ title, description, titleTag, disabled = false, className, ...rest }) => (jsxs("div", { ...rest, className: twMerge("min-w-0 flex-1", className), children: [jsx("div", { className: "grid min-w-0 grid-cols-[1fr_auto] items-center gap-2", children: jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [jsx(Text, { className: cvaTitleCard({ disabled }), "data-slot": "title", size: "medium", weight: "bold", children: title }), titleTag ? (jsx(Tag, { color: disabled ? "unknown" : titleTag.color, "data-slot": "title-tag", size: "small", children: titleTag.title })) : null] }) }), jsx(Text, { className: cvaDescriptionCard({ disabled }), "data-slot": "description", size: "small", type: "p", children: description })] }));
5680
+
5681
+ /**
5682
+ * Default property values for PreferenceCardSkeleton component.
5683
+ */
5684
+ const DEFAULT_SKELETON_PREFERENCE_CARD_PROPS = {
5685
+ hasIcon: false,
5686
+ hasTitleTag: false,
5687
+ hasCardTag: false,
5688
+ hasInput: false,
5689
+ };
5690
+ /**
5691
+ * Generates a random width in pixels within the given range.
5692
+ *
5693
+ * Note: Uses pixel values instead of percentages (unlike ListItemSkeleton) because
5694
+ * PreferenceCard text elements have fixed pixel-based layouts with truncation,
5695
+ * not percentage-based flexible widths.
5696
+ */
5697
+ const getRandomWidth = (min, max) => {
5698
+ return Math.floor(Math.random() * (max - min + 1)) + min;
5699
+ };
5700
+ /**
5701
+ * Skeleton loading indicator that mimics the PreferenceCard component structure.
5702
+ * Uses the same grid layout, spacing, and visual hierarchy as PreferenceCard.
5703
+ */
5704
+ const PreferenceCardSkeleton = ({ hasIcon = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS.hasIcon, hasTitleTag = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS.hasTitleTag, hasCardTag = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS.hasCardTag, hasInput = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS.hasInput, }) => {
5705
+ const gridAreas = useGridAreas(preferenceCardGrid);
5706
+ // Generate stable random widths once and never change them
5707
+ const lineWidths = useMemo(() => {
5708
+ return {
5709
+ title: getRandomWidth(80, 140),
5710
+ description: getRandomWidth(160, 240),
5711
+ };
5712
+ }, []);
5713
+ return (jsxs("div", { className: cvaPreferenceCard(), children: [hasInput ? (jsx("div", { className: cvaInputContainer({ itemPlacement: "center" }), children: jsx(Skeleton, { height: 20, variant: "block", width: 20 }) })) : null, jsx("div", { className: cvaContentWrapper(), children: jsx(GridAreas, { ...gridAreas, className: cvaContentContainer({ itemPlacement: "center" }), children: slots => (jsxs(Fragment$1, { children: [hasIcon ? (jsx("div", { ...slots.icon, children: jsx(Skeleton, { className: cvaIconBackground({ disabled: false }), height: 32, variant: "block", width: 32 }) })) : null, jsxs("div", { ...slots.information, className: "min-w-0 flex-1", children: [jsx("div", { className: "grid min-w-0 grid-cols-[1fr_auto] items-center gap-2", children: jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [jsx(Skeleton, { height: "text-sm", width: lineWidths.title }), hasTitleTag ? jsx(TagSkeleton, { size: "small" }) : null] }) }), jsx(Skeleton, { height: "text-xs", width: lineWidths.description })] }), hasCardTag ? (jsx("div", { ...slots.cardTag, className: "justify-self-end", children: jsx(TagSkeleton, { size: "medium" }) })) : null] })) }) })] }));
5714
+ };
5715
+ /**
5716
+ * Simple tag skeleton for use within PreferenceCardSkeleton.
5717
+ * Renders a rounded pill shape without any props.
5718
+ */
5719
+ const TagSkeleton = ({ size }) => {
5720
+ const width = useMemo(() => getRandomWidth(40, 64), []);
5721
+ const height = size === "small" ? 18 : 24;
5722
+ return jsx(Skeleton, { className: "rounded-full", height: height, variant: "block", width: width });
5723
+ };
5724
+
4643
5725
  function useConfirmExit(confirmExit, when = true) {
4644
5726
  useBlocker(confirmExit, when);
4645
5727
  }
@@ -6083,4 +7165,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
6083
7165
  return useMemo(() => ({ focused }), [focused]);
6084
7166
  };
6085
7167
 
6086
- export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
7168
+ export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SectionHeader, Sidebar, Skeleton, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };