typewritingclass 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.
Files changed (53) hide show
  1. package/README.md +107 -0
  2. package/package.json +71 -0
  3. package/src/css.ts +140 -0
  4. package/src/cx.ts +105 -0
  5. package/src/dcx.ts +79 -0
  6. package/src/dynamic.ts +117 -0
  7. package/src/hash.ts +54 -0
  8. package/src/index.ts +137 -0
  9. package/src/inject.ts +86 -0
  10. package/src/layer.ts +81 -0
  11. package/src/modifiers/aria.ts +15 -0
  12. package/src/modifiers/colorScheme.ts +32 -0
  13. package/src/modifiers/data.ts +6 -0
  14. package/src/modifiers/direction.ts +5 -0
  15. package/src/modifiers/group.ts +21 -0
  16. package/src/modifiers/index.ts +17 -0
  17. package/src/modifiers/media.ts +11 -0
  18. package/src/modifiers/peer.ts +24 -0
  19. package/src/modifiers/pseudo.ts +183 -0
  20. package/src/modifiers/pseudoElements.ts +26 -0
  21. package/src/modifiers/responsive.ts +110 -0
  22. package/src/modifiers/supports.ts +6 -0
  23. package/src/registry.ts +171 -0
  24. package/src/rule.ts +202 -0
  25. package/src/runtime.ts +36 -0
  26. package/src/theme/animations.ts +11 -0
  27. package/src/theme/borders.ts +9 -0
  28. package/src/theme/colors.ts +326 -0
  29. package/src/theme/createTheme.ts +238 -0
  30. package/src/theme/filters.ts +20 -0
  31. package/src/theme/index.ts +9 -0
  32. package/src/theme/inject-theme.ts +81 -0
  33. package/src/theme/shadows.ts +8 -0
  34. package/src/theme/sizes.ts +37 -0
  35. package/src/theme/spacing.ts +44 -0
  36. package/src/theme/typography.ts +72 -0
  37. package/src/types.ts +273 -0
  38. package/src/utilities/accessibility.ts +33 -0
  39. package/src/utilities/backgrounds.ts +86 -0
  40. package/src/utilities/borders.ts +610 -0
  41. package/src/utilities/colors.ts +127 -0
  42. package/src/utilities/effects.ts +169 -0
  43. package/src/utilities/filters.ts +96 -0
  44. package/src/utilities/index.ts +57 -0
  45. package/src/utilities/interactivity.ts +253 -0
  46. package/src/utilities/layout.ts +1149 -0
  47. package/src/utilities/spacing.ts +681 -0
  48. package/src/utilities/svg.ts +34 -0
  49. package/src/utilities/tables.ts +54 -0
  50. package/src/utilities/transforms.ts +85 -0
  51. package/src/utilities/transitions.ts +98 -0
  52. package/src/utilities/typography.ts +380 -0
  53. package/src/when.ts +63 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Typewriting Class
