stablekit.ts 0.2.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.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/stylelint.ts
21
+ var stylelint_exports = {};
22
+ __export(stylelint_exports, {
23
+ createStyleLint: () => createStyleLint
24
+ });
25
+ module.exports = __toCommonJS(stylelint_exports);
26
+ var pluginRuleName = "stablekit/no-functional-in-utility";
27
+ var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
28
+ function createFunctionalTokenPlugin(prefixes) {
29
+ const rule = (enabled) => {
30
+ return (root, result) => {
31
+ if (!enabled) return;
32
+ root.walkAtRules("utility", (atRule) => {
33
+ atRule.walkDecls((decl) => {
34
+ for (const prefix of prefixes) {
35
+ if (decl.value.includes(prefix)) {
36
+ result.warn(
37
+ `Functional token "${prefix}*" inside @utility. Functional colors belong in scoped selectors (e.g. .badge[data-status="paid"]), not reusable utilities.`,
38
+ { node: decl }
39
+ );
40
+ }
41
+ }
42
+ });
43
+ });
44
+ };
45
+ };
46
+ return {
47
+ ruleName: pluginRuleName,
48
+ rule
49
+ };
50
+ }
51
+ function createDescendantColorPlugin() {
52
+ const dataAttrWithDescendant = /\[data-[^\]]+\]\s+[.#\w]/;
53
+ const colorApplyPattern = /\btext-(?!xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|right|center|justify|wrap|nowrap|ellipsis|clip|truncate)\w/;
54
+ const message = `Setting color on a descendant inside a data-attribute selector. Set color on the [data-*] container and let children inherit via currentColor.`;
55
+ const rule = (enabled) => {
56
+ return (root, result) => {
57
+ if (!enabled) return;
58
+ root.walkRules((ruleNode) => {
59
+ if (!dataAttrWithDescendant.test(ruleNode.selector)) return;
60
+ ruleNode.walkDecls("color", (decl) => {
61
+ result.warn(message, { node: decl });
62
+ });
63
+ ruleNode.walkAtRules("apply", (atRule) => {
64
+ if (colorApplyPattern.test(atRule.params)) {
65
+ result.warn(message, { node: atRule });
66
+ }
67
+ });
68
+ });
69
+ };
70
+ };
71
+ return {
72
+ ruleName: descendantColorRuleName,
73
+ rule
74
+ };
75
+ }
76
+ function createStyleLint(options = {}) {
77
+ const {
78
+ ignoreTypes = ["html", "body"],
79
+ functionalTokens = [],
80
+ files = ["src/**/*.css"]
81
+ } = options;
82
+ const plugins = [createDescendantColorPlugin()];
83
+ if (functionalTokens.length > 0) {
84
+ plugins.push(createFunctionalTokenPlugin(functionalTokens));
85
+ }
86
+ const rules = {
87
+ // Ban element selectors — use classes and data-attributes instead.
88
+ "selector-max-type": [
89
+ 0,
90
+ {
91
+ ignoreTypes
92
+ }
93
+ ],
94
+ // !important breaks the cascade and fights the architecture.
95
+ "declaration-no-important": true,
96
+ // Ban color on descendants inside data-attribute selectors.
97
+ [descendantColorRuleName]: true,
98
+ // Ban animating layout properties — causes reflow on every frame.
99
+ // Use transform (scaleY, translateY) or opacity instead.
100
+ "declaration-property-value-disallowed-list": [
101
+ {
102
+ "transition": [/\b(width|height|max-height|min-height|max-width|min-width|margin|padding|top|right|bottom|left)\b/],
103
+ "transition-property": [/\b(width|height|max-height|min-height|max-width|min-width|margin|padding|top|right|bottom|left)\b/],
104
+ "animation-name": []
105
+ },
106
+ { message: "Animating layout properties causes reflow. Use transform or opacity instead." }
107
+ ]
108
+ };
109
+ if (functionalTokens.length > 0) {
110
+ rules[pluginRuleName] = true;
111
+ }
112
+ return { files, plugins, rules };
113
+ }
114
+ // Annotate the CommonJS export names for ESM import in node:
115
+ 0 && (module.exports = {
116
+ createStyleLint
117
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CSS Architecture Linter Factory
3
+ *
4
+ * Enforces the Structure → Presentation boundary on the CSS side.
5
+ *
6
+ * Four rules:
7
+ *
8
+ * 1. Don't target child elements by tag name for visual properties.
9
+ * `& svg { color: green }` is wrong — set color on the container
10
+ * and let currentColor inherit.
11
+ *
12
+ * 2. Don't use !important — it breaks the cascade.
13
+ *
14
+ * 3. Don't launder functional color tokens through @utility.
15
+ * `@utility text-status-success { color: var(--color-status-active) }`
16
+ * turns a functional color into a reusable className, which crosses
17
+ * back from Presentation into Structure. Functional tokens belong
18
+ * in scoped selectors like `.badge[data-status="paid"]`, not in
19
+ * utilities that can spread via @apply or className.
20
+ *
21
+ * 4. Don't set color on descendants inside data-attribute selectors.
22
+ * `.card[data-status="error"] .icon { color: red }` is wrong —
23
+ * set color on the container and let children inherit via currentColor.
24
+ *
25
+ * 5. Don't animate layout properties (width, height, margin, padding,
26
+ * top/right/bottom/left). These trigger reflow on every frame.
27
+ * Use transform (scaleY, translateY) or opacity instead.
28
+ */
29
+ interface StyleLintOptions {
30
+ /** Element selectors to allow (e.g. in resets).
31
+ * @default ["html", "body"] */
32
+ ignoreTypes?: string[];
33
+ /** CSS custom property prefixes that must not appear inside @utility blocks.
34
+ * e.g. ["--color-status-", "--color-danger"]
35
+ * Any var() referencing these inside @utility is a lint error. */
36
+ functionalTokens?: string[];
37
+ /** Glob patterns for files to lint.
38
+ * @default ["src/**\/*.css"] */
39
+ files?: string[];
40
+ }
41
+ declare function createStyleLint(options?: StyleLintOptions): {
42
+ files: string[];
43
+ plugins: unknown[];
44
+ rules: Record<string, unknown>;
45
+ };
46
+
47
+ export { type StyleLintOptions, createStyleLint };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CSS Architecture Linter Factory
3
+ *
4
+ * Enforces the Structure → Presentation boundary on the CSS side.
5
+ *
6
+ * Four rules:
7
+ *
8
+ * 1. Don't target child elements by tag name for visual properties.
9
+ * `& svg { color: green }` is wrong — set color on the container
10
+ * and let currentColor inherit.
11
+ *
12
+ * 2. Don't use !important — it breaks the cascade.
13
+ *
14
+ * 3. Don't launder functional color tokens through @utility.
15
+ * `@utility text-status-success { color: var(--color-status-active) }`
16
+ * turns a functional color into a reusable className, which crosses
17
+ * back from Presentation into Structure. Functional tokens belong
18
+ * in scoped selectors like `.badge[data-status="paid"]`, not in
19
+ * utilities that can spread via @apply or className.
20
+ *
21
+ * 4. Don't set color on descendants inside data-attribute selectors.
22
+ * `.card[data-status="error"] .icon { color: red }` is wrong —
23
+ * set color on the container and let children inherit via currentColor.
24
+ *
25
+ * 5. Don't animate layout properties (width, height, margin, padding,
26
+ * top/right/bottom/left). These trigger reflow on every frame.
27
+ * Use transform (scaleY, translateY) or opacity instead.
28
+ */
29
+ interface StyleLintOptions {
30
+ /** Element selectors to allow (e.g. in resets).
31
+ * @default ["html", "body"] */
32
+ ignoreTypes?: string[];
33
+ /** CSS custom property prefixes that must not appear inside @utility blocks.
34
+ * e.g. ["--color-status-", "--color-danger"]
35
+ * Any var() referencing these inside @utility is a lint error. */
36
+ functionalTokens?: string[];
37
+ /** Glob patterns for files to lint.
38
+ * @default ["src/**\/*.css"] */
39
+ files?: string[];
40
+ }
41
+ declare function createStyleLint(options?: StyleLintOptions): {
42
+ files: string[];
43
+ plugins: unknown[];
44
+ rules: Record<string, unknown>;
45
+ };
46
+
47
+ export { type StyleLintOptions, createStyleLint };
@@ -0,0 +1,92 @@
1
+ // src/stylelint.ts
2
+ var pluginRuleName = "stablekit/no-functional-in-utility";
3
+ var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
4
+ function createFunctionalTokenPlugin(prefixes) {
5
+ const rule = (enabled) => {
6
+ return (root, result) => {
7
+ if (!enabled) return;
8
+ root.walkAtRules("utility", (atRule) => {
9
+ atRule.walkDecls((decl) => {
10
+ for (const prefix of prefixes) {
11
+ if (decl.value.includes(prefix)) {
12
+ result.warn(
13
+ `Functional token "${prefix}*" inside @utility. Functional colors belong in scoped selectors (e.g. .badge[data-status="paid"]), not reusable utilities.`,
14
+ { node: decl }
15
+ );
16
+ }
17
+ }
18
+ });
19
+ });
20
+ };
21
+ };
22
+ return {
23
+ ruleName: pluginRuleName,
24
+ rule
25
+ };
26
+ }
27
+ function createDescendantColorPlugin() {
28
+ const dataAttrWithDescendant = /\[data-[^\]]+\]\s+[.#\w]/;
29
+ const colorApplyPattern = /\btext-(?!xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|right|center|justify|wrap|nowrap|ellipsis|clip|truncate)\w/;
30
+ const message = `Setting color on a descendant inside a data-attribute selector. Set color on the [data-*] container and let children inherit via currentColor.`;
31
+ const rule = (enabled) => {
32
+ return (root, result) => {
33
+ if (!enabled) return;
34
+ root.walkRules((ruleNode) => {
35
+ if (!dataAttrWithDescendant.test(ruleNode.selector)) return;
36
+ ruleNode.walkDecls("color", (decl) => {
37
+ result.warn(message, { node: decl });
38
+ });
39
+ ruleNode.walkAtRules("apply", (atRule) => {
40
+ if (colorApplyPattern.test(atRule.params)) {
41
+ result.warn(message, { node: atRule });
42
+ }
43
+ });
44
+ });
45
+ };
46
+ };
47
+ return {
48
+ ruleName: descendantColorRuleName,
49
+ rule
50
+ };
51
+ }
52
+ function createStyleLint(options = {}) {
53
+ const {
54
+ ignoreTypes = ["html", "body"],
55
+ functionalTokens = [],
56
+ files = ["src/**/*.css"]
57
+ } = options;
58
+ const plugins = [createDescendantColorPlugin()];
59
+ if (functionalTokens.length > 0) {
60
+ plugins.push(createFunctionalTokenPlugin(functionalTokens));
61
+ }
62
+ const rules = {
63
+ // Ban element selectors — use classes and data-attributes instead.
64
+ "selector-max-type": [
65
+ 0,
66
+ {
67
+ ignoreTypes
68
+ }
69
+ ],
70
+ // !important breaks the cascade and fights the architecture.
71
+ "declaration-no-important": true,
72
+ // Ban color on descendants inside data-attribute selectors.
73
+ [descendantColorRuleName]: true,
74
+ // Ban animating layout properties — causes reflow on every frame.
75
+ // Use transform (scaleY, translateY) or opacity instead.
76
+ "declaration-property-value-disallowed-list": [
77
+ {
78
+ "transition": [/\b(width|height|max-height|min-height|max-width|min-width|margin|padding|top|right|bottom|left)\b/],
79
+ "transition-property": [/\b(width|height|max-height|min-height|max-width|min-width|margin|padding|top|right|bottom|left)\b/],
80
+ "animation-name": []
81
+ },
82
+ { message: "Animating layout properties causes reflow. Use transform or opacity instead." }
83
+ ]
84
+ };
85
+ if (functionalTokens.length > 0) {
86
+ rules[pluginRuleName] = true;
87
+ }
88
+ return { files, plugins, rules };
89
+ }
90
+ export {
91
+ createStyleLint
92
+ };
@@ -0,0 +1,178 @@
1
+ /* stablekit — layout stability toolkit for React
2
+ *
3
+ * CSS class prefix: sk-
4
+ * All animations use CSS custom properties for themability.
5
+ * Styles are auto-injected at import time (opt-out via meta tag).
6
+ */
7
+
8
+ /* ── LayoutGroup / LayoutView ──────────────────────────────────────────────── */
9
+
10
+ /* All children overlap in the same grid cell.
11
+ Grid auto-sizes to the largest child.
12
+ Block-level groups use flex-column so content stretches to fill
13
+ the reserved width. Inline groups (data-inline) skip this — their
14
+ children are inline content that shouldn't be forced vertical. */
15
+ .sk-layout-group {
16
+ display: grid;
17
+ }
18
+ .sk-layout-group[data-inline] {
19
+ display: inline-grid;
20
+ }
21
+ .sk-layout-group > * {
22
+ grid-area: 1 / 1;
23
+ }
24
+ .sk-layout-group:not([data-inline]) > * {
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+ /* Inline groups: children inherit the parent's inline flow.
29
+ Without this, grid items default to block and stack content. */
30
+ .sk-layout-group[data-inline] > * {
31
+ display: inline;
32
+ }
33
+
34
+ /* Inactive LayoutView hiding — CSS-driven via data-state attribute.
35
+ LayoutView sets data-state="active"|"inactive" so consumers can
36
+ override transitions on .sk-layout-view without specificity fights.
37
+ [inert] handles accessibility (non-focusable, non-interactive). */
38
+ .sk-layout-view[data-state="inactive"] {
39
+ opacity: 0;
40
+ visibility: hidden;
41
+ }
42
+
43
+ /* ── SizeRatchet ───────────────────────────────────────────────────────────── */
44
+
45
+ /* contain isolates internal reflow from ancestors. */
46
+ .sk-size-ratchet {
47
+ contain: layout style;
48
+ }
49
+
50
+ /* ── Shimmer / Skeleton ────────────────────────────────────────────────────── */
51
+
52
+ .sk-skeleton-grid {
53
+ display: grid;
54
+ gap: var(--sk-skeleton-gap, 0.75rem);
55
+ contain: layout style;
56
+ }
57
+
58
+ .sk-skeleton-bone {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--sk-skeleton-bone-gap, 0.125rem);
62
+ padding: var(--sk-skeleton-bone-padding, 0.375rem 0.5rem);
63
+ }
64
+
65
+ .sk-shimmer-line {
66
+ height: 1lh;
67
+ border-radius: var(--sk-shimmer-radius, 0.125rem);
68
+ background: linear-gradient(
69
+ 90deg,
70
+ var(--sk-shimmer-color, #e5e7eb) 25%,
71
+ var(--sk-shimmer-highlight, #f3f4f6) 50%,
72
+ var(--sk-shimmer-color, #e5e7eb) 75%
73
+ );
74
+ background-size: 200% 100%;
75
+ animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;
76
+ }
77
+
78
+ /* Inert ghost inside a shimmer-line — sizes the shimmer to match
79
+ content width exactly. Invisible and non-interactive via [inert]. */
80
+ .sk-shimmer-line > [inert] {
81
+ visibility: hidden;
82
+ }
83
+
84
+ @keyframes sk-shimmer {
85
+ 0% { background-position: 200% 0; }
86
+ 100% { background-position: -200% 0; }
87
+ }
88
+
89
+ /* ── MediaSkeleton ─────────────────────────────────────────────────────────── */
90
+
91
+ /* Loading-aware media container.
92
+ Reserves space via aspect-ratio (set inline by the component).
93
+ Child constraints enforced via React.cloneElement inline styles. */
94
+ .sk-media {
95
+ overflow: hidden;
96
+ }
97
+ .sk-media-shimmer {
98
+ position: absolute;
99
+ inset: 0;
100
+ border-radius: var(--sk-shimmer-radius, 0.125rem);
101
+ background: linear-gradient(
102
+ 90deg,
103
+ var(--sk-shimmer-color, #e5e7eb) 25%,
104
+ var(--sk-shimmer-highlight, #f3f4f6) 50%,
105
+ var(--sk-shimmer-color, #e5e7eb) 75%
106
+ );
107
+ background-size: 200% 100%;
108
+ animation: sk-shimmer var(--sk-shimmer-duration, 1.5s) ease-in-out infinite;
109
+ transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);
110
+ }
111
+
112
+ /* ── Shared easing ────────────────────────────────────────────────────────── */
113
+
114
+ /* Decelerate: fast start, gentle finish — for elements entering view. */
115
+ /* Standard: balanced ease — for general-purpose transitions. */
116
+ /* Accelerate: gentle start, fast finish — for elements leaving view. */
117
+ :root {
118
+ --sk-ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0);
119
+ --sk-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
120
+ --sk-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
121
+ }
122
+
123
+ /* ── FadeTransition ────────────────────────────────────────────────────────── */
124
+
125
+ .sk-fade {
126
+ --sk-fade-duration: 400ms;
127
+ }
128
+
129
+ .sk-fade-entering {
130
+ animation: sk-emerge var(--sk-fade-duration) var(--sk-ease-decelerate) forwards;
131
+ }
132
+
133
+ .sk-fade-exiting {
134
+ animation: sk-collapse var(--sk-fade-duration) var(--sk-ease-accelerate) forwards;
135
+ }
136
+
137
+ @keyframes sk-emerge {
138
+ from {
139
+ opacity: 0;
140
+ transform: translateY(var(--sk-fade-offset-y, -12px)) scale(var(--sk-fade-offset-scale, 0.98));
141
+ }
142
+ to {
143
+ opacity: 1;
144
+ transform: translateY(0) scale(1);
145
+ }
146
+ }
147
+
148
+ @keyframes sk-collapse {
149
+ from { opacity: 1; transform: scaleY(1); transform-origin: top; }
150
+ to { opacity: 0; transform: scaleY(0); transform-origin: top; }
151
+ }
152
+
153
+ /* ── Loading layers ────────────────────────────────────────────────────────── */
154
+
155
+ /* Shared by shimmer and content layers inside skeleton components.
156
+ Both layers permanently occupy the same grid cell; only opacity
157
+ and interactivity change. CSS transitions handle the crossfade. */
158
+ .sk-loading-layer {
159
+ grid-area: 1 / 1;
160
+ transition: opacity var(--sk-loading-exit-duration, 400ms) var(--sk-ease-decelerate);
161
+ }
162
+
163
+ /* ── Reduced motion ────────────────────────────────────────────────────────── */
164
+
165
+ /* Disables all animations — shimmer, fade, and loading exit.
166
+ Layout changes still happen instantly so functionality is preserved. */
167
+ @media (prefers-reduced-motion: reduce) {
168
+ .sk-fade-entering,
169
+ .sk-fade-exiting,
170
+ .sk-shimmer-line,
171
+ .sk-media-shimmer {
172
+ animation-duration: 0s !important;
173
+ }
174
+ .sk-loading-layer,
175
+ .sk-media-shimmer {
176
+ transition-duration: 0s !important;
177
+ }
178
+ }