@crab-dev/css 0.1.17 → 0.1.18

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
@@ -78,6 +78,27 @@ const style = assignVars({
78
78
  })
79
79
  ```
80
80
 
81
+ ## Immutable design tokens
82
+
83
+ Use `defineTokens` for nested token structures that are interpolated from another module. Wake
84
+ recognizes only a direct top-level `const` initialized through this imported helper; the argument
85
+ must contain statically evaluable plain objects, arrays, and primitive values.
86
+
87
+ ```ts
88
+ import { css, defineTokens } from '@crab-dev/css'
89
+
90
+ export const tokens = defineTokens({
91
+ color: { accent: 'rebeccapurple' },
92
+ })
93
+
94
+ export const button = css`
95
+ color: ${tokens.color.accent};
96
+ `
97
+ ```
98
+
99
+ The return type is deeply readonly and the runtime value is deeply frozen. Ordinary mutable
100
+ objects do not acquire this cross-module compiler guarantee.
101
+
81
102
  ## Compile-time contract
82
103
 
83
104
  `css`, `keyframes`, and `globalStyle` must be used as direct tagged templates
package/index.cjs CHANGED
@@ -27,6 +27,50 @@ function globalStyle() {
27
27
  return compilerOnly('globalStyle')
28
28
  }
29
29
 
30
+ function defineTokens(value) {
31
+ if (value === null || typeof value !== 'object') {
32
+ throw new TypeError('@crab-dev/css: defineTokens expects a plain object or array.')
33
+ }
34
+
35
+ const pending = [value]
36
+ const seen = new WeakSet()
37
+ while (pending.length > 0) {
38
+ const current = pending.pop()
39
+ if (seen.has(current)) continue
40
+ seen.add(current)
41
+
42
+ const prototype = Object.getPrototypeOf(current)
43
+ const plainObject = prototype === null || Object.getPrototypeOf(prototype) === null
44
+ if (!Array.isArray(current) && !plainObject) {
45
+ throw new TypeError('@crab-dev/css: defineTokens accepts only plain objects and arrays.')
46
+ }
47
+
48
+ const descriptors = Object.getOwnPropertyDescriptors(current)
49
+ for (const key of Reflect.ownKeys(descriptors)) {
50
+ if (typeof key === 'symbol') {
51
+ throw new TypeError('@crab-dev/css: defineTokens does not accept symbol keys.')
52
+ }
53
+ const descriptor = descriptors[key]
54
+ if ('get' in descriptor || 'set' in descriptor) {
55
+ throw new TypeError('@crab-dev/css: defineTokens does not accept accessors.')
56
+ }
57
+ const item = descriptor.value
58
+ if (item === null) {
59
+ continue
60
+ } else if (typeof item === 'object') {
61
+ pending.push(item)
62
+ } else if (
63
+ !['string', 'number', 'boolean', 'undefined'].includes(typeof item) ||
64
+ (typeof item === 'number' && !Number.isFinite(item))
65
+ ) {
66
+ throw new TypeError('@crab-dev/css: defineTokens contains an unsupported value.')
67
+ }
68
+ }
69
+ Object.freeze(current)
70
+ }
71
+ return value
72
+ }
73
+
30
74
  function cx(...values) {
31
75
  const classes = []
32
76
  const pending = values.slice().reverse()
@@ -146,5 +190,6 @@ exports.assignVars = assignVars
146
190
  exports.createVar = createVar
147
191
  exports.css = css
148
192
  exports.cx = cx
193
+ exports.defineTokens = defineTokens
149
194
  exports.globalStyle = globalStyle
150
195
  exports.keyframes = keyframes
package/index.d.ts CHANGED
@@ -40,6 +40,22 @@ export type ClassValue =
40
40
 
41
41
  export type CSSVariableStyles = Record<string, string | number>
42
42
 
43
+ export type TokenPrimitive = string | number | boolean | null | undefined
44
+
45
+ export type TokenValue =
46
+ | TokenPrimitive
47
+ | { readonly [key: string]: TokenValue }
48
+ | readonly TokenValue[]
49
+
50
+ export type DeepReadonlyToken<T> =
51
+ T extends TokenPrimitive
52
+ ? T
53
+ : T extends readonly unknown[]
54
+ ? { readonly [K in keyof T]: DeepReadonlyToken<T[K]> }
55
+ : T extends object
56
+ ? { readonly [K in keyof T]: DeepReadonlyToken<T[K]> }
57
+ : never
58
+
43
59
  /**
44
60
  * Declares a scoped class. Wake replaces this template tag with a ClassName
45
61
  * and extracts its CSS during compilation.
@@ -70,6 +86,9 @@ export declare function globalStyle(
70
86
  ...interpolations: readonly CSSInterpolation[]
71
87
  ): void
72
88
 
89
+ /** Marks a recursively pure token structure as deeply immutable for safe ESM propagation. */
90
+ export declare function defineTokens<const T extends TokenValue>(value: T): DeepReadonlyToken<T>
91
+
73
92
  /** Creates a realm-unique `var(--custom-property)` reference. */
74
93
  export declare function createVar(debugName?: string): CSSVar
75
94
 
package/index.mjs CHANGED
@@ -25,6 +25,50 @@ export function globalStyle() {
25
25
  return compilerOnly('globalStyle')
26
26
  }
27
27
 
28
+ export function defineTokens(value) {
29
+ if (value === null || typeof value !== 'object') {
30
+ throw new TypeError('@crab-dev/css: defineTokens expects a plain object or array.')
31
+ }
32
+
33
+ const pending = [value]
34
+ const seen = new WeakSet()
35
+ while (pending.length > 0) {
36
+ const current = pending.pop()
37
+ if (seen.has(current)) continue
38
+ seen.add(current)
39
+
40
+ const prototype = Object.getPrototypeOf(current)
41
+ const plainObject = prototype === null || Object.getPrototypeOf(prototype) === null
42
+ if (!Array.isArray(current) && !plainObject) {
43
+ throw new TypeError('@crab-dev/css: defineTokens accepts only plain objects and arrays.')
44
+ }
45
+
46
+ const descriptors = Object.getOwnPropertyDescriptors(current)
47
+ for (const key of Reflect.ownKeys(descriptors)) {
48
+ if (typeof key === 'symbol') {
49
+ throw new TypeError('@crab-dev/css: defineTokens does not accept symbol keys.')
50
+ }
51
+ const descriptor = descriptors[key]
52
+ if ('get' in descriptor || 'set' in descriptor) {
53
+ throw new TypeError('@crab-dev/css: defineTokens does not accept accessors.')
54
+ }
55
+ const item = descriptor.value
56
+ if (item === null) {
57
+ continue
58
+ } else if (typeof item === 'object') {
59
+ pending.push(item)
60
+ } else if (
61
+ !['string', 'number', 'boolean', 'undefined'].includes(typeof item) ||
62
+ (typeof item === 'number' && !Number.isFinite(item))
63
+ ) {
64
+ throw new TypeError('@crab-dev/css: defineTokens contains an unsupported value.')
65
+ }
66
+ }
67
+ Object.freeze(current)
68
+ }
69
+ return value
70
+ }
71
+
28
72
  export function cx(...values) {
29
73
  const classes = []
30
74
  const pending = values.slice().reverse()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crab-dev/css",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Type-safe, zero-runtime CSS authoring for Wake",
5
5
  "keywords": [
6
6
  "css",