2
+
3
+ Core library for the Typewriting Class CSS-in-TS framework. Provides utility functions, modifiers, theme tokens, and the composition API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add typewritingclass
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { cx, p, bg, textColor, rounded, flex, gap, when } from 'typewritingclass'
15
+ import { hover, md, dark } from 'typewritingclass'
16
+ import { blue, white, slate } from 'typewritingclass/theme/colors'
17
+
18
+ const card = cx(
19
+ p(6), bg(white), rounded('lg'),
20
+ flex(), gap(4),
21
+ when(hover)(bg(blue[50])),
22
+ when(md)(p(8)),
23
+ when(dark)(bg(slate[800])),
24
+ )
25
+ ```
26
+
27
+ ## API
28
+
29
+ ### Composition
30
+
31
+ - **`cx(...rules)`** — compose utilities into a single class name
32
+ - **`when(...modifiers)(...rules)`** — apply styles conditionally
33
+
34
+ ### Utilities
35
+
36
+ | Category | Functions |
37
+ |---|---|
38
+ | Spacing | `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl`, `m`, `mx`, `my`, `mt`, `mr`, `mb`, `ml`, `gap`, `gapX`, `gapY` |
39
+ | Colors | `bg`, `textColor`, `borderColor` |
40
+ | Typography | `text`, `font`, `tracking`, `leading`, `textAlign` |
41
+ | Layout | `flex`, `flexCol`, `flexRow`, `grid`, `gridCols`, `gridRows`, `display`, `items`, `justify`, `self` |
42
+ | Sizing | `w`, `h`, `size`, `minW`, `minH`, `maxW`, `maxH` |
43
+ | Position | `relative`, `absolute`, `fixed`, `sticky`, `top`, `right`, `bottom`, `left`, `inset`, `z` |
44
+ | Borders | `rounded`, `roundedT`, `roundedB`, `roundedL`, `roundedR`, `border`, `borderT`, `borderR`, `borderB`, `borderL`, `ring` |
45
+ | Effects | `shadow`, `opacity`, `backdrop` |
46
+ | Interactivity | `cursor`, `select`, `pointerEvents` |
47
+ | Overflow | `overflow`, `overflowX`, `overflowY` |
48
+
49
+ ### Modifiers
50
+
51
+ | Type | Modifiers |
52
+ |---|---|
53
+ | Pseudo-classes | `hover`, `focus`, `active`, `disabled`, `focusVisible`, `focusWithin`, `firstChild`, `lastChild` |
54
+ | Responsive | `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `_2xl` (1536px) |
55
+ | Color scheme | `dark` |
56
+
57
+ ### Theme tokens
58
+
59
+ ```ts
60
+ import { blue, slate } from 'typewritingclass/theme/colors'
61
+ import { bold, lg } from 'typewritingclass/theme/typography'
62
+ import { md } from 'typewritingclass/theme/shadows'
63
+ import { lg as lgRadius } from 'typewritingclass/theme/borders'
64
+ ```
65
+
66
+ ### Custom themes
67
+
68
+ ```ts
69
+ import { createTheme } from 'typewritingclass/theme/createTheme'
70
+
71
+ const theme = createTheme({
72
+ colors: { brand: { 500: '#6366f1' } },
73
+ })
74
+ ```
75
+
76
+ ### Dynamic values
77
+
78
+ ```ts
79
+ import { dynamic } from 'typewritingclass/runtime'
80
+
81
+ // Returns { className, style } for runtime CSS custom properties
82
+ const result = dynamic(bg(userColor))
83
+ ```
84
+
85
+ ### Raw CSS escape hatch
86
+
87
+ ```ts
88
+ import { css } from 'typewritingclass'
89
+
90
+ cx(p(4), css({ transition: 'all 200ms ease' }))
91
+ ```
92
+
93
+ ## Exports
94
+
95
+ | Path | Contents |
96
+ |---|---|
97
+ | `typewritingclass` | Core API (`cx`, `when`, all utilities and modifiers) |
98
+ | `typewritingclass/theme` | All theme token exports |
99
+ | `typewritingclass/theme/colors` | Color scales |
100
+ | `typewritingclass/theme/typography` | Font sizes, weights, line heights |
101
+ | `typewritingclass/theme/shadows` | Shadow presets |
102
+ | `typewritingclass/theme/borders` | Border radius tokens |
103
+ | `typewritingclass/theme/spacing` | Spacing scale |
104
+ | `typewritingclass/theme/sizes` | Named sizes |
105
+ | `typewritingclass/theme/createTheme` | `createTheme()` |
106
+ | `typewritingclass/inject` | `injectTheme()`, `setTheme()` |
107
+ | `typewritingclass/runtime` | `dynamic()`, `isDynamic()` |
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "typewritingclass",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "default": "./src/index.ts"
9
+ },
10
+ "./theme/colors": {
11
+ "types": "./src/theme/colors.ts",
12
+ "default": "./src/theme/colors.ts"
13
+ },
14
+ "./theme/typography": {
15
+ "types": "./src/theme/typography.ts",
16
+ "default": "./src/theme/typography.ts"
17
+ },
18
+ "./theme/sizes": {
19
+ "types": "./src/theme/sizes.ts",
20
+ "default": "./src/theme/sizes.ts"
21
+ },
22
+ "./theme/shadows": {
23
+ "types": "./src/theme/shadows.ts",
24
+ "default": "./src/theme/shadows.ts"
25
+ },
26
+ "./theme/borders": {
27
+ "types": "./src/theme/borders.ts",
28
+ "default": "./src/theme/borders.ts"
29
+ },
30
+ "./theme/createTheme": {
31
+ "types": "./src/theme/createTheme.ts",
32
+ "default": "./src/theme/createTheme.ts"
33
+ },
34
+ "./theme/animations": {
35
+ "types": "./src/theme/animations.ts",
36
+ "default": "./src/theme/animations.ts"
37
+ },
38
+ "./theme/filters": {
39
+ "types": "./src/theme/filters.ts",
40
+ "default": "./src/theme/filters.ts"
41
+ },
42
+ "./theme": {
43
+ "types": "./src/theme/index.ts",
44
+ "default": "./src/theme/index.ts"
45
+ },
46
+ "./inject": {
47
+ "types": "./src/inject.ts",
48
+ "default": "./src/inject.ts"
49
+ },
50
+ "./runtime": {
51
+ "types": "./src/runtime.ts",
52
+ "default": "./src/runtime.ts"
53
+ },
54
+ "./rule": {
55
+ "types": "./src/rule.ts",
56
+ "default": "./src/rule.ts"
57
+ }
58
+ },
59
+ "files": [
60
+ "src"
61
+ ],
62
+ "devDependencies": {
63
+ "vitest": "^3.0.4",
64
+ "typescript": "^5.7.3"
65
+ },
66
+ "scripts": {
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
69
+ "bench": "tsx benchmarks/bench.ts"
70
+ }
71
+ }
package/src/css.ts ADDED
@@ -0,0 +1,140 @@
1
+ import type { StyleRule } from './types.ts'
2
+ import { createRule, createDynamicRule } from './rule.ts'
3
+ import { isDynamic } from './dynamic.ts'
4
+ import type { DynamicValue } from './dynamic.ts'
5
+
6
+ /**
7
+ * Creates a {@link StyleRule} from a plain object of CSS declarations.
8
+ *
9
+ * Use this overload when you want to write raw CSS property-value pairs
10
+ * without reaching for a specific utility function.
11
+ *
12
+ * @param declarations - A record mapping CSS property names to their values.
13
+ * @returns A {@link StyleRule} containing the given declarations.
14
+ *
15
+ * @example Object of declarations
16
+ * ```ts
17
+ * import { cx, css } from 'typewritingclass'
18
+ *
19
+ * cx(css({ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '1rem' }))
20
+ * // CSS: display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
21
+ * ```
22
+ */
23
+ export function css(declarations: Record<string, string>): StyleRule
24
+ /**
25
+ * Creates a {@link StyleRule} from a tagged template literal of CSS declarations.
26
+ *
27
+ * Interpolated values can be plain strings, numbers, or {@link DynamicValue}
28
+ * instances. Dynamic values are replaced with `var(--twc-dN)` references in
29
+ * the generated CSS and must be applied to the element via inline styles
30
+ * (see {@link dcx}).
31
+ *
32
+ * @param strings - The static portions of the template literal (provided automatically).
33
+ * @param values - Interpolated expressions -- strings, numbers, or {@link DynamicValue}s.
34
+ * @returns A {@link StyleRule} with parsed declarations (and `dynamicBindings`
35
+ * if any interpolated value was created with {@link dynamic}).
36
+ *
37
+ * @example Tagged template with static values
38
+ * ```ts
39
+ * import { cx, css } from 'typewritingclass'
40
+ *
41
+ * cx(css`
42
+ * display: flex;
43
+ * align-items: center;
44
+ * gap: 0.5rem;
45
+ * `)
46
+ * // CSS: display: flex; align-items: center; gap: 0.5rem;
47
+ * ```
48
+ *
49
+ * @example Tagged template with interpolated values
50
+ * ```ts
51
+ * import { cx, css } from 'typewritingclass'
52
+ * import { blue } from 'typewritingclass/theme/colors'
53
+ *
54
+ * const size = '2rem'
55
+ * cx(css`
56
+ * width: ${size};
57
+ * height: ${size};
58
+ * background-color: ${blue[500]};
59
+ * `)
60
+ * // CSS: width: 2rem; height: 2rem; background-color: #3b82f6;
61
+ * ```
62
+ *
63
+ * @example Tagged template with dynamic values
64
+ * ```ts
65
+ * import { dcx, css, dynamic } from 'typewritingclass'
66
+ *
67
+ * const color = dynamic('#e11d48')
68
+ * const { className, style } = dcx(css`
69
+ * background-color: ${color};
70
+ * padding: 1rem;
71
+ * `)
72
+ * // className => "_a1b2c"
73
+ * // style => { '--twc-d0': '#e11d48' }
74
+ * // CSS: ._a1b2c { background-color: var(--twc-d0); padding: 1rem; }
75
+ * ```
76
+ */
77
+ export function css(strings: TemplateStringsArray, ...values: (string | number | DynamicValue)[]): StyleRule
78
+ /**
79
+ * Creates a {@link StyleRule} from either a declarations object or a tagged
80
+ * template literal.
81
+ *
82
+ * This is the implementation signature that dispatches to the appropriate
83
+ * overload. See the individual overload signatures for detailed docs and
84
+ * examples.
85
+ *
86
+ * @param first - Either a `Record<string, string>` (object overload) or a
87
+ * `TemplateStringsArray` (tagged template overload).
88
+ * @param values - Interpolated template values (only used in the tagged
89
+ * template overload).
90
+ * @returns A {@link StyleRule} with the parsed CSS declarations.
91
+ */
92
+ export function css(
93
+ first: Record<string, string> | TemplateStringsArray,
94
+ ...values: (string | number | DynamicValue)[]
95
+ ): StyleRule {
96
+ // Object overload
97
+ if (!Array.isArray(first) && !('raw' in first)) {
98
+ return createRule(first as Record<string, string>)
99
+ }
100
+
101
+ // Tagged template literal overload
102
+ const strings = first as TemplateStringsArray
103
+ const declarations: Record<string, string> = {}
104
+ let dynamicBindings: Record<string, string> | undefined
105
+
106
+ // Reconstruct the template, collecting dynamic values
107
+ let raw = ''
108
+ for (let i = 0; i < strings.length; i++) {
109
+ raw += strings[i]
110
+ if (i < values.length) {
111
+ const val = values[i]
112
+ if (isDynamic(val)) {
113
+ if (!dynamicBindings) dynamicBindings = {}
114
+ dynamicBindings[val.__id] = String(val.__value)
115
+ raw += `var(${val.__id})`
116
+ } else {
117
+ raw += String(val)
118
+ }
119
+ }
120
+ }
121
+
122
+ // Parse "prop: value;" pairs from the raw string
123
+ const lines = raw.split(';')
124
+ for (const line of lines) {
125
+ const trimmed = line.trim()
126
+ if (!trimmed) continue
127
+ const colonIdx = trimmed.indexOf(':')
128
+ if (colonIdx === -1) continue
129
+ const prop = trimmed.slice(0, colonIdx).trim()
130
+ const value = trimmed.slice(colonIdx + 1).trim()
131
+ if (prop && value) {
132
+ declarations[prop] = value
133
+ }
134
+ }
135
+
136
+ if (dynamicBindings) {
137
+ return createDynamicRule(declarations, dynamicBindings)
138
+ }
139
+ return createRule(declarations)
140
+ }
package/src/cx.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type { StyleRule } from './types.ts'
2
+ import { generateHash } from './hash.ts'
3
+ import { register } from './registry.ts'
4
+ import { nextLayer, _resetLayer } from './layer.ts'
5
+
6
+ /**
7
+ * Composes style rules and string class names into a single CSS class string.
8
+ *
9
+ * Each {@link StyleRule} is registered in the global stylesheet and assigned a
10
+ * unique, deterministic class name. Later arguments override earlier ones when
11
+ * CSS properties conflict -- order is your specificity.
12
+ *
13
+ * Plain strings are passed through unchanged, so you can mix generated rules
14
+ * with external or hand-written class names.
15
+ *
16
+ * @param args - Style rules from utility functions, or plain class name strings.
17
+ * @returns A space-separated class string ready for `className` or `class`.
18
+ *
19
+ * @example Basic composition
20
+ * ```ts
21
+ * import { cx, p, bg, rounded } from 'typewritingclass'
22
+ * import { blue } from 'typewritingclass/theme/colors'
23
+ * import { lg } from 'typewritingclass/theme/borders'
24
+ *
25
+ * const className = cx(p(4), bg(blue[500]), rounded(lg))
26
+ * // => "_a1b2c _d3e4f _g5h6i"
27
+ * // CSS: padding: 1rem; background-color: #3b82f6; border-radius: 0.5rem;
28
+ * ```
29
+ *
30
+ * @example Override earlier rules
31
+ * ```ts
32
+ * import { cx, p } from 'typewritingclass'
33
+ *
34
+ * cx(p(4), p(2))
35
+ * // Only p(2) applies -- later rules are placed on a higher layer
36
+ * // CSS: padding: 0.5rem;
37
+ * ```
38
+ *
39
+ * @example Mix string classes with rules
40
+ * ```ts
41
+ * import { cx, p, bg } from 'typewritingclass'
42
+ * import { white } from 'typewritingclass/theme/colors'
43
+ *
44
+ * cx('my-component', p(4), bg(white))
45
+ * // => "my-component _a1b2c _d3e4f"
46
+ * ```
47
+ *
48
+ * @example With modifiers
49
+ * ```ts
50
+ * import { cx, p, bg, when, hover, md } from 'typewritingclass'
51
+ * import { blue } from 'typewritingclass/theme/colors'
52
+ *
53
+ * cx(
54
+ * p(4),
55
+ * when(hover)(bg(blue[600])),
56
+ * when(md)(p(8)),
57
+ * )
58
+ * // CSS:
59
+ * // .cls1 { padding: 1rem; }
60
+ * // .cls2:hover { background-color: #2563eb; }
61
+ * // @media (min-width: 768px) { .cls3 { padding: 2rem; } }
62
+ * ```
63
+ */
64
+ export function cx(...args: (StyleRule | string)[]): string {
65
+ if (process.env.NODE_ENV !== 'production') {
66
+ warnConflicts(args)
67
+ }
68
+ return args
69
+ .map((arg) => {
70
+ if (typeof arg === 'string') return arg
71
+ const layerNum = (arg as any)._layer ?? nextLayer()
72
+ const className = generateHash(arg, layerNum)
73
+ register(className, arg, layerNum)
74
+ return className
75
+ })
76
+ .join(' ')
77
+ }
78
+
79
+ /**
80
+ * In development mode, warns when multiple StyleRules in a single cx() call
81
+ * declare the same CSS property without being an obvious intentional override.
82
+ *
83
+ * This helps catch accidental conflicts like `cx(p(4), p(8))` where the user
84
+ * may have meant to use only one.
85
+ *
86
+ * @internal
87
+ */
88
+ function warnConflicts(args: (StyleRule | string)[]): void {
89
+ const seen = new Map<string, number>()
90
+ for (let i = 0; i < args.length; i++) {
91
+ const arg = args[i]
92
+ if (typeof arg === 'string') continue
93
+ for (const prop of Object.keys(arg.declarations)) {
94
+ if (seen.has(prop)) {
95
+ console.warn(
96
+ `[typewritingclass] cx() conflict: "${prop}" is set by arguments at index ${seen.get(prop)} and ${i}. ` +
97
+ `The later value will override. If intentional, this warning can be ignored.`,
98
+ )
99
+ }
100
+ seen.set(prop, i)
101
+ }
102
+ }
103
+ }
104
+
105
+ export { _resetLayer }
package/src/dcx.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type { StyleRule, DynamicResult } from './types.ts'
2
+ import { generateHash } from './hash.ts'
3
+ import { register } from './registry.ts'
4
+ import { nextLayer } from './layer.ts'
5
+
6
+ /**
7
+ * Composes style rules into a class string **and** an inline style object,
8
+ * supporting runtime-dynamic CSS values.
9
+ *
10
+ * Works exactly like {@link cx} for static rules, but also collects
11
+ * `dynamicBindings` from any rule that references a {@link DynamicValue}.
12
+ * The returned `style` object maps CSS custom properties to their current
13
+ * values and must be spread onto the element's `style` attribute so the
14
+ * generated `var()` references resolve correctly.
15
+ *
16
+ * Use `dcx` instead of `cx` whenever at least one of your style rules was
17
+ * built with {@link dynamic}.
18
+ *
19
+ * @param args - Style rules from utility functions, or plain class name strings.
20
+ * @returns A {@link DynamicResult} with `className` (space-separated class string)
21
+ * and `style` (CSS custom property assignments for inline styles).
22
+ *
23
+ * @example Dynamic background color
24
+ * ```ts
25
+ * import { dcx, bg, p, dynamic } from 'typewritingclass'
26
+ *
27
+ * const userColor = dynamic('#e11d48')
28
+ * const { className, style } = dcx(p(4), bg(userColor))
29
+ * // className => "_a1b2c _d3e4f"
30
+ * // style => { '--twc-d0': '#e11d48' }
31
+ *
32
+ * // In React JSX:
33
+ * // <div className={className} style={style} />
34
+ * // CSS: ._d3e4f { background-color: var(--twc-d0); }
35
+ * // Inline: style="--twc-d0: #e11d48;"
36
+ * ```
37
+ *
38
+ * @example Mixing static and dynamic rules
39
+ * ```ts
40
+ * import { dcx, p, bg, rounded, dynamic } from 'typewritingclass'
41
+ * import { blue } from 'typewritingclass/theme/colors'
42
+ *
43
+ * const radius = dynamic('12px')
44
+ * const { className, style } = dcx(p(4), bg(blue[500]), rounded(radius))
45
+ * // className => "_a1b2c _d3e4f _g5h6i"
46
+ * // style => { '--twc-d0': '12px' }
47
+ * // CSS: ._g5h6i { border-radius: var(--twc-d0); }
48
+ * ```
49
+ *
50
+ * @example No dynamic values -- style is an empty object
51
+ * ```ts
52
+ * import { dcx, p } from 'typewritingclass'
53
+ *
54
+ * const { className, style } = dcx(p(4))
55
+ * // className => "_a1b2c"
56
+ * // style => {}
57
+ * ```
58
+ */
59
+ export function dcx(...args: (StyleRule | string)[]): DynamicResult {
60
+ const classNames: string[] = []
61
+ const style: Record<string, string> = {}
62
+
63
+ for (const arg of args) {
64
+ if (typeof arg === 'string') {
65
+ classNames.push(arg)
66
+ continue
67
+ }
68
+ const layerNum = (arg as any)._layer ?? nextLayer()
69
+ const className = generateHash(arg, layerNum)
70
+ register(className, arg, layerNum)
71
+ classNames.push(className)
72
+
73
+ if (arg.dynamicBindings) {
74
+ Object.assign(style, arg.dynamicBindings)
75
+ }
76
+ }
77
+
78
+ return { className: classNames.join(' '), style }
79
+ }
package/src/dynamic.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A wrapper around a runtime-changeable CSS value.
3
+ *
4
+ * Instead of baking the value directly into the generated CSS, a
5
+ * `DynamicValue` is replaced with a CSS custom property reference
6
+ * (`var(--twc-dN)`). The actual value is applied at runtime through an
7
+ * inline `style` attribute, allowing it to change without regenerating
8
+ * the stylesheet.
9
+ *
10
+ * Create instances with the {@link dynamic} factory -- do not construct
11
+ * this interface manually.
12
+ *
13
+ * @typeParam T - The underlying value type, constrained to `string | number`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { dynamic, bg, dcx } from 'typewritingclass'
18
+ *
19
+ * const color = dynamic('#e11d48')
20
+ * // color._tag => 'DynamicValue'
21
+ * // color.__value => '#e11d48'
22
+ * // color.__id => '--twc-d0'
23
+ *
24
+ * const { className, style } = dcx(bg(color))
25
+ * // Generated CSS uses var(--twc-d0) instead of the literal color.
26
+ * // style maps '--twc-d0' to '#e11d48' for the inline style attribute.
27
+ * ```
28
+ */
29
+ export interface DynamicValue<T extends string | number = string | number> {
30
+ /** Discriminant tag for runtime type checking. Always `'DynamicValue'`. */
31
+ _tag: 'DynamicValue'
32
+ /** The current runtime value (e.g. `'#e11d48'` or `16`). */
33
+ __value: T
34
+ /** The generated CSS custom property name (e.g. `'--twc-d0'`). */
35
+ __id: string
36
+ }
37
+
38
+ let counter = 0
39
+
40
+ /**
41
+ * Wraps a value so it becomes a runtime-dynamic CSS custom property.
42
+ *
43
+ * The returned {@link DynamicValue} can be passed to any utility that
44
+ * accepts `DynamicValue` (e.g. `bg`, `p`, `textColor`, `rounded`). When
45
+ * composed via {@link dcx}, the value is not inlined into the CSS rule;
46
+ * instead a `var(--twc-dN)` reference is emitted, and the concrete value
47
+ * is placed in the `style` object so it can be changed at runtime without
48
+ * touching the stylesheet.
49
+ *
50
+ * Each call to `dynamic` allocates a new, globally unique custom property
51
+ * name (`--twc-d0`, `--twc-d1`, ...).
52
+ *
53
+ * @typeParam T - Inferred from the provided value; constrained to `string | number`.
54
+ * @param value - The initial CSS value (e.g. a color hex string, a pixel value, etc.).
55
+ * @returns A {@link DynamicValue} wrapping the given value with a unique CSS
56
+ * custom property identifier.
57
+ *
58
+ * @example Dynamic background color in React
59
+ * ```ts
60
+ * import { dcx, bg, p, dynamic } from 'typewritingclass'
61
+ *
62
+ * function Banner({ color }: { color: string }) {
63
+ * const { className, style } = dcx(p(4), bg(dynamic(color)))
64
+ * return <div className={className} style={style} />
65
+ * }
66
+ * // CSS: ._xyz { background-color: var(--twc-d0); padding: 1rem; }
67
+ * // Inline style: --twc-d0: <whatever `color` is at render time>
68
+ * ```
69
+ *
70
+ * @example Dynamic spacing
71
+ * ```ts
72
+ * import { dcx, p, dynamic } from 'typewritingclass'
73
+ *
74
+ * const spacing = dynamic('2.5rem')
75
+ * const { className, style } = dcx(p(spacing))
76
+ * // className => "_a1b2c"
77
+ * // style => { '--twc-d0': '2.5rem' }
78
+ * // CSS: ._a1b2c { padding: var(--twc-d0); }
79
+ * ```
80
+ */
81
+ export function dynamic<T extends string | number>(value: T): DynamicValue<T> {
82
+ return { _tag: 'DynamicValue', __value: value, __id: `--twc-d${counter++}` }
83
+ }
84
+
85
+ /**
86
+ * Type-guard that checks whether an unknown value is a {@link DynamicValue}.
87
+ *
88
+ * Useful inside utilities and the `css` tagged-template implementation to
89
+ * decide whether to emit a `var()` reference or inline the value directly.
90
+ *
91
+ * @param v - Any value to test.
92
+ * @returns `true` if `v` is a `DynamicValue`, narrowing its type accordingly.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { dynamic, isDynamic } from 'typewritingclass'
97
+ *
98
+ * const val = dynamic('#ff0000')
99
+ * isDynamic(val) // => true
100
+ * isDynamic('#ff0000') // => false
101
+ * isDynamic(42) // => false
102
+ * isDynamic(null) // => false
103
+ * ```
104
+ */
105
+ export function isDynamic(v: unknown): v is DynamicValue {
106
+ return (
107
+ typeof v === 'object' &&
108
+ v !== null &&
109
+ '_tag' in v &&
110
+ (v as DynamicValue)._tag === 'DynamicValue'
111
+ )
112
+ }
113
+
114
+ /** @internal — exposed for testing only */
115
+ export function _resetDynamicCounter(): void {
116
+ counter = 0
117
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { StyleRule } from './types.ts'
2
+
3
+ /**
4
+ * Computes a DJB2 hash of the given string.
5
+ *
6
+ * DJB2 is a fast, non-cryptographic hash function created by Daniel J. Bernstein.
7
+ * It produces a 32-bit unsigned integer from an arbitrary string input.
8
+ *
9
+ * @internal
10
+ * @param str - The string to hash.
11
+ * @returns A 32-bit unsigned integer hash value.
12
+ *
13
+ * @see {@link https://en.wikipedia.org/wiki/Daniel_J._Bernstein | DJB2 hash algorithm}
14
+ */
15
+ function djb2(str: string): number {
16
+ let hash = 5381
17
+ for (let i = 0; i < str.length; i++) {
18
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0
19
+ }
20
+ return hash >>> 0
21
+ }
22
+
23
+ /**
24
+ * Generates a unique, deterministic class name for a style rule at a given layer.
25
+ *
26
+ * The hash is computed from the serialised declarations, selectors, media queries,
27
+ * and layer number. Identical inputs always produce the same class name, enabling
28
+ * deduplication in the {@link register | registry}.
29
+ *
30
+ * The returned string is prefixed with `_` so it is a valid CSS class name
31
+ * (class names must not start with a digit), followed by the base-36 encoded
32
+ * DJB2 hash.
33
+ *
34
+ * @internal
35
+ * @param rule - The {@link StyleRule} to hash.
36
+ * @param layer - The layer ordering number assigned to this rule.
37
+ * @returns A deterministic class name string (e.g., `'_1a2b3c'`).
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const rule = createRule({ padding: '1rem' })
42
+ * generateHash(rule, 0) // '_h7k2m' (deterministic for the same input)
43
+ * generateHash(rule, 0) // '_h7k2m' (same input = same hash)
44
+ * ```
45
+ */
46
+ export function generateHash(rule: StyleRule, layer: number): string {
47
+ const input =
48
+ JSON.stringify(rule.declarations) +
49
+ JSON.stringify(rule.selectors) +
50
+ JSON.stringify(rule.mediaQueries) +
51
+ String(layer) +
52
+ (rule.selectorTemplate ?? '')
53
+ return '_' + djb2(input).toString(36)
54
+ }