@trackunit/react-components 1.13.10 → 1.13.12
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 +1103 -6
- package/index.esm.js +1091 -9
- package/package.json +5 -5
- package/src/components/GridAreas/GridAreas.d.ts +95 -0
- package/src/components/GridAreas/createGrid.d.ts +130 -0
- package/src/components/GridAreas/generateCss.d.ts +15 -0
- package/src/components/GridAreas/types.d.ts +104 -0
- package/src/components/GridAreas/useGridAreas.d.ts +62 -0
- package/src/components/GridAreas/validateGridConfig.d.ts +9 -0
- package/src/components/GridAreas/validateGridSlots.d.ts +9 -0
- package/src/components/ListItem/ListItem.variants.d.ts +5 -0
- package/src/components/PreferenceCard/PreferenceCard.d.ts +70 -0
- package/src/components/PreferenceCard/PreferenceCard.variants.d.ts +25 -0
- package/src/components/PreferenceCard/PreferenceCardSkeleton.d.ts +25 -0
- package/src/components/Skeleton/Skeleton.d.ts +115 -0
- package/src/components/Skeleton/Skeleton.variants.d.ts +1 -0
- package/src/components/Skeleton/index.d.ts +2 -0
- package/src/index.d.ts +8 -0
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 };
|