@primitiv-ui/tokens 0.1.28 → 0.1.30

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
@@ -58,7 +58,7 @@ and `dist/index.html` (UI). Stays running and rebuilds on change.
58
58
  ### 4. Load the plugin into Figma desktop (one-time per machine)
59
59
 
60
60
  In Figma desktop: **Plugins → Development → Import plugin from
61
- manifest…** and select
61
+ manifest...** and select
62
62
  `apps/primitiv-sync-figma-plugin/manifest.json`. After this Figma
63
63
  remembers it under **Plugins → Development → Primitiv Sync**.
64
64
 
@@ -110,7 +110,7 @@ backup "real".
110
110
  ## Conventions
111
111
 
112
112
  - Slash-separated Figma variable names (`font-family/heading`) become
113
- nested DTCG groups (`{ "font-family": { "heading": {…} } }`).
113
+ nested DTCG groups (`{ "font-family": { "heading": {...} } }`).
114
114
  - Each token has `$type` and `$value`. Aliases (next cycle) will use
115
115
  DTCG's `{group.sub.name}` reference string.
116
116
  - Colours emit as hex: `#rrggbb` opaque, `#rrggbbaa` translucent.
@@ -119,3 +119,31 @@ backup "real".
119
119
  key in that file (e.g. `palette.json` has `light` and `dark` keys).
120
120
  - Single-mode collections (`Primitives`, `Interaction`) are read from
121
121
  `defaultModeId` and emitted flat.
122
+
123
+ ## Dark mode: this repo and Figma encode it *oppositely*
124
+
125
+ Both are correct. They just put the inversion in different places, and knowing
126
+ which is which is the difference between reading a real bug and inventing one.
127
+
128
+ | | Where the flip lives | `content/primary` |
129
+ |---|---|---|
130
+ | **This repo** (`intent.json` + `palette.json`) | the **palette** flips per theme (`color.neutral.900` is `#121418` under `light`, `#e5ecf6` under `dark`) | `{color.neutral.900}` in **both** blocks |
131
+ | **Figma** | the **semantic alias** flips; `Primitives / Palette` stays pinned to **Light** on dark frames | `neutral/900` in Light, `neutral/50` in Dark |
132
+
133
+ So in `intent.json` a semantic token usually names the **same ramp step in both
134
+ blocks** and lets the inverted dark palette do the work; in Figma the palette is
135
+ held still and the alias moves. Emitted CSS follows the repo model — the
136
+ `[data-theme="dark"]` block re-points `--primitiv-color-neutral-*` itself, so
137
+ every semantic token above it can keep one step.
138
+
139
+ > **Don't diff the two by ramp index.** Doing that reports ~24 bogus mismatches
140
+ > across nearly all of Dark mode (hit 2026-07-28 during the RFC 0022/0023 Figma
141
+ > cross-check). Resolve each side through its own model first: **13 of 23** dark
142
+ > tokens are then byte-identical, and the remaining 10 differ by only 3–9/255
143
+ > because harmoni *generates* the dark ramp instead of literally reversing it, so
144
+ > only the endpoints (`50`↔`900`) mirror exactly. Reviewed and accepted as-is —
145
+ > those interior dark values aren't expressible as a light-palette alias, so
146
+ > there is nothing better for Figma to point at, and retargeting its dark aliases
147
+ > to this repo's steps would resolve them through Palette/Light and turn dark
148
+ > greys pale. The Figma side of this is in the `figma-variable-architecture`
149
+ > skill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/tokens",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "The DTCG-conformant design-token layer for the Primitiv ecosystem, plus helpers that transform Figma variable exports into DTCG JSON.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,10 @@
1
+ {
2
+ "breakpoint": {
3
+ "xs": { "$type": "number", "$value": 360 },
4
+ "sm": { "$type": "number", "$value": 640 },
5
+ "md": { "$type": "number", "$value": 768 },
6
+ "lg": { "$type": "number", "$value": 1024 },
7
+ "xl": { "$type": "number", "$value": 1280 },
8
+ "2xl": { "$type": "number", "$value": 1536 }
9
+ }
10
+ }
@@ -0,0 +1,64 @@
1
+ import context from './context.json'
2
+ import primitives from './primitives.json'
3
+
4
+ /*
5
+ * `code.*` is the mono typographic family — the one every colour value, hex,
6
+ * ratio and percentage in Harmoni is set in.
7
+ *
8
+ * Why this needs a test: the family was authored size-first, so it shipped with
9
+ * `font-size` and `line-height` and nothing else. That is invisible in the JSON
10
+ * (a group with two members looks intentional) but it is not a usable family —
11
+ * a Figma text style binds four members, so `code.*` could not be expressed as
12
+ * a text style at all, and `font-family.mono` sat in the primitives with
13
+ * nothing consuming it. This asserts the family is complete *and* that it is
14
+ * the mono one, which is the whole reason it exists apart from `body`.
15
+ */
16
+
17
+ type Mode = 'dense' | 'compact' | 'comfortable' | 'spacious'
18
+ type DtcgNode = { $type?: string; $value?: unknown; [key: string]: unknown }
19
+
20
+ const MODES: Mode[] = ['dense', 'compact', 'comfortable', 'spacious']
21
+ const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
22
+
23
+ /** The members a Figma text style binds, plus the weight the CSS layer needs. */
24
+ const MEMBERS = {
25
+ 'font-family': 'string',
26
+ 'font-weight': 'number',
27
+ 'font-size': 'number',
28
+ 'line-height': 'number',
29
+ 'font-style': 'string',
30
+ } as const
31
+
32
+ function codeSize(mode: Mode, size: string): DtcgNode {
33
+ const code = (context as Record<Mode, DtcgNode>)[mode].code as DtcgNode
34
+ return code[size] as DtcgNode
35
+ }
36
+
37
+ /** Resolve a DTCG alias (`{font-family.mono}`) against the primitive tree. */
38
+ function resolvePrimitive(ref: string): unknown {
39
+ const path = ref.slice(1, -1).split('.')
40
+ let node: DtcgNode = primitives as DtcgNode
41
+ for (const key of path) node = node[key] as DtcgNode
42
+ return node.$value
43
+ }
44
+
45
+ describe('code typography', () => {
46
+ for (const mode of MODES) {
47
+ for (const size of SIZES) {
48
+ it(`${mode} / code / ${size} is a complete typographic family`, () => {
49
+ const entry = codeSize(mode, size)
50
+ for (const [member, type] of Object.entries(MEMBERS)) {
51
+ const node = entry[member] as DtcgNode | undefined
52
+ expect(node, `${mode}.code.${size}.${member} is missing`).toBeDefined()
53
+ expect(node?.$type).toBe(type)
54
+ }
55
+ })
56
+
57
+ it(`${mode} / code / ${size} is set in the mono face`, () => {
58
+ const family = codeSize(mode, size)['font-family'] as DtcgNode | undefined
59
+ expect(family?.$value).toBe('{font-family.mono}')
60
+ expect(resolvePrimitive(family?.$value as string)).toBe('JetBrains Mono')
61
+ })
62
+ }
63
+ }
64
+ })