@pitlane/theme 0.2.0 → 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/CHANGELOG.md +119 -0
- package/README.md +66 -28
- package/dist/default.d.mts +605 -0
- package/dist/default.mjs +584 -0
- package/dist/dtcg.d.mts +127 -0
- package/dist/dtcg.mjs +468 -0
- package/dist/index.d.mts +76 -348
- package/dist/index.mjs +54 -458
- package/dist/schema-CRP607Pg.d.mts +320 -0
- package/dist/schema-JmfNnMzr.mjs +572 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +2 -0
- package/dist/theme-CMaWrQJ5.mjs +237 -0
- package/dist/theme-Ck-4suI4.d.mts +278 -0
- package/package.json +58 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
# @pitlane/theme
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
Authoring moves off W3C DTCG. `createTheme` now takes a schema tree and the
|
|
6
|
+
token tree it describes, and token values are the CSS they become. DTCG stays
|
|
7
|
+
as an interchange format behind `@pitlane/theme/dtcg`.
|
|
8
|
+
|
|
9
|
+
**Breaking.** Every theme written against 0.1.0 or 0.2.0 has to be rewritten.
|
|
10
|
+
There is no compatibility shim, and `createTheme` does not accept a DTCG
|
|
11
|
+
document any more. `fromDTCG` reads one, so an existing document can be moved
|
|
12
|
+
across without being retyped by hand:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createTheme } from "@pitlane/theme";
|
|
16
|
+
import { fromDTCG } from "@pitlane/theme/dtcg";
|
|
17
|
+
|
|
18
|
+
export let { token: t, raw, Theme } = createTheme(fromDTCG(existingDocument));
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
What changes in a hand-written theme:
|
|
22
|
+
|
|
23
|
+
| 0.2.0 | 0.3.0 |
|
|
24
|
+
| -------------------------------------------------- | ---------------------------------------- |
|
|
25
|
+
| `createTheme(document, { modes })` | `createTheme({ schema, tokens, modes })` |
|
|
26
|
+
| `{ $type: "color" }` on a group | `color: s.color()` in the schema tree |
|
|
27
|
+
| `{ $value: "#fff" }` | `"#fff"` |
|
|
28
|
+
| `$value: { value: 2.5, unit: "rem" }` | `"2.5rem"` |
|
|
29
|
+
| `$value: { colorSpace: "oklch", components: […] }` | the `oklch(…)` text |
|
|
30
|
+
| A shadow, border, transition, or gradient object | the CSS shorthand text |
|
|
31
|
+
| `$value: [0.25, 0.1, 0.25, 1]` on `cubicBezier` | the same tuple, under `s.easing()` |
|
|
32
|
+
| A shadow's `color: "{color.line}"` sub-value | the reference inside the shorthand text |
|
|
33
|
+
| `modes: { dark: { … { $value } } }` | `modes: { dark: { tokens: { … } } }` |
|
|
34
|
+
| `"{color.white}"` | `base.color.white` in an `extend` layer |
|
|
35
|
+
|
|
36
|
+
The accessor shape, `raw`, `<Theme />`, `css`, `tva`, `combine`, and `cx` are
|
|
37
|
+
all unchanged.
|
|
38
|
+
|
|
39
|
+
References need the most attention. The `"{color.white}"` string syntax is gone,
|
|
40
|
+
and a reference is a property access on the layer below:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
createTheme({ schema: { color: s.color() }, tokens: { color: { white: "#fff" } } }).extend(
|
|
44
|
+
base => ({
|
|
45
|
+
schema: { surface: s.color() },
|
|
46
|
+
tokens: { surface: { page: base.color.white } },
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The emitted CSS is the same, `--surface-page: var(--color-white)`, so overrides
|
|
52
|
+
still cascade. What changes is that the reference is now checked: its type has
|
|
53
|
+
to match its position, and renaming the target breaks the reference at build
|
|
54
|
+
time instead of emitting a variable nothing declares.
|
|
55
|
+
|
|
56
|
+
Three consequences worth planning for. A semantic tier becomes a separate
|
|
57
|
+
`extend` from the primitives it names. A mode override that references another
|
|
58
|
+
token goes in an `extend` layer too, since that is where an accessor is in
|
|
59
|
+
scope. And a DTCG sub-value alias, such as a shadow whose `color` field was
|
|
60
|
+
`{color.line}`, becomes an interpolation:
|
|
61
|
+
``tokens: { shadow: { card: `0 1px 2px ${base.color.line}` } }``.
|
|
62
|
+
|
|
63
|
+
`fromDTCG` converts a document's aliases for you, including the ones inside a
|
|
64
|
+
composite's sub-values, so an existing document needs no hand-editing.
|
|
65
|
+
|
|
66
|
+
One side effect worth knowing: a layer's declarations follow the ones it
|
|
67
|
+
references, so moving a semantic tier into an `extend` moves its declarations
|
|
68
|
+
later in the `:root` block. Custom properties in one rule resolve independently
|
|
69
|
+
of order, so nothing about the cascade changes. Declaring the namespace as an
|
|
70
|
+
empty group in the base tree reserves its position if the order matters for
|
|
71
|
+
diffing.
|
|
72
|
+
|
|
73
|
+
- `createTheme({ schema, tokens, modes })` replaces
|
|
74
|
+
`createTheme(document, options)`. A leaf is a string, a number, or an array;
|
|
75
|
+
a plain object is a group. No `$value` wrappers, and no reserved token names.
|
|
76
|
+
- `@pitlane/theme/schema` — one factory per token type, built on
|
|
77
|
+
`remix/data-schema` and designed for `import * as s`. `s.color()`,
|
|
78
|
+
`s.dimension()`, `s.duration()`, `s.number()`, `s.easing()`, `s.shadow()`,
|
|
79
|
+
`s.border()`, `s.transition()`, `s.gradient()`, `s.stroke()`,
|
|
80
|
+
`s.font.family()`, and `s.font.weight()` name a token type;
|
|
81
|
+
`s.group(self, children)` types a node and lets its children override it;
|
|
82
|
+
`s.scale()` declares a base whose accessor leaf multiplies; `s.any()`
|
|
83
|
+
declines to type a token at all.
|
|
84
|
+
- Composite types (`shadow`, `border`, `transition`, `gradient`) and
|
|
85
|
+
`cubicBezier` take CSS text. `inset`, `em`, `%`, `clamp()`, `light-dark()`,
|
|
86
|
+
and `color-mix()` all work, none of which DTCG can express. The structured
|
|
87
|
+
object forms remain on the DTCG import path.
|
|
88
|
+
- `theme.extend(patch)` deep-merges a patch and returns a new theme.
|
|
89
|
+
`theme.select(projection)` replaces the theme with a projection of it, which
|
|
90
|
+
may also reshape and rename. Both take a callback that receives the accessor,
|
|
91
|
+
so a layer can reference what it builds on.
|
|
92
|
+
- `<Theme />` carries the init it was compiled from as `$theme`, and
|
|
93
|
+
`createTheme(SomeTheme)` reads it, so a published theme is one import.
|
|
94
|
+
- `@pitlane/theme/default` — `DefaultTheme`, Tailwind v4's primitives with no
|
|
95
|
+
semantic layer. Pair it with `select` to avoid shipping every one of them.
|
|
96
|
+
- `@pitlane/theme/dtcg` — `fromDTCG` reads a conformant 2025.10 document and
|
|
97
|
+
derives its schema from each token's resolved `$type`; `toDTCG` writes one
|
|
98
|
+
out and counts the values the format cannot express.
|
|
99
|
+
- `scale(base)` multiplies any dimension, duration, or number token, keeping
|
|
100
|
+
its brand. `lightDark(light, dark)` writes a `light-dark()` color, and
|
|
101
|
+
composes with token references.
|
|
102
|
+
- A mode declares its own condition. `media` defaults to
|
|
103
|
+
`(prefers-color-scheme: <name>)`; `selector` emits a second block for a
|
|
104
|
+
user-selectable toggle, which outranks the media block on specificity so an
|
|
105
|
+
explicit choice beats the OS preference.
|
|
106
|
+
- A `"{a.b.c}"` string left over from the old format raises `ThemeError` naming
|
|
107
|
+
the `extend` that replaces it. Braces are never valid in a CSS value, so
|
|
108
|
+
passing one through would put `{color.white}` in the stylesheet where a color
|
|
109
|
+
belongs.
|
|
110
|
+
- A reference is a property access, so it is type-checked wherever it appears,
|
|
111
|
+
including interpolated into a composite's CSS text. `select` refuses a
|
|
112
|
+
projection that drops a token something it kept refers to, and a reference
|
|
113
|
+
whose type does not match its position raises `ThemeError`.
|
|
114
|
+
- Bad token values raise `ValidationError` from `remix/data-schema`, one issue
|
|
115
|
+
per bad token with its own path, all reported in one pass. Read `issues`
|
|
116
|
+
rather than `message`. `ThemeError` covers the structural failures: an
|
|
117
|
+
unknown or wrongly-typed reference, a reference to an untyped token, a
|
|
118
|
+
reference cycle, a variable collision, a reserved character in a name, and a
|
|
119
|
+
mode overriding a token that does not exist.
|
|
120
|
+
- `css()`, `tva()`, `combine()`, and `cx()` are unchanged.
|
|
121
|
+
|
|
3
122
|
## 0.2.0
|
|
4
123
|
|
|
5
124
|
- Raised the `remix` peer dependency to `^3.0.0-beta.10` (from
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pitlane/theme
|
|
2
2
|
|
|
3
|
-
Type-safe styling with
|
|
3
|
+
Type-safe styling with design tokens for [Remix 3](https://remix.run). `createTheme` compiles a schema tree and a tree of CSS values into a typed token accessor plus a `<Theme />` component that installs CSS custom properties. The `css`, `tva`, `combine`, and `cx` helpers wrap `remix/ui`'s `css()` mixin and enforce the theme palette at the type level.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,33 +14,40 @@ Requires `remix@^3.0.0-beta.10` as a peer.
|
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
17
|
+
Define the schema beside the tokens it describes. Token values are the CSS they become:
|
|
18
|
+
|
|
17
19
|
```ts
|
|
18
20
|
// app/theme.ts
|
|
19
|
-
import { createTheme } from "@pitlane/theme";
|
|
21
|
+
import { createTheme, lightDark } from "@pitlane/theme";
|
|
22
|
+
import * as s from "@pitlane/theme/schema";
|
|
20
23
|
|
|
21
24
|
export let {
|
|
22
25
|
token: t,
|
|
23
26
|
raw,
|
|
24
27
|
Theme,
|
|
25
|
-
} = createTheme(
|
|
26
|
-
{
|
|
27
|
-
color:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
space: { $type: "dimension", sm: { $value: "8px" }, md: { $value: "16px" } },
|
|
28
|
+
} = createTheme({
|
|
29
|
+
schema: {
|
|
30
|
+
color: s.color(),
|
|
31
|
+
spacing: s.scale(),
|
|
32
|
+
radius: s.dimension(),
|
|
33
|
+
shadow: s.shadow(),
|
|
34
|
+
animate: s.any(),
|
|
34
35
|
},
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
tokens: {
|
|
37
|
+
color: {
|
|
38
|
+
white: "#fff",
|
|
39
|
+
gray: { 50: "#fafafa", 900: "#171717" },
|
|
40
|
+
page: lightDark("#fff", "#171717"),
|
|
38
41
|
},
|
|
42
|
+
spacing: "0.25rem",
|
|
43
|
+
radius: { full: "999px", responsive: "clamp(0.25rem, 2vw, 1rem)" },
|
|
44
|
+
shadow: { card: "0 1px 2px rgb(0 0 0 / 0.07)" },
|
|
45
|
+
animate: { spin: "spin 1s linear infinite" },
|
|
39
46
|
},
|
|
40
|
-
);
|
|
47
|
+
});
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
Render `<Theme />` once near the root. It emits
|
|
50
|
+
Render `<Theme />` once near the root. It emits one `<style data-pitlane-theme>` element with the custom properties:
|
|
44
51
|
|
|
45
52
|
```tsx
|
|
46
53
|
import { Theme } from "./theme.ts";
|
|
@@ -57,7 +64,9 @@ function App() {
|
|
|
57
64
|
}
|
|
58
65
|
```
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
`<Theme />` declares `color-scheme: light dark` on `:root` whenever a token uses `lightDark()`, because `light-dark()` resolves to its light value when `color-scheme` is undeclared. Declare a narrower value yourself to override it.
|
|
68
|
+
|
|
69
|
+
Pass tokens to `css()` inline at each element through the `mix` prop. Token-mapped properties accept only the matching brand:
|
|
61
70
|
|
|
62
71
|
```tsx
|
|
63
72
|
import { css } from "@pitlane/theme";
|
|
@@ -65,24 +74,53 @@ import { t } from "./theme.ts";
|
|
|
65
74
|
|
|
66
75
|
<article
|
|
67
76
|
mix={css({
|
|
68
|
-
color: t.color.
|
|
69
|
-
padding: [t.
|
|
70
|
-
margin: 0,
|
|
71
|
-
// color: "#ff0000", //
|
|
77
|
+
color: t.color.page,
|
|
78
|
+
padding: [t.spacing(2), t.spacing(4)],
|
|
79
|
+
margin: 0,
|
|
80
|
+
// color: "#ff0000", // type error: outside the palette
|
|
72
81
|
"&:hover": { color: t.color.gray[900] },
|
|
73
82
|
})}
|
|
74
83
|
/>;
|
|
75
84
|
```
|
|
76
85
|
|
|
86
|
+
`t.spacing(4)` produces `calc(var(--spacing) * 4)`, and `t.spacing.token` is the unmultiplied `var(--spacing)`. Use module-level `scale(token)` to multiply an ordinary dimension, duration, or number token.
|
|
87
|
+
|
|
88
|
+
## Reference one token from another
|
|
89
|
+
|
|
90
|
+
A reference is a property access on the layer below, so it goes in an `extend`. There is no string syntax for one, which is what lets the compiler check every reference and break the build when a target is renamed:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export let {
|
|
94
|
+
token: t,
|
|
95
|
+
raw,
|
|
96
|
+
Theme,
|
|
97
|
+
} = createTheme({
|
|
98
|
+
schema: { palette: s.color() },
|
|
99
|
+
tokens: { palette: { ink: "#1c1a16", paper: "#f5f1e8" } },
|
|
100
|
+
}).extend(base => ({
|
|
101
|
+
schema: { color: s.color(), shadow: s.shadow() },
|
|
102
|
+
tokens: {
|
|
103
|
+
color: { text: base.palette.ink, surface: base.palette.paper },
|
|
104
|
+
// A composite is CSS text, so a reference goes in by interpolation.
|
|
105
|
+
shadow: { card: `0 1px 2px ${base.palette.ink}` },
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The emitted declarations keep their `var()` indirection, `--color-text: var(--palette-ink)`, so overriding a primitive reaches everything that references it. A mode override that references another token goes in an `extend` layer too, since that is where an accessor is in scope.
|
|
111
|
+
|
|
112
|
+
A layer's declarations follow the ones they reference, which moves them later in the `:root` block. Custom properties in one rule resolve independently of order. Declare the namespace as an empty group in the base tree to reserve its position.
|
|
113
|
+
|
|
77
114
|
## Exports
|
|
78
115
|
|
|
79
|
-
- `createTheme(
|
|
80
|
-
- `
|
|
81
|
-
- `
|
|
82
|
-
- `combine(...fns)`
|
|
83
|
-
- `
|
|
84
|
-
- `ThemeError`
|
|
85
|
-
-
|
|
116
|
+
- `createTheme({ schema, tokens, modes? })` compiles a theme and returns `{ token, raw, Theme, extend, select }`. `token`, conventionally `t`, mirrors the tree with branded `var()` strings. `raw(ref)` resolves a base value. `<Theme />` installs the custom properties.
|
|
117
|
+
- `createTheme(DefaultTheme)` accepts a published theme component and returns a derivable theme. `@pitlane/theme/default` exports `DefaultTheme`, Tailwind v4 primitives without a semantic layer.
|
|
118
|
+
- `css(props)` is `remix/ui`'s `css()` with token-brand enforcement. Call it inline at each `mix` callsite.
|
|
119
|
+
- `tva(config)` creates a cva-style variant resolver. `combine(...fns)` composes tva components. `cx(...)` joins clsx-compatible class values.
|
|
120
|
+
- `lightDark(light, dark)` returns CSS `light-dark()` text. `scale(token)` returns a multiplier for an ordinary dimension, duration, or number token.
|
|
121
|
+
- `ThemeError` reports structural failures: a reference whose type does not match its position, a reference to an untyped token, a variable collision, an undeclared token, a mode overriding an unknown token, and a `"{a.b.c}"` string left over from the pre-0.3.0 format. Invalid values raise `ValidationError` from `remix/data-schema`, whose `issues` array contains the detail.
|
|
122
|
+
- `@pitlane/theme/schema` exports `s.color()`, `s.dimension()`, `s.duration()`, `s.number()`, `s.easing()`, `s.shadow()`, `s.border()`, `s.transition()`, `s.gradient()`, `s.stroke()`, `s.font.family()`, `s.font.weight()`, `s.scale()`, `s.any()`, and `s.group()`.
|
|
123
|
+
- Types include `ThemeInit`, `ThemeMode`, `ThemeResult`, `ThemeComponent`, `ThemeProps`, `TokenTree`, `ScaleFn`, `Tokens`, `TokenValue`, `ThemedCSSProps`, `ThemedCSSMixin`, TVA types, and per-type token brands.
|
|
86
124
|
|
|
87
125
|
## Links
|
|
88
126
|
|