stablekit.ts 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,6 +202,31 @@ For CSP nonce support:
202
202
  <meta name="stablekit-nonce" content="your-nonce-here" />
203
203
  ```
204
204
 
205
+ ## Out of Scope: Font-Swap CLS
206
+
207
+ StableKit solves **structural** layout shifts — conditional mounting, dynamic content sizing, state-driven geometry changes. These are DOM structure problems that React components can fix.
208
+
209
+ Font-swap CLS is a different beast. When a web font loads and replaces the fallback font, the browser re-renders text with different metrics. This is a **typographic metrics** problem, not a DOM structure problem. No React component can pre-allocate geometry for a font that hasn't been downloaded yet — the browser doesn't know the dimensions until the font file arrives.
210
+
211
+ To fix font-swap CLS, use CSS `@font-face` metric overrides:
212
+
213
+ ```css
214
+ @font-face {
215
+ font-family: "Inter Fallback";
216
+ src: local("Arial");
217
+ size-adjust: 107%;
218
+ ascent-override: 90%;
219
+ descent-override: 22%;
220
+ line-gap-override: 0%;
221
+ }
222
+
223
+ body {
224
+ font-family: "Inter", "Inter Fallback", sans-serif;
225
+ }
226
+ ```
227
+
228
+ Tools like [Capsize](https://seek-oss.github.io/capsize/), [Fontaine](https://github.com/unjs/fontaine), and `next/font` generate these overrides automatically.
229
+
205
230
  ## License
206
231
 
207
232
  MIT
package/dist/eslint.cjs CHANGED
@@ -28,6 +28,8 @@ function createArchitectureLint(options) {
28
28
  stateTokens,
29
29
  variantProps = [],
30
30
  banColorUtilities = true,
31
+ classNamePassthrough = [],
32
+ loadingPassthrough = ["LoadingBoundary"],
31
33
  files = ["src/components/**/*.{tsx,jsx}"]
32
34
  } = options;
33
35
  const tokenPattern = stateTokens.join("|");
@@ -146,6 +148,23 @@ function createArchitectureLint(options) {
146
148
  {
147
149
  selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
148
150
  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."
151
+ },
152
+ // --- 5. className on custom components ---
153
+ ...classNamePassthrough.length ? [
154
+ {
155
+ selector: `JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${classNamePassthrough.join("|")})$/]) > JSXAttribute[name.name='className']`,
156
+ message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
157
+ }
158
+ ] : [
159
+ {
160
+ selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
161
+ message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
162
+ }
163
+ ],
164
+ // --- 6. Dual-paradigm conflict (loading + variable children) ---
165
+ {
166
+ selector: `JSXElement:has(JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${loadingPassthrough.join("|")})$/]) > JSXAttribute[name.name='loading']) > JSXExpressionContainer > Identifier`,
167
+ message: "Variable children inside a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
149
168
  }
150
169
  ]
151
170
  }
package/dist/eslint.d.cts CHANGED
@@ -25,6 +25,15 @@
25
25
  * and interpolated template literals. Each message guides toward
26
26
  * extracting the expression to a variable (for data transforms) or
27
27
  * using a StableKit component (for state-driven swaps). Always on.
28
+ *
29
+ * 5. className on custom components — passing className to a PascalCase
30
+ * component is a presentation leak across the Structure boundary.
31
+ * The component should own its own styling. Always on.
32
+ *
33
+ * 6. Dual-paradigm conflict — a component with a `loading` prop does
34
+ * internal content swapping (StateSwap). If children are also variable,
35
+ * both sides of the swap change simultaneously, defeating pre-allocation.
36
+ * Children of loading-swappable components must be static.
28
37
  */
29
38
  interface ArchitectureLintOptions {
30
39
  /** State token names that should never appear in JS as Tailwind classes.
@@ -38,6 +47,16 @@ interface ArchitectureLintOptions {
38
47
  * Colors must live in CSS — not in component classNames.
39
48
  * @default true */
40
49
  banColorUtilities?: boolean;
50
+ /** Components that transparently pass className to their root element.
51
+ * These are excluded from the className-on-component ban.
52
+ * e.g. ["StableText", "StableCounter", "MediaSkeleton", "ChevronDown"]
53
+ * @default [] */
54
+ classNamePassthrough?: string[];
55
+ /** Components where `loading` prop does NOT trigger a content swap
56
+ * (e.g. LoadingBoundary controls opacity, not geometry).
57
+ * These are excluded from the dual-paradigm conflict rule.
58
+ * @default ["LoadingBoundary"] */
59
+ loadingPassthrough?: string[];
41
60
  /** Glob patterns for files to lint.
42
61
  * @default ["src/components/**\/*.{tsx,jsx}"] */
43
62
  files?: string[];
package/dist/eslint.d.ts CHANGED
@@ -25,6 +25,15 @@
25
25
  * and interpolated template literals. Each message guides toward
26
26
  * extracting the expression to a variable (for data transforms) or
27
27
  * using a StableKit component (for state-driven swaps). Always on.
28
+ *
29
+ * 5. className on custom components — passing className to a PascalCase
30
+ * component is a presentation leak across the Structure boundary.
31
+ * The component should own its own styling. Always on.
32
+ *
33
+ * 6. Dual-paradigm conflict — a component with a `loading` prop does
34
+ * internal content swapping (StateSwap). If children are also variable,
35
+ * both sides of the swap change simultaneously, defeating pre-allocation.
36
+ * Children of loading-swappable components must be static.
28
37
  */
29
38
  interface ArchitectureLintOptions {
30
39
  /** State token names that should never appear in JS as Tailwind classes.
@@ -38,6 +47,16 @@ interface ArchitectureLintOptions {
38
47
  * Colors must live in CSS — not in component classNames.
39
48
  * @default true */
40
49
  banColorUtilities?: boolean;
50
+ /** Components that transparently pass className to their root element.
51
+ * These are excluded from the className-on-component ban.
52
+ * e.g. ["StableText", "StableCounter", "MediaSkeleton", "ChevronDown"]
53
+ * @default [] */
54
+ classNamePassthrough?: string[];
55
+ /** Components where `loading` prop does NOT trigger a content swap
56
+ * (e.g. LoadingBoundary controls opacity, not geometry).
57
+ * These are excluded from the dual-paradigm conflict rule.
58
+ * @default ["LoadingBoundary"] */
59
+ loadingPassthrough?: string[];
41
60
  /** Glob patterns for files to lint.
42
61
  * @default ["src/components/**\/*.{tsx,jsx}"] */
43
62
  files?: string[];
package/dist/eslint.js CHANGED
@@ -4,6 +4,8 @@ function createArchitectureLint(options) {
4
4
  stateTokens,
5
5
  variantProps = [],
6
6
  banColorUtilities = true,
7
+ classNamePassthrough = [],
8
+ loadingPassthrough = ["LoadingBoundary"],
7
9
  files = ["src/components/**/*.{tsx,jsx}"]
8
10
  } = options;
9
11
  const tokenPattern = stateTokens.join("|");
@@ -122,6 +124,23 @@ function createArchitectureLint(options) {
122
124
  {
123
125
  selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
124
126
  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."
127
+ },
128
+ // --- 5. className on custom components ---
129
+ ...classNamePassthrough.length ? [
130
+ {
131
+ selector: `JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${classNamePassthrough.join("|")})$/]) > JSXAttribute[name.name='className']`,
132
+ message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
133
+ }
134
+ ] : [
135
+ {
136
+ selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
137
+ message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
138
+ }
139
+ ],
140
+ // --- 6. Dual-paradigm conflict (loading + variable children) ---
141
+ {
142
+ selector: `JSXElement:has(JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${loadingPassthrough.join("|")})$/]) > JSXAttribute[name.name='loading']) > JSXExpressionContainer > Identifier`,
143
+ message: "Variable children inside a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
125
144
  }
126
145
  ]
127
146
  }
package/dist/index.cjs CHANGED
@@ -43,7 +43,7 @@ module.exports = __toCommonJS(index_exports);
43
43
  var import_react = require("react");
44
44
 
45
45
  // src/styles.css
46
- var styles_default = '/* stablekit \u2014 layout stability toolkit for React\n *\n * CSS class prefix: sk-\n * All animations use CSS custom properties for themability.\n * Styles are auto-injected at import time (opt-out via meta tag).\n */\n\n/* \u2500\u2500 LayoutGroup / LayoutView \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* All children overlap in the same grid cell.\n Grid auto-sizes to the largest child.\n Views use flex-column so their content stretches to fill\n the reserved width \u2014 the visual footprint is constant.\n Inline groups (StateSwap) use inline-grid for inline contexts. */\n.sk-layout-group {\n display: grid;\n}\n.sk-layout-group[data-inline] {\n display: inline-grid;\n}\n.sk-layout-group > * {\n grid-area: 1 / 1;\n display: flex;\n flex-direction: column;\n}\n.sk-layout-group[data-inline] > * {\n display: inline;\n}\n\n/* Inactive LayoutView hiding \u2014 CSS-driven via data-state attribute.\n LayoutView sets data-state="active"|"inactive" so consumers can\n override transitions on .sk-layout-view without specificity fights.\n [inert] handles accessibility (non-focusable, non-interactive). */\n.sk-layout-view[data-state="inactive"] {\n opacity: 0;\n visibility: hidden;\n}\n\n/* \u2500\u2500 SizeRatchet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* contain isolates internal reflow from ancestors. */\n.sk-size-ratchet {\n contain: layout style;\n}\n\n/* \u2500\u2500 Shimmer / Skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-skeleton-grid {\n display: grid;\n gap: var(--sk-skeleton-gap, 0.75rem);\n contain: layout style;\n}\n\n.sk-skeleton-bone {\n display: flex;\n flex-direction: column;\n gap: var(--sk-skeleton-bone-gap, 0.125rem);\n padding: var(--sk-skeleton-bone-padding, 0.375rem 0.5rem);\n}\n\n.sk-shimmer-line {\n height: 1lh;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n}\n\n/* Inert ghost inside a shimmer-line \u2014 sizes the shimmer to match\n content width exactly. Invisible and non-interactive via [inert]. */\n.sk-shimmer-line > [inert] {\n visibility: hidden;\n}\n\n@keyframes sk-shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* \u2500\u2500 MediaSkeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Loading-aware media container.\n Reserves space via aspect-ratio (set inline by the component).\n Child constraints enforced via React.cloneElement inline styles. */\n.sk-media {\n overflow: hidden;\n}\n.sk-media-shimmer {\n position: absolute;\n inset: 0;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Shared easing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Decelerate: fast start, gentle finish \u2014 for elements entering view. */\n/* Standard: balanced ease \u2014 for general-purpose transitions. */\n/* Accelerate: gentle start, fast finish \u2014 for elements leaving view. */\n:root {\n --sk-ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0);\n --sk-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --sk-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);\n}\n\n/* \u2500\u2500 FadeTransition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-fade {\n --sk-fade-duration: 400ms;\n}\n\n.sk-fade-entering {\n animation: sk-emerge var(--sk-fade-duration) var(--sk-ease-decelerate) forwards;\n}\n\n.sk-fade-exiting {\n animation: sk-collapse var(--sk-fade-duration) var(--sk-ease-accelerate) forwards;\n}\n\n@keyframes sk-emerge {\n from {\n opacity: 0;\n transform: translateY(var(--sk-fade-offset-y, -12px)) scale(var(--sk-fade-offset-scale, 0.98));\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes sk-collapse {\n from { opacity: 1; transform: scaleY(1); transform-origin: top; }\n to { opacity: 0; transform: scaleY(0); transform-origin: top; }\n}\n\n/* \u2500\u2500 Loading layers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Shared by shimmer and content layers inside skeleton components.\n Both layers permanently occupy the same grid cell; only opacity\n and interactivity change. CSS transitions handle the crossfade. */\n.sk-loading-layer {\n grid-area: 1 / 1;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Disables all animations \u2014 shimmer, fade, and loading exit.\n Layout changes still happen instantly so functionality is preserved. */\n@media (prefers-reduced-motion: reduce) {\n .sk-fade-entering,\n .sk-fade-exiting,\n .sk-shimmer-line,\n .sk-media-shimmer {\n animation-duration: 0s !important;\n }\n .sk-loading-layer,\n .sk-media-shimmer {\n transition-duration: 0s !important;\n }\n}\n';
46
+ var styles_default = '/* stablekit \u2014 layout stability toolkit for React\n *\n * CSS class prefix: sk-\n * All animations use CSS custom properties for themability.\n * Styles are auto-injected at import time (opt-out via meta tag).\n */\n\n/* \u2500\u2500 LayoutGroup / LayoutView \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* All children overlap in the same grid cell.\n Grid auto-sizes to the largest child.\n Views use flex-column so their content stretches to fill\n the reserved width \u2014 the visual footprint is constant.\n Inline groups (StateSwap) use inline-grid for inline contexts. */\n.sk-layout-group {\n display: grid;\n}\n.sk-layout-group[data-inline] {\n display: inline-grid;\n}\n.sk-layout-group > * {\n grid-area: 1 / 1;\n display: flex;\n flex-direction: column;\n}\n.sk-layout-group[data-inline] > * {\n display: inline-flex;\n align-items: center;\n}\n\n/* Inactive LayoutView hiding \u2014 CSS-driven via data-state attribute.\n LayoutView sets data-state="active"|"inactive" so consumers can\n override transitions on .sk-layout-view without specificity fights.\n [inert] handles accessibility (non-focusable, non-interactive). */\n.sk-layout-view[data-state="inactive"] {\n opacity: 0;\n visibility: hidden;\n}\n\n/* \u2500\u2500 SizeRatchet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* contain isolates internal reflow from ancestors. */\n.sk-size-ratchet {\n contain: layout style;\n}\n\n/* \u2500\u2500 Shimmer / Skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-skeleton-grid {\n display: grid;\n gap: var(--sk-skeleton-gap, 0.75rem);\n contain: layout style;\n}\n\n.sk-skeleton-bone {\n display: flex;\n flex-direction: column;\n gap: var(--sk-skeleton-bone-gap, 0.125rem);\n padding: var(--sk-skeleton-bone-padding, 0.375rem 0.5rem);\n}\n\n.sk-shimmer-line {\n height: 1lh;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n}\n\n/* Inert ghost inside a shimmer-line \u2014 sizes the shimmer to match\n content width exactly. Invisible and non-interactive via [inert]. */\n.sk-shimmer-line > [inert] {\n visibility: hidden;\n}\n\n@keyframes sk-shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* \u2500\u2500 MediaSkeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Loading-aware media container.\n Reserves space via aspect-ratio (set inline by the component).\n Child constraints enforced via React.cloneElement inline styles. */\n.sk-media {\n overflow: hidden;\n}\n.sk-media-shimmer {\n position: absolute;\n inset: 0;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Shared easing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Decelerate: fast start, gentle finish \u2014 for elements entering view. */\n/* Standard: balanced ease \u2014 for general-purpose transitions. */\n/* Accelerate: gentle start, fast finish \u2014 for elements leaving view. */\n:root {\n --sk-ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0);\n --sk-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --sk-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);\n}\n\n/* \u2500\u2500 FadeTransition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-fade {\n --sk-fade-duration: 400ms;\n}\n\n.sk-fade-entering {\n animation: sk-emerge var(--sk-fade-duration) var(--sk-ease-decelerate) forwards;\n}\n\n.sk-fade-exiting {\n animation: sk-collapse var(--sk-fade-duration) var(--sk-ease-accelerate) forwards;\n}\n\n@keyframes sk-emerge {\n from {\n opacity: 0;\n transform: translateY(var(--sk-fade-offset-y, -12px)) scale(var(--sk-fade-offset-scale, 0.98));\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes sk-collapse {\n from { opacity: 1; transform: scaleY(1); transform-origin: top; }\n to { opacity: 0; transform: scaleY(0); transform-origin: top; }\n}\n\n/* \u2500\u2500 Loading layers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Shared by shimmer and content layers inside skeleton components.\n Both layers permanently occupy the same grid cell; only opacity\n and interactivity change. CSS transitions handle the crossfade. */\n.sk-loading-layer {\n grid-area: 1 / 1;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Disables all animations \u2014 shimmer, fade, and loading exit.\n Layout changes still happen instantly so functionality is preserved. */\n@media (prefers-reduced-motion: reduce) {\n .sk-fade-entering,\n .sk-fade-exiting,\n .sk-shimmer-line,\n .sk-media-shimmer {\n animation-duration: 0s !important;\n }\n .sk-loading-layer,\n .sk-media-shimmer {\n transition-duration: 0s !important;\n }\n}\n';
47
47
 
48
48
  // src/internal/inject-styles.ts
49
49
  var injected = false;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  } from "react";
10
10
 
11
11
  // src/styles.css
12
- var styles_default = '/* stablekit \u2014 layout stability toolkit for React\n *\n * CSS class prefix: sk-\n * All animations use CSS custom properties for themability.\n * Styles are auto-injected at import time (opt-out via meta tag).\n */\n\n/* \u2500\u2500 LayoutGroup / LayoutView \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* All children overlap in the same grid cell.\n Grid auto-sizes to the largest child.\n Views use flex-column so their content stretches to fill\n the reserved width \u2014 the visual footprint is constant.\n Inline groups (StateSwap) use inline-grid for inline contexts. */\n.sk-layout-group {\n display: grid;\n}\n.sk-layout-group[data-inline] {\n display: inline-grid;\n}\n.sk-layout-group > * {\n grid-area: 1 / 1;\n display: flex;\n flex-direction: column;\n}\n.sk-layout-group[data-inline] > * {\n display: inline;\n}\n\n/* Inactive LayoutView hiding \u2014 CSS-driven via data-state attribute.\n LayoutView sets data-state="active"|"inactive" so consumers can\n override transitions on .sk-layout-view without specificity fights.\n [inert] handles accessibility (non-focusable, non-interactive). */\n.sk-layout-view[data-state="inactive"] {\n opacity: 0;\n visibility: hidden;\n}\n\n/* \u2500\u2500 SizeRatchet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* contain isolates internal reflow from ancestors. */\n.sk-size-ratchet {\n contain: layout style;\n}\n\n/* \u2500\u2500 Shimmer / Skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-skeleton-grid {\n display: grid;\n gap: var(--sk-skeleton-gap, 0.75rem);\n contain: layout style;\n}\n\n.sk-skeleton-bone {\n display: flex;\n flex-direction: column;\n gap: var(--sk-skeleton-bone-gap, 0.125rem);\n padding: var(--sk-skeleton-bone-padding, 0.375rem 0.5rem);\n}\n\n.sk-shimmer-line {\n height: 1lh;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n}\n\n/* Inert ghost inside a shimmer-line \u2014 sizes the shimmer to match\n content width exactly. Invisible and non-interactive via [inert]. */\n.sk-shimmer-line > [inert] {\n visibility: hidden;\n}\n\n@keyframes sk-shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* \u2500\u2500 MediaSkeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Loading-aware media container.\n Reserves space via aspect-ratio (set inline by the component).\n Child constraints enforced via React.cloneElement inline styles. */\n.sk-media {\n overflow: hidden;\n}\n.sk-media-shimmer {\n position: absolute;\n inset: 0;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Shared easing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Decelerate: fast start, gentle finish \u2014 for elements entering view. */\n/* Standard: balanced ease \u2014 for general-purpose transitions. */\n/* Accelerate: gentle start, fast finish \u2014 for elements leaving view. */\n:root {\n --sk-ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0);\n --sk-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --sk-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);\n}\n\n/* \u2500\u2500 FadeTransition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-fade {\n --sk-fade-duration: 400ms;\n}\n\n.sk-fade-entering {\n animation: sk-emerge var(--sk-fade-duration) var(--sk-ease-decelerate) forwards;\n}\n\n.sk-fade-exiting {\n animation: sk-collapse var(--sk-fade-duration) var(--sk-ease-accelerate) forwards;\n}\n\n@keyframes sk-emerge {\n from {\n opacity: 0;\n transform: translateY(var(--sk-fade-offset-y, -12px)) scale(var(--sk-fade-offset-scale, 0.98));\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes sk-collapse {\n from { opacity: 1; transform: scaleY(1); transform-origin: top; }\n to { opacity: 0; transform: scaleY(0); transform-origin: top; }\n}\n\n/* \u2500\u2500 Loading layers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Shared by shimmer and content layers inside skeleton components.\n Both layers permanently occupy the same grid cell; only opacity\n and interactivity change. CSS transitions handle the crossfade. */\n.sk-loading-layer {\n grid-area: 1 / 1;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Disables all animations \u2014 shimmer, fade, and loading exit.\n Layout changes still happen instantly so functionality is preserved. */\n@media (prefers-reduced-motion: reduce) {\n .sk-fade-entering,\n .sk-fade-exiting,\n .sk-shimmer-line,\n .sk-media-shimmer {\n animation-duration: 0s !important;\n }\n .sk-loading-layer,\n .sk-media-shimmer {\n transition-duration: 0s !important;\n }\n}\n';
12
+ var styles_default = '/* stablekit \u2014 layout stability toolkit for React\n *\n * CSS class prefix: sk-\n * All animations use CSS custom properties for themability.\n * Styles are auto-injected at import time (opt-out via meta tag).\n */\n\n/* \u2500\u2500 LayoutGroup / LayoutView \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* All children overlap in the same grid cell.\n Grid auto-sizes to the largest child.\n Views use flex-column so their content stretches to fill\n the reserved width \u2014 the visual footprint is constant.\n Inline groups (StateSwap) use inline-grid for inline contexts. */\n.sk-layout-group {\n display: grid;\n}\n.sk-layout-group[data-inline] {\n display: inline-grid;\n}\n.sk-layout-group > * {\n grid-area: 1 / 1;\n display: flex;\n flex-direction: column;\n}\n.sk-layout-group[data-inline] > * {\n display: inline-flex;\n align-items: center;\n}\n\n/* Inactive LayoutView hiding \u2014 CSS-driven via data-state attribute.\n LayoutView sets data-state="active"|"inactive" so consumers can\n override transitions on .sk-layout-view without specificity fights.\n [inert] handles accessibility (non-focusable, non-interactive). */\n.sk-layout-view[data-state="inactive"] {\n opacity: 0;\n visibility: hidden;\n}\n\n/* \u2500\u2500 SizeRatchet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* contain isolates internal reflow from ancestors. */\n.sk-size-ratchet {\n contain: layout style;\n}\n\n/* \u2500\u2500 Shimmer / Skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-skeleton-grid {\n display: grid;\n gap: var(--sk-skeleton-gap, 0.75rem);\n contain: layout style;\n}\n\n.sk-skeleton-bone {\n display: flex;\n flex-direction: column;\n gap: var(--sk-skeleton-bone-gap, 0.125rem);\n padding: var(--sk-skeleton-bone-padding, 0.375rem 0.5rem);\n}\n\n.sk-shimmer-line {\n height: 1lh;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n}\n\n/* Inert ghost inside a shimmer-line \u2014 sizes the shimmer to match\n content width exactly. Invisible and non-interactive via [inert]. */\n.sk-shimmer-line > [inert] {\n visibility: hidden;\n}\n\n@keyframes sk-shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* \u2500\u2500 MediaSkeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Loading-aware media container.\n Reserves space via aspect-ratio (set inline by the component).\n Child constraints enforced via React.cloneElement inline styles. */\n.sk-media {\n overflow: hidden;\n}\n.sk-media-shimmer {\n position: absolute;\n inset: 0;\n border-radius: var(--sk-shimmer-radius, 0.125rem);\n background: linear-gradient(\n 90deg,\n var(--sk-shimmer-color, #e5e7eb) 25%,\n var(--sk-shimmer-highlight, #f3f4f6) 50%,\n var(--sk-shimmer-color, #e5e7eb) 75%\n );\n background-size: 200% 100%;\n animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Shared easing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Decelerate: fast start, gentle finish \u2014 for elements entering view. */\n/* Standard: balanced ease \u2014 for general-purpose transitions. */\n/* Accelerate: gentle start, fast finish \u2014 for elements leaving view. */\n:root {\n --sk-ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0);\n --sk-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);\n --sk-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);\n}\n\n/* \u2500\u2500 FadeTransition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sk-fade {\n --sk-fade-duration: 400ms;\n}\n\n.sk-fade-entering {\n animation: sk-emerge var(--sk-fade-duration) var(--sk-ease-decelerate) forwards;\n}\n\n.sk-fade-exiting {\n animation: sk-collapse var(--sk-fade-duration) var(--sk-ease-accelerate) forwards;\n}\n\n@keyframes sk-emerge {\n from {\n opacity: 0;\n transform: translateY(var(--sk-fade-offset-y, -12px)) scale(var(--sk-fade-offset-scale, 0.98));\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes sk-collapse {\n from { opacity: 1; transform: scaleY(1); transform-origin: top; }\n to { opacity: 0; transform: scaleY(0); transform-origin: top; }\n}\n\n/* \u2500\u2500 Loading layers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Shared by shimmer and content layers inside skeleton components.\n Both layers permanently occupy the same grid cell; only opacity\n and interactivity change. CSS transitions handle the crossfade. */\n.sk-loading-layer {\n grid-area: 1 / 1;\n transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);\n}\n\n/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Disables all animations \u2014 shimmer, fade, and loading exit.\n Layout changes still happen instantly so functionality is preserved. */\n@media (prefers-reduced-motion: reduce) {\n .sk-fade-entering,\n .sk-fade-exiting,\n .sk-shimmer-line,\n .sk-media-shimmer {\n animation-duration: 0s !important;\n }\n .sk-loading-layer,\n .sk-media-shimmer {\n transition-duration: 0s !important;\n }\n}\n';
13
13
 
14
14
  // src/internal/inject-styles.ts
15
15
  var injected = false;
package/dist/styles.css CHANGED
@@ -24,7 +24,8 @@
24
24
  flex-direction: column;
25
25
  }
26
26
  .sk-layout-group[data-inline] > * {
27
- display: inline;
27
+ display: inline-flex;
28
+ align-items: center;
28
29
  }
29
30
 
30
31
  /* Inactive LayoutView hiding — CSS-driven via data-state attribute.
package/llms.txt CHANGED
@@ -415,6 +415,16 @@ currently behaving.
415
415
  .sk-badge[data-variant="active"] { color: var(--color-success); }
416
416
  ```
417
417
 
418
+ ## Out of Scope: Font-Swap CLS
419
+
420
+ Do NOT build a component to solve font-swap layout shift. StableKit solves
421
+ structural CLS (conditional mounting, dynamic content sizing). Font-swap CLS
422
+ is a typographic metrics problem — no React component can pre-allocate
423
+ geometry for a font that hasn't been downloaded yet. The fix is CSS
424
+ `@font-face` metric overrides (`size-adjust`, `ascent-override`,
425
+ `descent-override`, `line-gap-override`) via tools like Capsize, Fontaine,
426
+ or `next/font`.
427
+
418
428
  ## Component selection guide
419
429
 
420
430
  | Problem | Component |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stablekit.ts",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "React toolkit for layout stability — zero-shift components for loading states, content swaps, and spatial containers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",