stablekit.ts 0.4.0 → 0.5.1
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/README.md +6 -5
- package/dist/eslint.cjs +6 -1
- package/dist/eslint.d.cts +4 -3
- package/dist/eslint.d.ts +4 -3
- package/dist/eslint.js +6 -1
- package/dist/index.cjs +61 -45
- package/dist/index.d.cts +34 -11
- package/dist/index.d.ts +34 -11
- package/dist/index.js +60 -45
- package/llms.txt +39 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,10 +48,10 @@ If a component depends on async data, its bounding box is declared synchronously
|
|
|
48
48
|
|
|
49
49
|
### Spatial Pre-allocation
|
|
50
50
|
|
|
51
|
-
If a UI region has multiple states, all states render simultaneously in a CSS grid overlap. The container sizes to the largest. `
|
|
51
|
+
If a UI region has multiple states, all states render simultaneously in a CSS grid overlap. The container sizes to the largest. `StateMap` renders a dictionary of views, toggles visibility with `[inert]` + `data-state`, and never changes dimensions. `StateSwap` is a boolean convenience wrapper around `StateMap` for two-state toggles.
|
|
52
52
|
|
|
53
53
|
```tsx
|
|
54
|
-
<
|
|
54
|
+
<StateMap value={activeTab} map={{
|
|
55
55
|
profile: <Profile />,
|
|
56
56
|
invoices: <Invoices />,
|
|
57
57
|
settings: <Settings />,
|
|
@@ -74,9 +74,10 @@ Once a container expands, it cannot shrink unless explicitly reset. `SizeRatchet
|
|
|
74
74
|
| `StableText` | Typography + skeleton in one tag |
|
|
75
75
|
| `MediaSkeleton` | Aspect-ratio placeholder that constrains its child |
|
|
76
76
|
| `CollectionSkeleton` | Loading-aware list with forced stub count |
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
77
|
+
| `StateMap` | Multi-state content swap — keyed dictionary, inline, zero shift |
|
|
78
|
+
| `StateSwap` | Boolean content swap — thin wrapper around StateMap |
|
|
79
|
+
| `LayoutMap` | Block-level dictionary of views (tab panels, wizards) |
|
|
80
|
+
| `LayoutGroup` + `LayoutView` | Manual multi-state container (use StateMap or LayoutMap) |
|
|
80
81
|
| `StableCounter` | Numeric/text width pre-allocation via ghost reserve |
|
|
81
82
|
| `StableField` | Form error height pre-allocation via ghost reserve |
|
|
82
83
|
| `SizeRatchet` | Container that never shrinks (ResizeObserver ratchet) |
|
package/dist/eslint.cjs
CHANGED
|
@@ -190,7 +190,7 @@ function createArchitectureLint(options) {
|
|
|
190
190
|
// --- 4. Geometric instability (conditional content) ---
|
|
191
191
|
{
|
|
192
192
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > ConditionalExpression",
|
|
193
|
-
message: "Conditional content in JSX children. Extract to a const above the return (the linter won't flag a plain variable). Do NOT replace with the hidden attribute. For state-driven swaps, use <StateSwap> for text, <
|
|
193
|
+
message: "Conditional content in JSX children. Extract to a const above the return (the linter won't flag a plain variable). Do NOT replace with the hidden attribute. For state-driven swaps, use <StateSwap> for text, <StateMap> for keyed views, or <LoadingBoundary> for async states."
|
|
194
194
|
},
|
|
195
195
|
{
|
|
196
196
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='&&']",
|
|
@@ -208,6 +208,11 @@ function createArchitectureLint(options) {
|
|
|
208
208
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
|
|
209
209
|
message: "Interpolated text in JSX children. Extract to a const above the return (the linter won't flag a plain variable). For state-driven values, use <StableCounter> for numbers or <StateSwap> for text variants."
|
|
210
210
|
},
|
|
211
|
+
// --- 4b. Conditional hidden prop (geometric instability) ---
|
|
212
|
+
{
|
|
213
|
+
selector: "JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal)",
|
|
214
|
+
message: "Conditional hidden prop causes layout shift \u2014 the element occupies zero space when hidden, then expands when shown. Use <StateSwap> to pre-allocate geometry for all visual states."
|
|
215
|
+
},
|
|
211
216
|
// --- 5. className on firewalled components ---
|
|
212
217
|
...classNameBlocked.length ? [
|
|
213
218
|
{
|
package/dist/eslint.d.cts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* 4. Geometric instability — conditional content in JSX children that
|
|
24
24
|
* causes layout shift: ternary swaps, && mounts, || / ?? fallbacks,
|
|
25
|
-
*
|
|
26
|
-
* extracting the expression to a variable
|
|
27
|
-
* using a StableKit component (for
|
|
25
|
+
* interpolated template literals, and conditional hidden props.
|
|
26
|
+
* Each message guides toward extracting the expression to a variable
|
|
27
|
+
* (for data transforms) or using a StableKit component (for
|
|
28
|
+
* state-driven swaps). Always on.
|
|
28
29
|
*
|
|
29
30
|
* 5. className on custom components — passing className to a PascalCase
|
|
30
31
|
* component is a presentation leak across the Structure boundary.
|
package/dist/eslint.d.ts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* 4. Geometric instability — conditional content in JSX children that
|
|
24
24
|
* causes layout shift: ternary swaps, && mounts, || / ?? fallbacks,
|
|
25
|
-
*
|
|
26
|
-
* extracting the expression to a variable
|
|
27
|
-
* using a StableKit component (for
|
|
25
|
+
* interpolated template literals, and conditional hidden props.
|
|
26
|
+
* Each message guides toward extracting the expression to a variable
|
|
27
|
+
* (for data transforms) or using a StableKit component (for
|
|
28
|
+
* state-driven swaps). Always on.
|
|
28
29
|
*
|
|
29
30
|
* 5. className on custom components — passing className to a PascalCase
|
|
30
31
|
* component is a presentation leak across the Structure boundary.
|
package/dist/eslint.js
CHANGED
|
@@ -166,7 +166,7 @@ function createArchitectureLint(options) {
|
|
|
166
166
|
// --- 4. Geometric instability (conditional content) ---
|
|
167
167
|
{
|
|
168
168
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > ConditionalExpression",
|
|
169
|
-
message: "Conditional content in JSX children. Extract to a const above the return (the linter won't flag a plain variable). Do NOT replace with the hidden attribute. For state-driven swaps, use <StateSwap> for text, <
|
|
169
|
+
message: "Conditional content in JSX children. Extract to a const above the return (the linter won't flag a plain variable). Do NOT replace with the hidden attribute. For state-driven swaps, use <StateSwap> for text, <StateMap> for keyed views, or <LoadingBoundary> for async states."
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='&&']",
|
|
@@ -184,6 +184,11 @@ function createArchitectureLint(options) {
|
|
|
184
184
|
selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
|
|
185
185
|
message: "Interpolated text in JSX children. Extract to a const above the return (the linter won't flag a plain variable). For state-driven values, use <StableCounter> for numbers or <StateSwap> for text variants."
|
|
186
186
|
},
|
|
187
|
+
// --- 4b. Conditional hidden prop (geometric instability) ---
|
|
188
|
+
{
|
|
189
|
+
selector: "JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal)",
|
|
190
|
+
message: "Conditional hidden prop causes layout shift \u2014 the element occupies zero space when hidden, then expands when shown. Use <StateSwap> to pre-allocate geometry for all visual states."
|
|
191
|
+
},
|
|
187
192
|
// --- 5. className on firewalled components ---
|
|
188
193
|
...classNameBlocked.length ? [
|
|
189
194
|
{
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
StableCounter: () => StableCounter,
|
|
33
33
|
StableField: () => StableField,
|
|
34
34
|
StableText: () => StableText,
|
|
35
|
+
StateMap: () => StateMap,
|
|
35
36
|
StateSwap: () => StateSwap,
|
|
36
37
|
TextSkeleton: () => TextSkeleton,
|
|
37
38
|
createPrimitive: () => createPrimitive,
|
|
@@ -180,25 +181,39 @@ var LayoutView = (0, import_react2.forwardRef)(
|
|
|
180
181
|
}
|
|
181
182
|
);
|
|
182
183
|
|
|
183
|
-
// src/components/state-
|
|
184
|
+
// src/components/state-map.tsx
|
|
184
185
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
186
|
+
function StateMap({
|
|
187
|
+
value,
|
|
188
|
+
map,
|
|
189
|
+
as: Tag = "span",
|
|
190
|
+
...props
|
|
191
|
+
}) {
|
|
192
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LayoutGroup, { as: Tag, value, axis: "both", "data-inline": true, ...props, children: Object.entries(map).map(([key, node]) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LayoutView, { as: "span", name: key, children: node }, key)) });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/components/state-swap.tsx
|
|
196
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
185
197
|
function StateSwap({
|
|
186
198
|
state,
|
|
187
199
|
true: onTrue,
|
|
188
200
|
false: onFalse,
|
|
189
|
-
as: Tag = "span",
|
|
190
201
|
...props
|
|
191
202
|
}) {
|
|
192
|
-
return /* @__PURE__ */ (0,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
204
|
+
StateMap,
|
|
205
|
+
{
|
|
206
|
+
value: state ? "true" : "false",
|
|
207
|
+
map: { true: onTrue, false: onFalse },
|
|
208
|
+
...props
|
|
209
|
+
}
|
|
210
|
+
);
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
// src/components/layout-map.tsx
|
|
199
|
-
var
|
|
214
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
200
215
|
function LayoutMap({ value, map, ...props }) {
|
|
201
|
-
return /* @__PURE__ */ (0,
|
|
216
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LayoutGroup, { value, ...props, children: Object.entries(map).map(([key, node]) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LayoutView, { name: key, children: node }, key)) });
|
|
202
217
|
}
|
|
203
218
|
|
|
204
219
|
// src/components/size-ratchet.tsx
|
|
@@ -267,13 +282,13 @@ function useStableSlot(options = {}) {
|
|
|
267
282
|
}
|
|
268
283
|
|
|
269
284
|
// src/components/size-ratchet.tsx
|
|
270
|
-
var
|
|
285
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
271
286
|
var SizeRatchet = (0, import_react4.forwardRef)(
|
|
272
287
|
function SizeRatchet2({ axis = "height", resetKey, as: Tag = "div", className, style, children, ...props }, fwdRef) {
|
|
273
288
|
(0, import_react4.useInsertionEffect)(injectStyles, []);
|
|
274
289
|
const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis, resetKey });
|
|
275
290
|
const merged = className ? `sk-size-ratchet ${className}` : "sk-size-ratchet";
|
|
276
|
-
return /* @__PURE__ */ (0,
|
|
291
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
277
292
|
Tag,
|
|
278
293
|
{
|
|
279
294
|
ref: mergeRefs(ratchetRef, fwdRef),
|
|
@@ -288,7 +303,7 @@ var SizeRatchet = (0, import_react4.forwardRef)(
|
|
|
288
303
|
|
|
289
304
|
// src/components/stable-counter.tsx
|
|
290
305
|
var import_react5 = require("react");
|
|
291
|
-
var
|
|
306
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
292
307
|
var GHOST_STYLE = {
|
|
293
308
|
visibility: "hidden",
|
|
294
309
|
gridArea: "1 / 1",
|
|
@@ -299,7 +314,7 @@ var VALUE_STYLE = {
|
|
|
299
314
|
};
|
|
300
315
|
var StableCounter = (0, import_react5.forwardRef)(
|
|
301
316
|
function StableCounter2({ value, reserve, as: Tag = "span", style, className, ...props }, ref) {
|
|
302
|
-
return /* @__PURE__ */ (0,
|
|
317
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
303
318
|
Tag,
|
|
304
319
|
{
|
|
305
320
|
ref,
|
|
@@ -307,8 +322,8 @@ var StableCounter = (0, import_react5.forwardRef)(
|
|
|
307
322
|
style: { ...style, display: "inline-grid" },
|
|
308
323
|
...props,
|
|
309
324
|
children: [
|
|
310
|
-
/* @__PURE__ */ (0,
|
|
311
|
-
/* @__PURE__ */ (0,
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", style: GHOST_STYLE, children: reserve }),
|
|
326
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: VALUE_STYLE, children: value })
|
|
312
327
|
]
|
|
313
328
|
}
|
|
314
329
|
);
|
|
@@ -317,7 +332,7 @@ var StableCounter = (0, import_react5.forwardRef)(
|
|
|
317
332
|
|
|
318
333
|
// src/components/stable-field.tsx
|
|
319
334
|
var import_react6 = require("react");
|
|
320
|
-
var
|
|
335
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
321
336
|
var GHOST_STYLE2 = {
|
|
322
337
|
visibility: "hidden",
|
|
323
338
|
gridArea: "1 / 1",
|
|
@@ -333,11 +348,11 @@ var ERROR_HIDDEN_STYLE = {
|
|
|
333
348
|
var StableField = (0, import_react6.forwardRef)(
|
|
334
349
|
function StableField2({ error, reserve, children, style, className, ...props }, ref) {
|
|
335
350
|
const hasError = error != null && error !== false && error !== "";
|
|
336
|
-
return /* @__PURE__ */ (0,
|
|
351
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref, className, style, ...props, children: [
|
|
337
352
|
children,
|
|
338
|
-
/* @__PURE__ */ (0,
|
|
339
|
-
/* @__PURE__ */ (0,
|
|
340
|
-
/* @__PURE__ */ (0,
|
|
353
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "grid" }, children: [
|
|
354
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "aria-hidden": "true", style: GHOST_STYLE2, children: reserve }),
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
341
356
|
"span",
|
|
342
357
|
{
|
|
343
358
|
role: hasError ? "alert" : void 0,
|
|
@@ -355,17 +370,17 @@ var import_react8 = require("react");
|
|
|
355
370
|
|
|
356
371
|
// src/components/loading-context.tsx
|
|
357
372
|
var import_react7 = require("react");
|
|
358
|
-
var
|
|
373
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
359
374
|
var LoadingStateContext = (0, import_react7.createContext)(false);
|
|
360
375
|
function useLoadingState() {
|
|
361
376
|
return (0, import_react7.useContext)(LoadingStateContext);
|
|
362
377
|
}
|
|
363
378
|
function LoadingContext({ loading, children }) {
|
|
364
|
-
return /* @__PURE__ */ (0,
|
|
379
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LoadingStateContext.Provider, { value: loading, children });
|
|
365
380
|
}
|
|
366
381
|
|
|
367
382
|
// src/components/loading-boundary.tsx
|
|
368
|
-
var
|
|
383
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
369
384
|
var LoadingBoundary = (0, import_react8.forwardRef)(
|
|
370
385
|
function LoadingBoundary2({ loading, exitDuration, as: Tag = "div", className, style, children, ...props }, ref) {
|
|
371
386
|
(0, import_react8.useInsertionEffect)(injectStyles, []);
|
|
@@ -373,28 +388,28 @@ var LoadingBoundary = (0, import_react8.forwardRef)(
|
|
|
373
388
|
...style,
|
|
374
389
|
"--sk-loading-exit-duration": `${exitDuration}ms`
|
|
375
390
|
};
|
|
376
|
-
return /* @__PURE__ */ (0,
|
|
391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SizeRatchet, { ref, as: Tag, className, style: merged, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(LoadingContext, { loading, children }) });
|
|
377
392
|
}
|
|
378
393
|
);
|
|
379
394
|
|
|
380
395
|
// src/components/text-skeleton.tsx
|
|
381
396
|
var import_react9 = require("react");
|
|
382
|
-
var
|
|
397
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
383
398
|
function TextSkeleton({ loading, as: Tag = "span", className, style, children, ...props }) {
|
|
384
399
|
(0, import_react9.useInsertionEffect)(injectStyles, []);
|
|
385
400
|
const contextLoading = useLoadingState();
|
|
386
401
|
const isLoading = loading ?? contextLoading;
|
|
387
|
-
return /* @__PURE__ */ (0,
|
|
388
|
-
/* @__PURE__ */ (0,
|
|
402
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Tag, { className, style: { ...style, display: "inline-grid" }, ...props, children: [
|
|
403
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
389
404
|
"span",
|
|
390
405
|
{
|
|
391
406
|
className: "sk-shimmer-line sk-loading-layer",
|
|
392
407
|
"aria-hidden": "true",
|
|
393
408
|
style: { opacity: isLoading ? 1 : 0 },
|
|
394
|
-
children: /* @__PURE__ */ (0,
|
|
409
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { inert: true, children })
|
|
395
410
|
}
|
|
396
411
|
),
|
|
397
|
-
/* @__PURE__ */ (0,
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
398
413
|
"span",
|
|
399
414
|
{
|
|
400
415
|
className: "sk-loading-layer",
|
|
@@ -407,19 +422,19 @@ function TextSkeleton({ loading, as: Tag = "span", className, style, children, .
|
|
|
407
422
|
}
|
|
408
423
|
|
|
409
424
|
// src/components/stable-text.tsx
|
|
410
|
-
var
|
|
425
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
411
426
|
function StableText({
|
|
412
427
|
as: Tag = "p",
|
|
413
428
|
loading,
|
|
414
429
|
children,
|
|
415
430
|
...props
|
|
416
431
|
}) {
|
|
417
|
-
return /* @__PURE__ */ (0,
|
|
432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Tag, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TextSkeleton, { loading, children }) });
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
// src/components/media-skeleton.tsx
|
|
421
436
|
var import_react10 = require("react");
|
|
422
|
-
var
|
|
437
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
423
438
|
var CHILD_STYLE = {
|
|
424
439
|
position: "absolute",
|
|
425
440
|
inset: 0,
|
|
@@ -473,8 +488,8 @@ function MediaSkeleton({
|
|
|
473
488
|
...child.props.style
|
|
474
489
|
}
|
|
475
490
|
}) : child;
|
|
476
|
-
return /* @__PURE__ */ (0,
|
|
477
|
-
/* @__PURE__ */ (0,
|
|
491
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: containerClass, style: containerStyle, ...props, children: [
|
|
492
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
478
493
|
"div",
|
|
479
494
|
{
|
|
480
495
|
className: "sk-media-shimmer",
|
|
@@ -491,18 +506,18 @@ var import_react12 = require("react");
|
|
|
491
506
|
|
|
492
507
|
// src/components/skeleton-grid.tsx
|
|
493
508
|
var import_react11 = require("react");
|
|
494
|
-
var
|
|
509
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
495
510
|
var SkeletonGrid = (0, import_react11.forwardRef)(
|
|
496
511
|
function SkeletonGrid2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
|
|
497
512
|
(0, import_react11.useInsertionEffect)(injectStyles, []);
|
|
498
513
|
const merged = className ? `sk-skeleton-grid ${className}` : "sk-skeleton-grid";
|
|
499
514
|
const count = columns ? rows * columns : rows;
|
|
500
|
-
const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0,
|
|
501
|
-
/* @__PURE__ */ (0,
|
|
502
|
-
/* @__PURE__ */ (0,
|
|
515
|
+
const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "sk-skeleton-bone", children: [
|
|
516
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "sk-shimmer-line" }),
|
|
517
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "sk-shimmer-line" })
|
|
503
518
|
] }, i));
|
|
504
519
|
const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)` } : void 0;
|
|
505
|
-
return /* @__PURE__ */ (0,
|
|
520
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
506
521
|
Tag,
|
|
507
522
|
{
|
|
508
523
|
ref,
|
|
@@ -516,7 +531,7 @@ var SkeletonGrid = (0, import_react11.forwardRef)(
|
|
|
516
531
|
);
|
|
517
532
|
|
|
518
533
|
// src/components/collection-skeleton.tsx
|
|
519
|
-
var
|
|
534
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
520
535
|
function CollectionSkeletonInner({
|
|
521
536
|
items,
|
|
522
537
|
loading,
|
|
@@ -534,17 +549,17 @@ function CollectionSkeletonInner({
|
|
|
534
549
|
display: "grid",
|
|
535
550
|
"--sk-loading-exit-duration": `${exitDuration}ms`
|
|
536
551
|
};
|
|
537
|
-
return /* @__PURE__ */ (0,
|
|
538
|
-
/* @__PURE__ */ (0,
|
|
552
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(SizeRatchet, { ref, axis: "height", as: Tag, className, style: merged, ...props, children: [
|
|
553
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
539
554
|
"div",
|
|
540
555
|
{
|
|
541
556
|
className: "sk-loading-layer",
|
|
542
557
|
"aria-hidden": "true",
|
|
543
558
|
style: { opacity: loading ? 1 : 0 },
|
|
544
|
-
children: /* @__PURE__ */ (0,
|
|
559
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SkeletonGrid, { rows: stubCount })
|
|
545
560
|
}
|
|
546
561
|
),
|
|
547
|
-
/* @__PURE__ */ (0,
|
|
562
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
548
563
|
"div",
|
|
549
564
|
{
|
|
550
565
|
className: "sk-loading-layer",
|
|
@@ -585,7 +600,7 @@ function usePresence(show) {
|
|
|
585
600
|
}
|
|
586
601
|
|
|
587
602
|
// src/components/fade-transition.tsx
|
|
588
|
-
var
|
|
603
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
589
604
|
var FadeTransition = (0, import_react14.forwardRef)(
|
|
590
605
|
function FadeTransition2({ show, as: Tag = "div", className, style, children, ...props }, fwdRef) {
|
|
591
606
|
(0, import_react14.useInsertionEffect)(injectStyles, []);
|
|
@@ -593,7 +608,7 @@ var FadeTransition = (0, import_react14.forwardRef)(
|
|
|
593
608
|
if (!mounted) return null;
|
|
594
609
|
const phaseClass = phase === "entering" ? "sk-fade-entering" : phase === "exiting" ? "sk-fade-exiting" : "";
|
|
595
610
|
const merged = ["sk-fade", phaseClass, className].filter(Boolean).join(" ");
|
|
596
|
-
return /* @__PURE__ */ (0,
|
|
611
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
597
612
|
Tag,
|
|
598
613
|
{
|
|
599
614
|
ref: fwdRef,
|
|
@@ -638,6 +653,7 @@ function createPrimitive(tag, baseClass, variants) {
|
|
|
638
653
|
StableCounter,
|
|
639
654
|
StableField,
|
|
640
655
|
StableText,
|
|
656
|
+
StateMap,
|
|
641
657
|
StateSwap,
|
|
642
658
|
TextSkeleton,
|
|
643
659
|
createPrimitive,
|
package/dist/index.d.cts
CHANGED
|
@@ -15,6 +15,7 @@ interface StateSwapProps extends HTMLAttributes<HTMLElement> {
|
|
|
15
15
|
/**
|
|
16
16
|
* Boolean content swap with zero layout shift.
|
|
17
17
|
*
|
|
18
|
+
* Thin wrapper around StateMap with two keys ("true" / "false").
|
|
18
19
|
* Reserves the width of the wider option so the container never changes
|
|
19
20
|
* dimensions when toggling between states.
|
|
20
21
|
*
|
|
@@ -27,17 +28,8 @@ interface StateSwapProps extends HTMLAttributes<HTMLElement> {
|
|
|
27
28
|
* <StateSwap state={open} true="Close" false="Open" />
|
|
28
29
|
* </button>
|
|
29
30
|
* ```
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```tsx
|
|
33
|
-
* <StateSwap
|
|
34
|
-
* state={expanded}
|
|
35
|
-
* true={<ChevronUp />}
|
|
36
|
-
* false={<ChevronDown />}
|
|
37
|
-
* />
|
|
38
|
-
* ```
|
|
39
31
|
*/
|
|
40
|
-
declare function StateSwap({ state, true: onTrue, false: onFalse,
|
|
32
|
+
declare function StateSwap({ state, true: onTrue, false: onFalse, ...props }: StateSwapProps): react_jsx_runtime.JSX.Element;
|
|
41
33
|
|
|
42
34
|
type Axis = "width" | "height" | "both";
|
|
43
35
|
interface LayoutGroupProps extends HTMLAttributes<HTMLElement> {
|
|
@@ -147,6 +139,37 @@ interface LayoutMapProps<K extends string> extends HTMLAttributes<HTMLElement> {
|
|
|
147
139
|
*/
|
|
148
140
|
declare function LayoutMap<K extends string>({ value, map, ...props }: LayoutMapProps<K>): react_jsx_runtime.JSX.Element;
|
|
149
141
|
|
|
142
|
+
interface StateMapProps<K extends string> extends HTMLAttributes<HTMLElement> {
|
|
143
|
+
/**
|
|
144
|
+
* The active key — must match one of the keys in `map`.
|
|
145
|
+
*
|
|
146
|
+
* Uses `NoInfer<K>` so TypeScript infers the key union from `map`
|
|
147
|
+
* and checks `value` against it — typos are compile-time errors.
|
|
148
|
+
*/
|
|
149
|
+
value: NoInfer<K>;
|
|
150
|
+
/** Dictionary of views keyed by state name. TypeScript infers and enforces the keys. */
|
|
151
|
+
map: Record<K, ReactNode>;
|
|
152
|
+
/** HTML element to render. Default: "span" (safe inside buttons). */
|
|
153
|
+
as?: ElementType;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Multi-state content swap with zero layout shift.
|
|
157
|
+
*
|
|
158
|
+
* The keyed counterpart to StateSwap — same inline defaults,
|
|
159
|
+
* same grid overlap physics, but for N states instead of two.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <StateMap value={phase} map={{
|
|
164
|
+
* loading: <Shimmer />,
|
|
165
|
+
* unconfirmed: <Button intent="secondary">Deliver All</Button>,
|
|
166
|
+
* confirmed: <span><CheckCircle /> All confirmed</span>,
|
|
167
|
+
* chargeable: <Button intent="primary">Charge All</Button>,
|
|
168
|
+
* }} />
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function StateMap<K extends string>({ value, map, as: Tag, ...props }: StateMapProps<K>): react_jsx_runtime.JSX.Element;
|
|
172
|
+
|
|
150
173
|
interface SizeRatchetProps extends HTMLAttributes<HTMLElement> {
|
|
151
174
|
/** Which axis to ratchet. Default: "height". */
|
|
152
175
|
axis?: Axis;
|
|
@@ -470,4 +493,4 @@ declare const FadeTransition: react.ForwardRefExoticComponent<FadeTransitionProp
|
|
|
470
493
|
*/
|
|
471
494
|
declare function createPrimitive<Tag extends keyof JSX.IntrinsicElements, const Variants extends Record<string, readonly string[]> = {}>(tag: Tag, baseClass: string, variants?: Variants): (props: Omit<JSX.IntrinsicElements[Tag], "style" | "className" | keyof Variants> & { [K in keyof Variants]: Variants[K][number]; }) => react.DOMElement<Record<string, unknown>, Element>;
|
|
472
495
|
|
|
473
|
-
export { type Axis, CollectionSkeleton, type CollectionSkeletonProps, FadeTransition, type FadeTransitionProps, LayoutGroup, type LayoutGroupProps, LayoutMap, type LayoutMapProps, LayoutView, type LayoutViewProps, LoadingBoundary, type LoadingBoundaryProps, LoadingContext, type LoadingContextProps, MediaSkeleton, type MediaSkeletonProps, SizeRatchet, type SizeRatchetProps, StableCounter, type StableCounterProps, StableField, type StableFieldProps, StableText, type StableTextProps, StateSwap, type StateSwapProps, TextSkeleton, type TextSkeletonProps, createPrimitive, useLoadingState };
|
|
496
|
+
export { type Axis, CollectionSkeleton, type CollectionSkeletonProps, FadeTransition, type FadeTransitionProps, LayoutGroup, type LayoutGroupProps, LayoutMap, type LayoutMapProps, LayoutView, type LayoutViewProps, LoadingBoundary, type LoadingBoundaryProps, LoadingContext, type LoadingContextProps, MediaSkeleton, type MediaSkeletonProps, SizeRatchet, type SizeRatchetProps, StableCounter, type StableCounterProps, StableField, type StableFieldProps, StableText, type StableTextProps, StateMap, type StateMapProps, StateSwap, type StateSwapProps, TextSkeleton, type TextSkeletonProps, createPrimitive, useLoadingState };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface StateSwapProps extends HTMLAttributes<HTMLElement> {
|
|
|
15
15
|
/**
|
|
16
16
|
* Boolean content swap with zero layout shift.
|
|
17
17
|
*
|
|
18
|
+
* Thin wrapper around StateMap with two keys ("true" / "false").
|
|
18
19
|
* Reserves the width of the wider option so the container never changes
|
|
19
20
|
* dimensions when toggling between states.
|
|
20
21
|
*
|
|
@@ -27,17 +28,8 @@ interface StateSwapProps extends HTMLAttributes<HTMLElement> {
|
|
|
27
28
|
* <StateSwap state={open} true="Close" false="Open" />
|
|
28
29
|
* </button>
|
|
29
30
|
* ```
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```tsx
|
|
33
|
-
* <StateSwap
|
|
34
|
-
* state={expanded}
|
|
35
|
-
* true={<ChevronUp />}
|
|
36
|
-
* false={<ChevronDown />}
|
|
37
|
-
* />
|
|
38
|
-
* ```
|
|
39
31
|
*/
|
|
40
|
-
declare function StateSwap({ state, true: onTrue, false: onFalse,
|
|
32
|
+
declare function StateSwap({ state, true: onTrue, false: onFalse, ...props }: StateSwapProps): react_jsx_runtime.JSX.Element;
|
|
41
33
|
|
|
42
34
|
type Axis = "width" | "height" | "both";
|
|
43
35
|
interface LayoutGroupProps extends HTMLAttributes<HTMLElement> {
|
|
@@ -147,6 +139,37 @@ interface LayoutMapProps<K extends string> extends HTMLAttributes<HTMLElement> {
|
|
|
147
139
|
*/
|
|
148
140
|
declare function LayoutMap<K extends string>({ value, map, ...props }: LayoutMapProps<K>): react_jsx_runtime.JSX.Element;
|
|
149
141
|
|
|
142
|
+
interface StateMapProps<K extends string> extends HTMLAttributes<HTMLElement> {
|
|
143
|
+
/**
|
|
144
|
+
* The active key — must match one of the keys in `map`.
|
|
145
|
+
*
|
|
146
|
+
* Uses `NoInfer<K>` so TypeScript infers the key union from `map`
|
|
147
|
+
* and checks `value` against it — typos are compile-time errors.
|
|
148
|
+
*/
|
|
149
|
+
value: NoInfer<K>;
|
|
150
|
+
/** Dictionary of views keyed by state name. TypeScript infers and enforces the keys. */
|
|
151
|
+
map: Record<K, ReactNode>;
|
|
152
|
+
/** HTML element to render. Default: "span" (safe inside buttons). */
|
|
153
|
+
as?: ElementType;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Multi-state content swap with zero layout shift.
|
|
157
|
+
*
|
|
158
|
+
* The keyed counterpart to StateSwap — same inline defaults,
|
|
159
|
+
* same grid overlap physics, but for N states instead of two.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <StateMap value={phase} map={{
|
|
164
|
+
* loading: <Shimmer />,
|
|
165
|
+
* unconfirmed: <Button intent="secondary">Deliver All</Button>,
|
|
166
|
+
* confirmed: <span><CheckCircle /> All confirmed</span>,
|
|
167
|
+
* chargeable: <Button intent="primary">Charge All</Button>,
|
|
168
|
+
* }} />
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function StateMap<K extends string>({ value, map, as: Tag, ...props }: StateMapProps<K>): react_jsx_runtime.JSX.Element;
|
|
172
|
+
|
|
150
173
|
interface SizeRatchetProps extends HTMLAttributes<HTMLElement> {
|
|
151
174
|
/** Which axis to ratchet. Default: "height". */
|
|
152
175
|
axis?: Axis;
|
|
@@ -470,4 +493,4 @@ declare const FadeTransition: react.ForwardRefExoticComponent<FadeTransitionProp
|
|
|
470
493
|
*/
|
|
471
494
|
declare function createPrimitive<Tag extends keyof JSX.IntrinsicElements, const Variants extends Record<string, readonly string[]> = {}>(tag: Tag, baseClass: string, variants?: Variants): (props: Omit<JSX.IntrinsicElements[Tag], "style" | "className" | keyof Variants> & { [K in keyof Variants]: Variants[K][number]; }) => react.DOMElement<Record<string, unknown>, Element>;
|
|
472
495
|
|
|
473
|
-
export { type Axis, CollectionSkeleton, type CollectionSkeletonProps, FadeTransition, type FadeTransitionProps, LayoutGroup, type LayoutGroupProps, LayoutMap, type LayoutMapProps, LayoutView, type LayoutViewProps, LoadingBoundary, type LoadingBoundaryProps, LoadingContext, type LoadingContextProps, MediaSkeleton, type MediaSkeletonProps, SizeRatchet, type SizeRatchetProps, StableCounter, type StableCounterProps, StableField, type StableFieldProps, StableText, type StableTextProps, StateSwap, type StateSwapProps, TextSkeleton, type TextSkeletonProps, createPrimitive, useLoadingState };
|
|
496
|
+
export { type Axis, CollectionSkeleton, type CollectionSkeletonProps, FadeTransition, type FadeTransitionProps, LayoutGroup, type LayoutGroupProps, LayoutMap, type LayoutMapProps, LayoutView, type LayoutViewProps, LoadingBoundary, type LoadingBoundaryProps, LoadingContext, type LoadingContextProps, MediaSkeleton, type MediaSkeletonProps, SizeRatchet, type SizeRatchetProps, StableCounter, type StableCounterProps, StableField, type StableFieldProps, StableText, type StableTextProps, StateMap, type StateMapProps, StateSwap, type StateSwapProps, TextSkeleton, type TextSkeletonProps, createPrimitive, useLoadingState };
|
package/dist/index.js
CHANGED
|
@@ -152,25 +152,39 @@ var LayoutView = forwardRef2(
|
|
|
152
152
|
}
|
|
153
153
|
);
|
|
154
154
|
|
|
155
|
+
// src/components/state-map.tsx
|
|
156
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
157
|
+
function StateMap({
|
|
158
|
+
value,
|
|
159
|
+
map,
|
|
160
|
+
as: Tag = "span",
|
|
161
|
+
...props
|
|
162
|
+
}) {
|
|
163
|
+
return /* @__PURE__ */ jsx3(LayoutGroup, { as: Tag, value, axis: "both", "data-inline": true, ...props, children: Object.entries(map).map(([key, node]) => /* @__PURE__ */ jsx3(LayoutView, { as: "span", name: key, children: node }, key)) });
|
|
164
|
+
}
|
|
165
|
+
|
|
155
166
|
// src/components/state-swap.tsx
|
|
156
|
-
import { jsx as
|
|
167
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
157
168
|
function StateSwap({
|
|
158
169
|
state,
|
|
159
170
|
true: onTrue,
|
|
160
171
|
false: onFalse,
|
|
161
|
-
as: Tag = "span",
|
|
162
172
|
...props
|
|
163
173
|
}) {
|
|
164
|
-
return /* @__PURE__ */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
return /* @__PURE__ */ jsx4(
|
|
175
|
+
StateMap,
|
|
176
|
+
{
|
|
177
|
+
value: state ? "true" : "false",
|
|
178
|
+
map: { true: onTrue, false: onFalse },
|
|
179
|
+
...props
|
|
180
|
+
}
|
|
181
|
+
);
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
// src/components/layout-map.tsx
|
|
171
|
-
import { jsx as
|
|
185
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
172
186
|
function LayoutMap({ value, map, ...props }) {
|
|
173
|
-
return /* @__PURE__ */
|
|
187
|
+
return /* @__PURE__ */ jsx5(LayoutGroup, { value, ...props, children: Object.entries(map).map(([key, node]) => /* @__PURE__ */ jsx5(LayoutView, { name: key, children: node }, key)) });
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
// src/components/size-ratchet.tsx
|
|
@@ -239,13 +253,13 @@ function useStableSlot(options = {}) {
|
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
// src/components/size-ratchet.tsx
|
|
242
|
-
import { jsx as
|
|
256
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
243
257
|
var SizeRatchet = forwardRef3(
|
|
244
258
|
function SizeRatchet2({ axis = "height", resetKey, as: Tag = "div", className, style, children, ...props }, fwdRef) {
|
|
245
259
|
useInsertionEffect3(injectStyles, []);
|
|
246
260
|
const { ref: ratchetRef, style: ratchetStyle } = useStableSlot({ axis, resetKey });
|
|
247
261
|
const merged = className ? `sk-size-ratchet ${className}` : "sk-size-ratchet";
|
|
248
|
-
return /* @__PURE__ */
|
|
262
|
+
return /* @__PURE__ */ jsx6(
|
|
249
263
|
Tag,
|
|
250
264
|
{
|
|
251
265
|
ref: mergeRefs(ratchetRef, fwdRef),
|
|
@@ -260,7 +274,7 @@ var SizeRatchet = forwardRef3(
|
|
|
260
274
|
|
|
261
275
|
// src/components/stable-counter.tsx
|
|
262
276
|
import { forwardRef as forwardRef4 } from "react";
|
|
263
|
-
import { jsx as
|
|
277
|
+
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
264
278
|
var GHOST_STYLE = {
|
|
265
279
|
visibility: "hidden",
|
|
266
280
|
gridArea: "1 / 1",
|
|
@@ -271,7 +285,7 @@ var VALUE_STYLE = {
|
|
|
271
285
|
};
|
|
272
286
|
var StableCounter = forwardRef4(
|
|
273
287
|
function StableCounter2({ value, reserve, as: Tag = "span", style, className, ...props }, ref) {
|
|
274
|
-
return /* @__PURE__ */
|
|
288
|
+
return /* @__PURE__ */ jsxs(
|
|
275
289
|
Tag,
|
|
276
290
|
{
|
|
277
291
|
ref,
|
|
@@ -279,8 +293,8 @@ var StableCounter = forwardRef4(
|
|
|
279
293
|
style: { ...style, display: "inline-grid" },
|
|
280
294
|
...props,
|
|
281
295
|
children: [
|
|
282
|
-
/* @__PURE__ */
|
|
283
|
-
/* @__PURE__ */
|
|
296
|
+
/* @__PURE__ */ jsx7("span", { "aria-hidden": "true", style: GHOST_STYLE, children: reserve }),
|
|
297
|
+
/* @__PURE__ */ jsx7("span", { style: VALUE_STYLE, children: value })
|
|
284
298
|
]
|
|
285
299
|
}
|
|
286
300
|
);
|
|
@@ -289,7 +303,7 @@ var StableCounter = forwardRef4(
|
|
|
289
303
|
|
|
290
304
|
// src/components/stable-field.tsx
|
|
291
305
|
import { forwardRef as forwardRef5 } from "react";
|
|
292
|
-
import { jsx as
|
|
306
|
+
import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
293
307
|
var GHOST_STYLE2 = {
|
|
294
308
|
visibility: "hidden",
|
|
295
309
|
gridArea: "1 / 1",
|
|
@@ -305,11 +319,11 @@ var ERROR_HIDDEN_STYLE = {
|
|
|
305
319
|
var StableField = forwardRef5(
|
|
306
320
|
function StableField2({ error, reserve, children, style, className, ...props }, ref) {
|
|
307
321
|
const hasError = error != null && error !== false && error !== "";
|
|
308
|
-
return /* @__PURE__ */
|
|
322
|
+
return /* @__PURE__ */ jsxs2("div", { ref, className, style, ...props, children: [
|
|
309
323
|
children,
|
|
310
|
-
/* @__PURE__ */
|
|
311
|
-
/* @__PURE__ */
|
|
312
|
-
/* @__PURE__ */
|
|
324
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "grid" }, children: [
|
|
325
|
+
/* @__PURE__ */ jsx8("span", { "aria-hidden": "true", style: GHOST_STYLE2, children: reserve }),
|
|
326
|
+
/* @__PURE__ */ jsx8(
|
|
313
327
|
"span",
|
|
314
328
|
{
|
|
315
329
|
role: hasError ? "alert" : void 0,
|
|
@@ -330,17 +344,17 @@ import {
|
|
|
330
344
|
|
|
331
345
|
// src/components/loading-context.tsx
|
|
332
346
|
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
333
|
-
import { jsx as
|
|
347
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
334
348
|
var LoadingStateContext = createContext2(false);
|
|
335
349
|
function useLoadingState() {
|
|
336
350
|
return useContext2(LoadingStateContext);
|
|
337
351
|
}
|
|
338
352
|
function LoadingContext({ loading, children }) {
|
|
339
|
-
return /* @__PURE__ */
|
|
353
|
+
return /* @__PURE__ */ jsx9(LoadingStateContext.Provider, { value: loading, children });
|
|
340
354
|
}
|
|
341
355
|
|
|
342
356
|
// src/components/loading-boundary.tsx
|
|
343
|
-
import { jsx as
|
|
357
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
344
358
|
var LoadingBoundary = forwardRef6(
|
|
345
359
|
function LoadingBoundary2({ loading, exitDuration, as: Tag = "div", className, style, children, ...props }, ref) {
|
|
346
360
|
useInsertionEffect4(injectStyles, []);
|
|
@@ -348,28 +362,28 @@ var LoadingBoundary = forwardRef6(
|
|
|
348
362
|
...style,
|
|
349
363
|
"--sk-loading-exit-duration": `${exitDuration}ms`
|
|
350
364
|
};
|
|
351
|
-
return /* @__PURE__ */
|
|
365
|
+
return /* @__PURE__ */ jsx10(SizeRatchet, { ref, as: Tag, className, style: merged, ...props, children: /* @__PURE__ */ jsx10(LoadingContext, { loading, children }) });
|
|
352
366
|
}
|
|
353
367
|
);
|
|
354
368
|
|
|
355
369
|
// src/components/text-skeleton.tsx
|
|
356
370
|
import { useInsertionEffect as useInsertionEffect5 } from "react";
|
|
357
|
-
import { jsx as
|
|
371
|
+
import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
358
372
|
function TextSkeleton({ loading, as: Tag = "span", className, style, children, ...props }) {
|
|
359
373
|
useInsertionEffect5(injectStyles, []);
|
|
360
374
|
const contextLoading = useLoadingState();
|
|
361
375
|
const isLoading = loading ?? contextLoading;
|
|
362
|
-
return /* @__PURE__ */
|
|
363
|
-
/* @__PURE__ */
|
|
376
|
+
return /* @__PURE__ */ jsxs3(Tag, { className, style: { ...style, display: "inline-grid" }, ...props, children: [
|
|
377
|
+
/* @__PURE__ */ jsx11(
|
|
364
378
|
"span",
|
|
365
379
|
{
|
|
366
380
|
className: "sk-shimmer-line sk-loading-layer",
|
|
367
381
|
"aria-hidden": "true",
|
|
368
382
|
style: { opacity: isLoading ? 1 : 0 },
|
|
369
|
-
children: /* @__PURE__ */
|
|
383
|
+
children: /* @__PURE__ */ jsx11("span", { inert: true, children })
|
|
370
384
|
}
|
|
371
385
|
),
|
|
372
|
-
/* @__PURE__ */
|
|
386
|
+
/* @__PURE__ */ jsx11(
|
|
373
387
|
"span",
|
|
374
388
|
{
|
|
375
389
|
className: "sk-loading-layer",
|
|
@@ -382,14 +396,14 @@ function TextSkeleton({ loading, as: Tag = "span", className, style, children, .
|
|
|
382
396
|
}
|
|
383
397
|
|
|
384
398
|
// src/components/stable-text.tsx
|
|
385
|
-
import { jsx as
|
|
399
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
386
400
|
function StableText({
|
|
387
401
|
as: Tag = "p",
|
|
388
402
|
loading,
|
|
389
403
|
children,
|
|
390
404
|
...props
|
|
391
405
|
}) {
|
|
392
|
-
return /* @__PURE__ */
|
|
406
|
+
return /* @__PURE__ */ jsx12(Tag, { ...props, children: /* @__PURE__ */ jsx12(TextSkeleton, { loading, children }) });
|
|
393
407
|
}
|
|
394
408
|
|
|
395
409
|
// src/components/media-skeleton.tsx
|
|
@@ -403,7 +417,7 @@ import {
|
|
|
403
417
|
useRef as useRef4,
|
|
404
418
|
useState as useState2
|
|
405
419
|
} from "react";
|
|
406
|
-
import { jsx as
|
|
420
|
+
import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
407
421
|
var CHILD_STYLE = {
|
|
408
422
|
position: "absolute",
|
|
409
423
|
inset: 0,
|
|
@@ -457,8 +471,8 @@ function MediaSkeleton({
|
|
|
457
471
|
...child.props.style
|
|
458
472
|
}
|
|
459
473
|
}) : child;
|
|
460
|
-
return /* @__PURE__ */
|
|
461
|
-
/* @__PURE__ */
|
|
474
|
+
return /* @__PURE__ */ jsxs4("div", { className: containerClass, style: containerStyle, ...props, children: [
|
|
475
|
+
/* @__PURE__ */ jsx13(
|
|
462
476
|
"div",
|
|
463
477
|
{
|
|
464
478
|
className: "sk-media-shimmer",
|
|
@@ -478,18 +492,18 @@ import {
|
|
|
478
492
|
|
|
479
493
|
// src/components/skeleton-grid.tsx
|
|
480
494
|
import { forwardRef as forwardRef7, useInsertionEffect as useInsertionEffect7 } from "react";
|
|
481
|
-
import { jsx as
|
|
495
|
+
import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
482
496
|
var SkeletonGrid = forwardRef7(
|
|
483
497
|
function SkeletonGrid2({ rows, columns, as: Tag = "div", className, children, ...props }, ref) {
|
|
484
498
|
useInsertionEffect7(injectStyles, []);
|
|
485
499
|
const merged = className ? `sk-skeleton-grid ${className}` : "sk-skeleton-grid";
|
|
486
500
|
const count = columns ? rows * columns : rows;
|
|
487
|
-
const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */
|
|
488
|
-
/* @__PURE__ */
|
|
489
|
-
/* @__PURE__ */
|
|
501
|
+
const cells = Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs5("div", { className: "sk-skeleton-bone", children: [
|
|
502
|
+
/* @__PURE__ */ jsx14("div", { className: "sk-shimmer-line" }),
|
|
503
|
+
/* @__PURE__ */ jsx14("div", { className: "sk-shimmer-line" })
|
|
490
504
|
] }, i));
|
|
491
505
|
const gridStyle = columns ? { gridTemplateColumns: `repeat(${columns}, auto)` } : void 0;
|
|
492
|
-
return /* @__PURE__ */
|
|
506
|
+
return /* @__PURE__ */ jsx14(
|
|
493
507
|
Tag,
|
|
494
508
|
{
|
|
495
509
|
ref,
|
|
@@ -503,7 +517,7 @@ var SkeletonGrid = forwardRef7(
|
|
|
503
517
|
);
|
|
504
518
|
|
|
505
519
|
// src/components/collection-skeleton.tsx
|
|
506
|
-
import { jsx as
|
|
520
|
+
import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
507
521
|
function CollectionSkeletonInner({
|
|
508
522
|
items,
|
|
509
523
|
loading,
|
|
@@ -521,17 +535,17 @@ function CollectionSkeletonInner({
|
|
|
521
535
|
display: "grid",
|
|
522
536
|
"--sk-loading-exit-duration": `${exitDuration}ms`
|
|
523
537
|
};
|
|
524
|
-
return /* @__PURE__ */
|
|
525
|
-
/* @__PURE__ */
|
|
538
|
+
return /* @__PURE__ */ jsxs6(SizeRatchet, { ref, axis: "height", as: Tag, className, style: merged, ...props, children: [
|
|
539
|
+
/* @__PURE__ */ jsx15(
|
|
526
540
|
"div",
|
|
527
541
|
{
|
|
528
542
|
className: "sk-loading-layer",
|
|
529
543
|
"aria-hidden": "true",
|
|
530
544
|
style: { opacity: loading ? 1 : 0 },
|
|
531
|
-
children: /* @__PURE__ */
|
|
545
|
+
children: /* @__PURE__ */ jsx15(SkeletonGrid, { rows: stubCount })
|
|
532
546
|
}
|
|
533
547
|
),
|
|
534
|
-
/* @__PURE__ */
|
|
548
|
+
/* @__PURE__ */ jsx15(
|
|
535
549
|
"div",
|
|
536
550
|
{
|
|
537
551
|
className: "sk-loading-layer",
|
|
@@ -579,7 +593,7 @@ function usePresence(show) {
|
|
|
579
593
|
}
|
|
580
594
|
|
|
581
595
|
// src/components/fade-transition.tsx
|
|
582
|
-
import { jsx as
|
|
596
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
583
597
|
var FadeTransition = forwardRef9(
|
|
584
598
|
function FadeTransition2({ show, as: Tag = "div", className, style, children, ...props }, fwdRef) {
|
|
585
599
|
useInsertionEffect9(injectStyles, []);
|
|
@@ -587,7 +601,7 @@ var FadeTransition = forwardRef9(
|
|
|
587
601
|
if (!mounted) return null;
|
|
588
602
|
const phaseClass = phase === "entering" ? "sk-fade-entering" : phase === "exiting" ? "sk-fade-exiting" : "";
|
|
589
603
|
const merged = ["sk-fade", phaseClass, className].filter(Boolean).join(" ");
|
|
590
|
-
return /* @__PURE__ */
|
|
604
|
+
return /* @__PURE__ */ jsx16(
|
|
591
605
|
Tag,
|
|
592
606
|
{
|
|
593
607
|
ref: fwdRef,
|
|
@@ -631,6 +645,7 @@ export {
|
|
|
631
645
|
StableCounter,
|
|
632
646
|
StableField,
|
|
633
647
|
StableText,
|
|
648
|
+
StateMap,
|
|
634
649
|
StateSwap,
|
|
635
650
|
TextSkeleton,
|
|
636
651
|
createPrimitive,
|
package/llms.txt
CHANGED
|
@@ -21,7 +21,7 @@ This means:
|
|
|
21
21
|
`stubCount`, `StableText` reserves space at line-height.)
|
|
22
22
|
- **Spatial pre-allocation:** If a UI region has multiple states, all
|
|
23
23
|
states must render simultaneously in a CSS grid overlap. The container
|
|
24
|
-
sizes to the largest. (`
|
|
24
|
+
sizes to the largest. (`StateMap`, `StateSwap`, `LayoutMap`, `LayoutGroup`)
|
|
25
25
|
- **Monotonic geometry:** Once a container expands, it cannot shrink
|
|
26
26
|
unless explicitly reset. (`SizeRatchet` with optional `resetKey`)
|
|
27
27
|
- **Structural Integrity over Temporal Hacks:** Never unmount a state
|
|
@@ -46,7 +46,7 @@ rendering for the same UI region.
|
|
|
46
46
|
| `StateSwap` | Spatial | Boolean content swap, zero shift |
|
|
47
47
|
| `LayoutGroup` | Spatial | Multi-state spatial stability container |
|
|
48
48
|
| `LayoutView` | Spatial | Single view inside a LayoutGroup |
|
|
49
|
-
| `
|
|
49
|
+
| `StateMap` | Spatial | Multi-state content swap, keyed, inline |
|
|
50
50
|
| `StableCounter` | Spatial | Numeric/text width pre-allocation |
|
|
51
51
|
| `StableField` | Spatial | Form error height pre-allocation |
|
|
52
52
|
| `SizeRatchet` | Monotonic | Container that never shrinks |
|
|
@@ -125,7 +125,7 @@ CORRECT — one button, swap content inside it:
|
|
|
125
125
|
Use LayoutGroup/LayoutView only when swapping structurally different
|
|
126
126
|
components (e.g. tab panels, multi-step forms).
|
|
127
127
|
|
|
128
|
-
### 5. Do not use LayoutGroup/LayoutView for tab content — use
|
|
128
|
+
### 5. Do not use LayoutGroup/LayoutView for tab content — use StateMap
|
|
129
129
|
|
|
130
130
|
WRONG — string typos silently break rendering:
|
|
131
131
|
```tsx
|
|
@@ -137,7 +137,7 @@ WRONG — string typos silently break rendering:
|
|
|
137
137
|
|
|
138
138
|
CORRECT — dictionary keys are checked by TypeScript:
|
|
139
139
|
```tsx
|
|
140
|
-
<
|
|
140
|
+
<StateMap value={activeTab} map={{
|
|
141
141
|
profile: <Profile />,
|
|
142
142
|
invoices: <Invoices />,
|
|
143
143
|
}} />
|
|
@@ -311,11 +311,15 @@ change to structure requires editing `.css`, a boundary has leaked.
|
|
|
311
311
|
The linter also enforces geometric stability by banning all conditional
|
|
312
312
|
content in JSX children: ternary swaps (`{x ? <A/> : <B/>}`), conditional
|
|
313
313
|
mounting (`{x && <Panel/>}`), fallback content (`{x || "default"}`),
|
|
314
|
-
nullish fallbacks (`{x ?? "loading"}`),
|
|
315
|
-
(`` {`text ${var}`} ``)
|
|
314
|
+
nullish fallbacks (`{x ?? "loading"}`), interpolated template literals
|
|
315
|
+
(`` {`text ${var}`} ``), and conditional `hidden` props
|
|
316
|
+
(`hidden={x || undefined}`). The `hidden` attribute causes an element to
|
|
317
|
+
occupy zero space — toggling it conditionally is a layout shift. Use
|
|
318
|
+
`<StateSwap>` to pre-allocate geometry for all visual states instead.
|
|
319
|
+
Each error message guides toward the right fix:
|
|
316
320
|
extract the expression to a variable above the JSX (for data transforms),
|
|
317
321
|
or use a StableKit component (for state-driven swaps — StateSwap,
|
|
318
|
-
|
|
322
|
+
StateMap, LoadingBoundary, FadeTransition, StableField, StableCounter,
|
|
319
323
|
LayoutGroup). These rules are always on.
|
|
320
324
|
`classNameBlocked` declares your firewalled primitives (e.g.
|
|
321
325
|
`["Badge", "Button", "Card", "Input"]`). Only those components are flagged
|
|
@@ -428,6 +432,32 @@ currently behaving.
|
|
|
428
432
|
.sk-badge[data-variant="active"] { color: var(--color-success); }
|
|
429
433
|
```
|
|
430
434
|
|
|
435
|
+
### 12. Do not use conditional hidden props to swap between states
|
|
436
|
+
|
|
437
|
+
WRONG — hidden element takes zero space, causes layout shift when toggled:
|
|
438
|
+
```tsx
|
|
439
|
+
<Button hidden={status !== "active" || undefined}>
|
|
440
|
+
<Pause size={14} /> Pause
|
|
441
|
+
</Button>
|
|
442
|
+
<Button hidden={status !== "paused" || undefined}>
|
|
443
|
+
<Play size={14} /> Resume
|
|
444
|
+
</Button>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
CORRECT — StateSwap pre-allocates geometry for the wider option:
|
|
448
|
+
```tsx
|
|
449
|
+
<StateSwap
|
|
450
|
+
state={status === "active"}
|
|
451
|
+
true={<Button><Pause size={14} /> Pause</Button>}
|
|
452
|
+
false={<Button><Play size={14} /> Resume</Button>}
|
|
453
|
+
/>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The `hidden` attribute sets `display: none` — the element is removed from
|
|
457
|
+
flow entirely. When siblings toggle `hidden` with mutually exclusive
|
|
458
|
+
conditions, the container resizes every time. StateSwap renders both states
|
|
459
|
+
in a CSS grid overlap so the bounding box stays stable.
|
|
460
|
+
|
|
431
461
|
## Out of Scope: Font-Swap CLS
|
|
432
462
|
|
|
433
463
|
Do NOT build a component to solve font-swap layout shift. StableKit solves
|
|
@@ -444,7 +474,7 @@ or `next/font`.
|
|
|
444
474
|
|--------------------------------------------|----------------------|
|
|
445
475
|
| Button text changes on toggle | `StateSwap` |
|
|
446
476
|
| Icon changes on toggle | `StateSwap` |
|
|
447
|
-
| Tab panels with stable height | `
|
|
477
|
+
| Tab panels with stable height | `StateMap` |
|
|
448
478
|
| Multi-step wizard with stable dimensions | `LayoutGroup` + `LayoutView` |
|
|
449
479
|
| Text loading from API | `StableText` inside `LoadingBoundary` |
|
|
450
480
|
| Image/video loading causes shift | `MediaSkeleton` |
|
|
@@ -465,7 +495,7 @@ import {
|
|
|
465
495
|
StableText,
|
|
466
496
|
MediaSkeleton,
|
|
467
497
|
StateSwap,
|
|
468
|
-
|
|
498
|
+
StateMap,
|
|
469
499
|
SizeRatchet,
|
|
470
500
|
StableCounter,
|
|
471
501
|
StableField,
|
package/package.json
CHANGED