@halogen-ui/tokens 0.1.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/LICENSE.md +102 -0
- package/README.md +43 -0
- package/dist/CONTRAST-REPORT.md +511 -0
- package/dist/figma.json +1932 -0
- package/dist/fonts/Geist-Variable.woff2 +0 -0
- package/dist/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/fonts/LICENSE-Geist-OFL-1.1.txt +92 -0
- package/dist/fonts.css +24 -0
- package/dist/halogen.tokens.json +2061 -0
- package/dist/theme.css +859 -0
- package/dist/token-review.html +902 -0
- package/dist/tokens.css +782 -0
- package/dist/tokens.d.ts +535 -0
- package/dist/tokens.js +532 -0
- package/dist/tokens.ts +540 -0
- package/dist/tw-classgroups.json +351 -0
- package/fonts/Geist-Variable.woff2 +0 -0
- package/fonts/GeistMono-Variable.woff2 +0 -0
- package/fonts/LICENSE-Geist-OFL-1.1.txt +92 -0
- package/package.json +66 -0
- package/src/color/oklch.ts +306 -0
- package/src/contrast-pairs.ts +473 -0
- package/src/namespaces.ts +59 -0
- package/src/primitives/color.ts +404 -0
- package/src/primitives/elevation.ts +38 -0
- package/src/primitives/motion.ts +129 -0
- package/src/primitives/scale.ts +163 -0
- package/src/primitives/typography.ts +184 -0
- package/src/semantic/index.ts +415 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halogen — dimensional primitives: space, radius, size, border width, layer.
|
|
3
|
+
*
|
|
4
|
+
* Every value here is a decision from the aesthetic direction, not a default.
|
|
5
|
+
* The gaps in the spacing scale are deliberate: there is no 28, 36, 44 or 56,
|
|
6
|
+
* because a scale with every value in it is not a scale.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 4px base. Two sub-step values exist for hairlines and optical nudges only.
|
|
11
|
+
*
|
|
12
|
+
* The 2px step is named `half`, not `0.5`. A dot is not valid in a CSS custom
|
|
13
|
+
* property name, and `--halogen-space-0.5` does not merely fail on its own — the
|
|
14
|
+
* parser's error recovery discarded the surrounding declarations, which took out
|
|
15
|
+
* every spacing and color primitive declared before it. The page rendered with
|
|
16
|
+
* no padding and no gaps anywhere, while every utility still compiled and every
|
|
17
|
+
* test still passed.
|
|
18
|
+
*/
|
|
19
|
+
export const space = {
|
|
20
|
+
0: '0px',
|
|
21
|
+
px: '1px',
|
|
22
|
+
half: '2px',
|
|
23
|
+
1: '4px',
|
|
24
|
+
2: '8px',
|
|
25
|
+
3: '12px',
|
|
26
|
+
4: '16px',
|
|
27
|
+
5: '20px',
|
|
28
|
+
6: '24px',
|
|
29
|
+
8: '32px',
|
|
30
|
+
10: '40px',
|
|
31
|
+
12: '48px',
|
|
32
|
+
16: '64px',
|
|
33
|
+
20: '80px',
|
|
34
|
+
24: '96px',
|
|
35
|
+
32: '128px',
|
|
36
|
+
40: '160px',
|
|
37
|
+
48: '192px',
|
|
38
|
+
/**
|
|
39
|
+
* 64 and 80 are card widths, not gaps.
|
|
40
|
+
*
|
|
41
|
+
* The scale's gaps are deliberate everywhere else — there is no 28, 36, 44 or
|
|
42
|
+
* 56, because a scale with every value in it is not a scale. These two earn a
|
|
43
|
+
* place because a horizontally scrolling rail needs a fixed item width, and
|
|
44
|
+
* `Rail` shipped asking for `w-64` and `w-80` against a scale that stopped at
|
|
45
|
+
* 48. Both compiled to nothing, so the default item width and the large one
|
|
46
|
+
* silently became `auto` — sm (192px) was the only width that worked, and
|
|
47
|
+
* `lg` was not larger than it.
|
|
48
|
+
*/
|
|
49
|
+
64: '256px',
|
|
50
|
+
80: '320px',
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Radius. The nesting law — inner = outer − padding, clamped to the nearest
|
|
55
|
+
* step, never below `xs` — is enforced by a helper in @halogen-ui/react, not by
|
|
56
|
+
* convention. `full` is reserved for components that *are* pills.
|
|
57
|
+
*/
|
|
58
|
+
export const radius = {
|
|
59
|
+
none: '0px',
|
|
60
|
+
xs: '4px',
|
|
61
|
+
sm: '6px',
|
|
62
|
+
md: '10px',
|
|
63
|
+
lg: '16px',
|
|
64
|
+
xl: '24px',
|
|
65
|
+
'2xl': '32px',
|
|
66
|
+
full: '9999px',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Control heights. `md` is the default. The 44px minimum touch target is not on
|
|
71
|
+
* this scale because it is not a height — it is a hit-area floor applied via
|
|
72
|
+
* pseudo-element expansion, so a visually 32px control still takes a 44px tap.
|
|
73
|
+
*/
|
|
74
|
+
export const control = {
|
|
75
|
+
sm: '32px',
|
|
76
|
+
md: '40px',
|
|
77
|
+
lg: '48px',
|
|
78
|
+
xl: '56px',
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
/** Icon sizes. Three, and only three. */
|
|
82
|
+
export const icon = {
|
|
83
|
+
sm: '16px',
|
|
84
|
+
md: '20px',
|
|
85
|
+
lg: '24px',
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
/** Icon stroke widths, scaled with size so optical weight stays constant. */
|
|
89
|
+
export const iconStroke = {
|
|
90
|
+
sm: '1.5',
|
|
91
|
+
md: '1.75',
|
|
92
|
+
lg: '2',
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
export const borderWidth = {
|
|
96
|
+
0: '0px',
|
|
97
|
+
1: '1px',
|
|
98
|
+
2: '2px',
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
/** Minimum interactive hit area. Exceeds WCAG 2.5.8 AA (24px) deliberately. */
|
|
102
|
+
export const target = {
|
|
103
|
+
/** WCAG 2.5.8 AA floor — the absolute minimum, used only where 44 is impossible. */
|
|
104
|
+
min: '24px',
|
|
105
|
+
/** The Halogen house standard for anything reachable by touch. */
|
|
106
|
+
touch: '44px',
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
/** Layout containers, from aesthetic direction §8. */
|
|
110
|
+
export const container = {
|
|
111
|
+
prose: '720px',
|
|
112
|
+
default: '1280px',
|
|
113
|
+
wide: '1440px',
|
|
114
|
+
} as const;
|
|
115
|
+
|
|
116
|
+
export const breakpoint = {
|
|
117
|
+
sm: '640px',
|
|
118
|
+
md: '768px',
|
|
119
|
+
lg: '1024px',
|
|
120
|
+
xl: '1280px',
|
|
121
|
+
'2xl': '1536px',
|
|
122
|
+
} as const;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Stacking order. Named rather than numeric at the call site so an overlay can
|
|
126
|
+
* never be given an arbitrary z-index, and so the layering relationship between
|
|
127
|
+
* a popover inside a dialog is a system decision rather than a local one.
|
|
128
|
+
*/
|
|
129
|
+
export const layer = {
|
|
130
|
+
base: '0',
|
|
131
|
+
raised: '10',
|
|
132
|
+
sticky: '100',
|
|
133
|
+
overlay: '1000',
|
|
134
|
+
modal: '1100',
|
|
135
|
+
popover: '1200',
|
|
136
|
+
toast: '1300',
|
|
137
|
+
tooltip: '1400',
|
|
138
|
+
} as const;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Media aspect ratios.
|
|
142
|
+
*
|
|
143
|
+
* A token scale rather than free-form ratios, for the same reason spacing is a
|
|
144
|
+
* scale: three ratios used consistently reads as a system, and eleven ratios
|
|
145
|
+
* used once each reads as whatever each author felt like that afternoon.
|
|
146
|
+
*
|
|
147
|
+
* `wide` exists because it was already in use as `aspect-[21/9]` inside a lookup
|
|
148
|
+
* map in ContentCard — the only arbitrary value in the library, and invisible to
|
|
149
|
+
* the rule that forbids them until the rule learned to read class-shaped strings
|
|
150
|
+
* outside call sites.
|
|
151
|
+
*/
|
|
152
|
+
export const aspect = {
|
|
153
|
+
/** Video, screenshots, most product media. */
|
|
154
|
+
video: '16 / 9',
|
|
155
|
+
/** Avatars, tiles, gallery thumbnails. */
|
|
156
|
+
square: '1 / 1',
|
|
157
|
+
/** A cinematic banner. Deliberately the only ratio wider than video. */
|
|
158
|
+
wide: '21 / 9',
|
|
159
|
+
} as const;
|
|
160
|
+
|
|
161
|
+
// No `portrait`. The scale holds the three ratios ContentCard actually offers;
|
|
162
|
+
// a fourth added "for completeness" is a token nothing uses, and the audit
|
|
163
|
+
// framework is explicit that tokens are not added to satisfy a checklist.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halogen — typographic primitives.
|
|
3
|
+
*
|
|
4
|
+
* Two scales, deliberately separate: a fixed UI scale for product surfaces and
|
|
5
|
+
* a fluid display scale for marketing. Mixing them is what produces interfaces
|
|
6
|
+
* where a dashboard header is accidentally 44px on a laptop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const fontFamily = {
|
|
10
|
+
/**
|
|
11
|
+
* `Geist`, not `Geist Sans`.
|
|
12
|
+
*
|
|
13
|
+
* The family is named **Geist**. This token asked for `'Geist Sans'` — a name
|
|
14
|
+
* no face has ever carried — from Phase 1 until Phase 6, so it matched
|
|
15
|
+
* nothing and every sans glyph in the system silently painted in
|
|
16
|
+
* `ui-sans-serif`. Measured to prove it: at 600/32px the string used for the
|
|
17
|
+
* check rendered 603.45px under `'Geist Sans'` and 603.45px under bare
|
|
18
|
+
* `ui-sans-serif` — identical — against 677.05px under `'Geist'`.
|
|
19
|
+
*
|
|
20
|
+
* The comment that used to sit here claimed "self-hosted, SIL OFL,
|
|
21
|
+
* redistributable to buyers" while the showcase loaded the family from
|
|
22
|
+
* Google's CDN. Now true: the variable faces are vendored in
|
|
23
|
+
* `packages/tokens/fonts/` with the OFL text beside them.
|
|
24
|
+
*
|
|
25
|
+
* Fallbacks are system faces with similar metrics so the page does not reflow
|
|
26
|
+
* when the webfont lands.
|
|
27
|
+
*/
|
|
28
|
+
sans: "'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
29
|
+
mono: "'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The vendored faces, and what has to be true for shipping them to be legal.
|
|
34
|
+
*
|
|
35
|
+
* Geist is SIL Open Font License 1.1, Copyright (c) 2023 Vercel in
|
|
36
|
+
* collaboration with basement.studio. OFL permits bundling and sale as part of a
|
|
37
|
+
* larger work, provided the license text travels with the font and the font is
|
|
38
|
+
* not sold on its own. The copyright line declares no Reserved Font Name, so the
|
|
39
|
+
* renaming clause is moot here — and moot anyway, since the faces are shipped
|
|
40
|
+
* unmodified.
|
|
41
|
+
*
|
|
42
|
+
* Asserted in the token suite: the files exist, the license sits beside them,
|
|
43
|
+
* and the license text is really OFL 1.1.
|
|
44
|
+
*/
|
|
45
|
+
export const VENDORED_FONTS = [
|
|
46
|
+
{ family: 'Geist', file: 'Geist-Variable.woff2', weights: '100 900', style: 'normal' },
|
|
47
|
+
{ family: 'Geist Mono', file: 'GeistMono-Variable.woff2', weights: '100 900', style: 'normal' },
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export const FONT_LICENSE = {
|
|
51
|
+
file: 'LICENSE-Geist-OFL-1.1.txt',
|
|
52
|
+
spdx: 'OFL-1.1',
|
|
53
|
+
holder: 'Vercel, in collaboration with basement.studio',
|
|
54
|
+
permitsRedistribution: true,
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Four weights. 300 and lighter are absent by design: light weights bloom and
|
|
59
|
+
* thin on dark grounds, and this is a dark-first system.
|
|
60
|
+
*/
|
|
61
|
+
export const fontWeight = {
|
|
62
|
+
regular: '400',
|
|
63
|
+
medium: '500',
|
|
64
|
+
semibold: '600',
|
|
65
|
+
bold: '700',
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Tracking law from aesthetic direction §7 — tightens as type grows, loosens as
|
|
70
|
+
* it shrinks. Encoded as named steps so a component cannot invent one.
|
|
71
|
+
*/
|
|
72
|
+
export const tracking = {
|
|
73
|
+
/**
|
|
74
|
+
* 11px uppercase eyebrow.
|
|
75
|
+
*
|
|
76
|
+
* Matches the `2xs` step's own letter-spacing on purpose. Components reach for
|
|
77
|
+
* `tracking-wider` explicitly on eyebrows and table headers, which overrides
|
|
78
|
+
* whatever the size step declares — so if the two disagree, tightening the step
|
|
79
|
+
* changes nothing and the disagreement is invisible. One value, two routes to
|
|
80
|
+
* it.
|
|
81
|
+
*/
|
|
82
|
+
wider: '0.015em',
|
|
83
|
+
/** <= 13px. Matches the `xs` step for the same reason. */
|
|
84
|
+
wide: '0.005em',
|
|
85
|
+
normal: '0em',
|
|
86
|
+
/** 16-20px */
|
|
87
|
+
tight: '-0.01em',
|
|
88
|
+
/** 20-32px */
|
|
89
|
+
tighter: '-0.015em',
|
|
90
|
+
/** 32-48px */
|
|
91
|
+
tightest: '-0.02em',
|
|
92
|
+
/** >= 48px */
|
|
93
|
+
display: '-0.03em',
|
|
94
|
+
/** the largest display step only */
|
|
95
|
+
displayTight: '-0.035em',
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
export const lineHeight = {
|
|
99
|
+
none: '1',
|
|
100
|
+
'display-2xl': '0.95',
|
|
101
|
+
'display-xl': '1',
|
|
102
|
+
'display-lg': '1.05',
|
|
103
|
+
'display-md': '1.1',
|
|
104
|
+
'display-sm': '1.2',
|
|
105
|
+
'display-xs': '1.25',
|
|
106
|
+
snug: '1.35',
|
|
107
|
+
normal: '1.5',
|
|
108
|
+
relaxed: '1.6',
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
/** Product/UI scale — fixed px. Tops out at `xl`; anything larger is display. */
|
|
112
|
+
export const fontSize = {
|
|
113
|
+
'2xs': '11px',
|
|
114
|
+
xs: '12px',
|
|
115
|
+
sm: '13px',
|
|
116
|
+
base: '14px',
|
|
117
|
+
md: '16px',
|
|
118
|
+
lg: '18px',
|
|
119
|
+
xl: '20px',
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Line-height and tracking paired to each size step.
|
|
124
|
+
*
|
|
125
|
+
* Tailwind pairs these via `--text-<name>--line-height` and
|
|
126
|
+
* `--text-<name>--letter-spacing`. Without the pairing a `text-*` utility sets
|
|
127
|
+
* the font size and leaves line-height inherited from the document — so a 60px
|
|
128
|
+
* display headline renders at a 1.5 body leading, which is exactly what
|
|
129
|
+
* happened: the hero read as two loosely stacked lines instead of one block.
|
|
130
|
+
*
|
|
131
|
+
* Pairing also makes the tracking law from the aesthetic direction automatic.
|
|
132
|
+
* Tracking tightens as type grows; encoding it here means a heading cannot be
|
|
133
|
+
* set at the wrong tracking by forgetting a second utility.
|
|
134
|
+
*/
|
|
135
|
+
export const fontMetrics: Record<string, { lineHeight: string; tracking: string }> = {
|
|
136
|
+
/**
|
|
137
|
+
* The two smallest steps sit tighter than the "loosen as it shrinks" law
|
|
138
|
+
* would put them.
|
|
139
|
+
*
|
|
140
|
+
* The law is right about lowercase copy at small sizes, and wrong here: both
|
|
141
|
+
* of these steps are used almost exclusively for SHORT UPPERCASE labels —
|
|
142
|
+
* table headers, eyebrows, stat captions — and uppercase already carries its
|
|
143
|
+
* own optical spacing. At 0.06em an eleven-character header read as five
|
|
144
|
+
* separate words; 0.03em was still loose. 0.015em is the setting where an
|
|
145
|
+
* uppercase label reads as one word at 11px.
|
|
146
|
+
*/
|
|
147
|
+
'2xs': { lineHeight: '16px', tracking: '0.015em' },
|
|
148
|
+
xs: { lineHeight: '16px', tracking: '0.005em' },
|
|
149
|
+
sm: { lineHeight: '20px', tracking: '0.01em' },
|
|
150
|
+
base: { lineHeight: '20px', tracking: '0em' },
|
|
151
|
+
md: { lineHeight: '24px', tracking: '0em' },
|
|
152
|
+
lg: { lineHeight: '28px', tracking: '-0.005em' },
|
|
153
|
+
xl: { lineHeight: '28px', tracking: '-0.01em' },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** The display scale's own metrics. Leading tightens as the size grows. */
|
|
157
|
+
export const displayMetrics: Record<string, { lineHeight: string; tracking: string }> = {
|
|
158
|
+
xs: { lineHeight: '1.25', tracking: '-0.01em' },
|
|
159
|
+
sm: { lineHeight: '1.2', tracking: '-0.015em' },
|
|
160
|
+
md: { lineHeight: '1.1', tracking: '-0.02em' },
|
|
161
|
+
lg: { lineHeight: '1.05', tracking: '-0.025em' },
|
|
162
|
+
xl: { lineHeight: '1', tracking: '-0.03em' },
|
|
163
|
+
'2xl': { lineHeight: '0.95', tracking: '-0.035em' },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Display scale — fluid. `clamp()` bounds are the min and max from aesthetic
|
|
168
|
+
* direction §7; the middle term scales with viewport width, and every step is
|
|
169
|
+
* capped so a 4K monitor does not produce a 200px headline.
|
|
170
|
+
*/
|
|
171
|
+
export const displaySize = {
|
|
172
|
+
xs: 'clamp(1.5rem, 1.4rem + 0.5vw, 1.5rem)',
|
|
173
|
+
sm: 'clamp(1.75rem, 1.6rem + 0.75vw, 1.875rem)',
|
|
174
|
+
md: 'clamp(2.25rem, 1.9rem + 1.5vw, 2.75rem)',
|
|
175
|
+
lg: 'clamp(2.75rem, 2.1rem + 3vw, 3.75rem)',
|
|
176
|
+
xl: 'clamp(3.5rem, 2.4rem + 5vw, 5.5rem)',
|
|
177
|
+
'2xl': 'clamp(4.5rem, 2.6rem + 8vw, 8rem)',
|
|
178
|
+
} as const;
|
|
179
|
+
|
|
180
|
+
/** Body copy never exceeds this measure. */
|
|
181
|
+
export const measure = {
|
|
182
|
+
prose: '68ch',
|
|
183
|
+
narrow: '46ch',
|
|
184
|
+
} as const;
|