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