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.
- package/README.md +107 -0
- package/package.json +71 -0
- package/src/css.ts +140 -0
- package/src/cx.ts +105 -0
- package/src/dcx.ts +79 -0
- package/src/dynamic.ts +117 -0
- package/src/hash.ts +54 -0
- package/src/index.ts +137 -0
- package/src/inject.ts +86 -0
- package/src/layer.ts +81 -0
- package/src/modifiers/aria.ts +15 -0
- package/src/modifiers/colorScheme.ts +32 -0
- package/src/modifiers/data.ts +6 -0
- package/src/modifiers/direction.ts +5 -0
- package/src/modifiers/group.ts +21 -0
- package/src/modifiers/index.ts +17 -0
- package/src/modifiers/media.ts +11 -0
- package/src/modifiers/peer.ts +24 -0
- package/src/modifiers/pseudo.ts +183 -0
- package/src/modifiers/pseudoElements.ts +26 -0
- package/src/modifiers/responsive.ts +110 -0
- package/src/modifiers/supports.ts +6 -0
- package/src/registry.ts +171 -0
- package/src/rule.ts +202 -0
- package/src/runtime.ts +36 -0
- package/src/theme/animations.ts +11 -0
- package/src/theme/borders.ts +9 -0
- package/src/theme/colors.ts +326 -0
- package/src/theme/createTheme.ts +238 -0
- package/src/theme/filters.ts +20 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/inject-theme.ts +81 -0
- package/src/theme/shadows.ts +8 -0
- package/src/theme/sizes.ts +37 -0
- package/src/theme/spacing.ts +44 -0
- package/src/theme/typography.ts +72 -0
- package/src/types.ts +273 -0
- package/src/utilities/accessibility.ts +33 -0
- package/src/utilities/backgrounds.ts +86 -0
- package/src/utilities/borders.ts +610 -0
- package/src/utilities/colors.ts +127 -0
- package/src/utilities/effects.ts +169 -0
- package/src/utilities/filters.ts +96 -0
- package/src/utilities/index.ts +57 -0
- package/src/utilities/interactivity.ts +253 -0
- package/src/utilities/layout.ts +1149 -0
- package/src/utilities/spacing.ts +681 -0
- package/src/utilities/svg.ts +34 -0
- package/src/utilities/tables.ts +54 -0
- package/src/utilities/transforms.ts +85 -0
- package/src/utilities/transitions.ts +98 -0
- package/src/utilities/typography.ts +380 -0
- 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
|
+
}
|