@bubble-design-system/ui 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 ADDED
@@ -0,0 +1,1039 @@
1
+ # @bubble-design-system/ui
2
+
3
+ > Bubble — a **neutral, composable, token-driven UI foundation** built on [Base UI](https://base-ui.com/), shipped as a single plain CSS file.
4
+
5
+ Bubble's signature is the **soft tone with a teal brand**: a soft-gray page (`#ECEDEF`) on which **white pill-shaped surfaces float** via layered shadows + an inset white top-highlight, accented by teal (`#00CEC8`) and a pink→magenta→violet **gradient blob** mark. The canonical identity is `tone=soft · brand=teal · gray=slate · radius=default · density=default · font=geist · light`.
6
+
7
+ Every visual decision — tone, color, brand, gray family, radius, density, typography, motion — is driven by CSS custom properties. Toggle a single `data-*` attribute on `<html>` and the whole app re-skins live, with no rebuild.
8
+
9
+ ```tsx
10
+ <html
11
+ data-theme="light"
12
+ data-tone="soft"
13
+ data-brand="teal"
14
+ data-gray="slate"
15
+ data-radius="default"
16
+ data-density="default"
17
+ data-font="geist"
18
+ >
19
+ ```
20
+
21
+ - **21 components** — Button, Input, Textarea, Checkbox, Radio, Switch, Select, Badge, Avatar, Divider, Modal, Toast, Tooltip, Tabs, Alert, DropdownMenu, Skeleton, Card, StatusPill, Segmented (plus Container + Grid layout primitives). Each wraps an [`@base-ui/react`](https://base-ui.com/) primitive where one exists — accessible by construction, styled with a single shipped stylesheet.
22
+ - **A 3-layer, multi-theme token system** spanning color (light/dark · 3 gray families · 6 brand palettes including teal), **3 tones** (vivid · pastel · soft — soft is the signature look), radius (4 scales), density (3 scales), typography (3 font pairs), layered shadows, and motion.
23
+ - **Live theme switching** via seven `data-*` attributes on any ancestor. Every CSS rule reads `var(--…)` at use-site, so swapping `data-tone="vivid"` for `data-tone="soft"` reflows the UI without re-rendering or rebuilding.
24
+ - **No build dependency in consumer apps.** One CSS import. No PostCSS, no Tailwind, no preprocessor required.
25
+ - **Dual ESM + CJS** with per-format `.d.ts` / `.d.cts`. `sideEffects` is scoped to the CSS so unused components tree-shake.
26
+ - **Composable, not opinionated** — components take `children`, spread `...props`, forward refs, expose compound sub-parts (`Card.Header`, `StatusPill.Indicator`, `Segmented.Item`), and emit stable BEM class names that you can target with plain CSS to override defaults.
27
+
28
+ ## Design principles
29
+
30
+ The five rules every decision in Bubble traces back to:
31
+
32
+ 1. **Restraint over decoration.** Few colors, light shadows, moderate radius. Every visual element must justify itself.
33
+ 2. **Composable, not opinionated.** No business logic baked into components, so the next person can remix freely (a `Card` never forces a header).
34
+ 3. **Accessible by default.** Contrast, focus state, keyboard nav pass WCAG AA without extra thought.
35
+ 4. **Token-driven.** Nothing is hardcoded in a component — every value references a token, so a new theme re-skins the whole system instantly.
36
+ 5. **One way to do things.** If there are two ways to do the same thing, pick one.
37
+
38
+ ---
39
+
40
+ ## Table of contents
41
+
42
+ - [Tech stack](#tech-stack)
43
+ - [Installation](#installation)
44
+ - [Setup](#setup)
45
+ - [Runtime theming](#runtime-theming)
46
+ - [Components](#components)
47
+ - [Design tokens](#design-tokens)
48
+ - [Overriding styles](#overriding-styles)
49
+ - [Local development](#local-development)
50
+ - [Contributing](#contributing)
51
+ - [License](#license)
52
+
53
+ ---
54
+
55
+ ## Tech stack
56
+
57
+ | Concern | Tool |
58
+ |---|---|
59
+ | Framework | React 19 (works with ≥ 18.2) |
60
+ | Primitives | `@base-ui/react` ≥ 1.0 (the post-rename successor to `@base-ui-components/react`) |
61
+ | Styling | Plain CSS — one shipped stylesheet, hand-authored per component |
62
+ | Class composition | `clsx` (re-exported as `cn()`) |
63
+ | Build tool | `tsup` (ESM + CJS + dual `.d.ts`) + a 50-line Node script for CSS bundling |
64
+ | Language | TypeScript 6 |
65
+ | Package manager | `pnpm@10.33.0` |
66
+ | Node | ≥ 20 |
67
+
68
+ ---
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ # npm
74
+ npm install @bubble-design-system/ui
75
+
76
+ # pnpm
77
+ pnpm add @bubble-design-system/ui
78
+
79
+ # yarn
80
+ yarn add @bubble-design-system/ui
81
+ ```
82
+
83
+ Then install the peer dependencies your app must have:
84
+
85
+ ```bash
86
+ npm install react react-dom @base-ui/react
87
+ ```
88
+
89
+ | Peer dependency | Required version |
90
+ |---|---|
91
+ | `react` | ≥ 18.2 |
92
+ | `react-dom` | ≥ 18.2 |
93
+ | `@base-ui/react` | ≥ 1.0.0 |
94
+
95
+ ---
96
+
97
+ ## Setup
98
+
99
+ ### 1. Import the stylesheet
100
+
101
+ The shipped CSS contains the design tokens, a minimal reset, and every component rule. One import wires everything up — no PostCSS plugin, no Tailwind config, no preprocessor.
102
+
103
+ ```css
104
+ /* app/globals.css — or wherever your global styles live */
105
+ @import "@bubble-design-system/ui/styles.css";
106
+ ```
107
+
108
+ Or, in a TS/JS entry file (Vite, Next.js App Router, Webpack, Parcel, …):
109
+
110
+ ```ts
111
+ import "@bubble-design-system/ui/styles.css";
112
+ ```
113
+
114
+ If you only want the raw CSS custom properties (no component rules), import the tokens file directly:
115
+
116
+ ```css
117
+ @import "@bubble-design-system/ui/tokens.css";
118
+ ```
119
+
120
+ ### 2. Set the theme attributes on your root element
121
+
122
+ ```tsx
123
+ // app/layout.tsx (Next.js App Router example)
124
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
125
+ return (
126
+ <html
127
+ lang="en"
128
+ data-theme="light"
129
+ data-tone="soft"
130
+ data-brand="teal"
131
+ data-gray="slate"
132
+ data-radius="default"
133
+ data-density="default"
134
+ data-font="geist"
135
+ >
136
+ <body>{children}</body>
137
+ </html>
138
+ );
139
+ }
140
+ ```
141
+
142
+ Every attribute has a sensible default if omitted, but spelling them out makes the design surface explicit.
143
+
144
+ ### 3. Use components
145
+
146
+ ```tsx
147
+ import { Button, Modal, Divider } from "@bubble-design-system/ui";
148
+
149
+ export function Example() {
150
+ return (
151
+ <div>
152
+ <Button variant="primary">Save</Button>
153
+ <Divider />
154
+ <Modal.Root>
155
+ <Modal.Trigger render={<Button variant="secondary" />}>Open</Modal.Trigger>
156
+ <Modal.Content>
157
+ <Modal.Title>Confirm</Modal.Title>
158
+ <Modal.Description>Are you sure?</Modal.Description>
159
+ </Modal.Content>
160
+ </Modal.Root>
161
+ </div>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Runtime theming
169
+
170
+ Seven `data-*` attributes on any ancestor element (typically `<html>` or `<body>`) re-skin every descendant at runtime, with no rebuild.
171
+
172
+ | Attribute | Values | Default | What it controls |
173
+ |---|---|---|---|
174
+ | `data-theme` | `light` · `dark` | `light` | Semantic color mapping (background, text, border, shadow). |
175
+ | `data-tone` | `vivid` · `pastel` · `soft` | `soft` | Surface model, palette saturation, control radius. `soft` is the signature look. |
176
+ | `data-gray` | `slate` · `neutral` · `stone` | `slate` | The gray family used for surfaces and text. |
177
+ | `data-brand` | `blue` · `violet` · `emerald` · `orange` · `mono` · `teal` | `teal` | The brand palette (`--brand-50` through `--brand-950`). |
178
+ | `data-radius` | `default` · `sharp` · `soft` · `pill` | `default` | The corner radius scale (`--radius-xs` through `--radius-2xl`). |
179
+ | `data-density` | `default` · `compact` · `comfortable` | `default` | Control heights and padding (`--control-h-*`, `--control-px-*`). |
180
+ | `data-font` | `geist` · `plex` · `system` | `geist` | The font pair (`--font-sans` / `--font-mono`). |
181
+
182
+ Toggle them with any approach you like — `setAttribute`, React state, a media-query listener, a server cookie:
183
+
184
+ ```ts
185
+ document.documentElement.setAttribute("data-theme", "dark");
186
+ document.documentElement.setAttribute("data-brand", "violet");
187
+ ```
188
+
189
+ A working live-switcher implementation (with `localStorage` persistence and an SSR-safe bootstrap script) lives in the docs app — see `apps/docs/app/ThemeBar.tsx` and `apps/docs/app/layout.tsx` in the [repository](https://github.com/mushroomgead/bubble-design-system).
190
+
191
+ ---
192
+
193
+ ## Components
194
+
195
+ Every component is exported from the package root:
196
+
197
+ ```tsx
198
+ import {
199
+ Alert, Avatar, Badge, Button, Card, Checkbox, Container, Divider,
200
+ DropdownMenu, Grid, Input, Modal, Radio, RadioGroup, Segmented,
201
+ Select, Skeleton, StatusPill, Switch, Tabs, Textarea, Toast, Tooltip,
202
+ useToast, cn,
203
+ } from "@bubble-design-system/ui";
204
+ ```
205
+
206
+ Conventions shared by all components:
207
+
208
+ - They `forwardRef` to the underlying Base UI primitive.
209
+ - Native HTML attributes are spread via `...props` — `onClick`, `aria-*`, `id`, `style` all just work.
210
+ - Each component emits stable BEM class names (`pds-btn`, `pds-btn--primary`, `pds-card__header`, …). Your `className` is appended last in the final class string.
211
+ - Variants and sizes are string-literal enums with documented defaults.
212
+ - Both `:disabled` and `[data-disabled]` are styled (Base UI uses the data-attr form).
213
+
214
+ ---
215
+
216
+ ### Alert
217
+
218
+ A static informational banner with a variant-specific icon, title, and body.
219
+
220
+ | Prop | Type | Default | Description |
221
+ |---|---|---|---|
222
+ | `variant` | `"info" \| "success" \| "warning" \| "danger"` | `"info"` | Visual tone and default icon. |
223
+ | `icon` | `ReactNode \| false` | variant-specific | Override the default icon, or pass `false` to hide it. |
224
+ | `title` | `ReactNode` | — | Optional bold header line. |
225
+ | `children` | `ReactNode` | — | The body copy, rendered in the secondary text color. |
226
+ | `className` | `string` | — | Extra classes (appended after the library's). |
227
+ | `...props` | `HTMLAttributes<HTMLDivElement>` (minus `title`) | — | All native div attributes. |
228
+
229
+ ```tsx
230
+ <Alert variant="success" title="Saved">Your changes are live.</Alert>
231
+ <Alert variant="danger" title="Couldn't save" icon={false}>
232
+ Try again in a moment.
233
+ </Alert>
234
+ ```
235
+
236
+ ---
237
+
238
+ ### Avatar
239
+
240
+ Compound component built on `@base-ui/react/avatar` — handles image load failure with a fallback.
241
+
242
+ | Sub-component | Description |
243
+ |---|---|
244
+ | `Avatar` (root) | Sized circular surface. |
245
+ | `Avatar.Image` | The image element. |
246
+ | `Avatar.Fallback` | Shown while the image is loading or after it fails. |
247
+
248
+ **`Avatar` props:**
249
+
250
+ | Prop | Type | Default | Description |
251
+ |---|---|---|---|
252
+ | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | 24 / 32 / 40 / 48 px. |
253
+ | `className` | `string` | — | Extra classes. |
254
+ | `...props` | Base UI `Avatar.Root` props | — | Spread to the root. |
255
+
256
+ `Avatar.Image` and `Avatar.Fallback` accept their Base UI props plus `className`.
257
+
258
+ ```tsx
259
+ <Avatar size="lg">
260
+ <Avatar.Image src="/me.jpg" alt="Ada" />
261
+ <Avatar.Fallback>AL</Avatar.Fallback>
262
+ </Avatar>
263
+ ```
264
+
265
+ ---
266
+
267
+ ### Badge
268
+
269
+ Small inline pill for status, counts, or labels.
270
+
271
+ | Prop | Type | Default | Description |
272
+ |---|---|---|---|
273
+ | `variant` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger"` | `"neutral"` | Background and text color. |
274
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Pill height and padding. |
275
+ | `className` | `string` | — | Extra classes. |
276
+ | `...props` | `HTMLAttributes<HTMLSpanElement>` | — | Native span attributes. |
277
+
278
+ ```tsx
279
+ <Badge variant="success">Active</Badge>
280
+ <Badge variant="brand" size="sm">New</Badge>
281
+ ```
282
+
283
+ ---
284
+
285
+ ### Button
286
+
287
+ Wraps `@base-ui/react/button`.
288
+
289
+ | Prop | Type | Default | Description |
290
+ |---|---|---|---|
291
+ | `variant` | `"primary" \| "secondary" \| "destructive" \| "ghost"` | `"primary"` | Visual style. |
292
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Maps to `--control-h-*` / `--control-px-*` so density attribute affects it. |
293
+ | `className` | `string` | — | Extra classes. |
294
+ | `...props` | Base UI `Button` props | — | Includes `disabled`, `type`, `onClick`, etc. |
295
+
296
+ ```tsx
297
+ <Button variant="primary" size="lg" onClick={save}>Save</Button>
298
+ <Button variant="destructive">Delete</Button>
299
+ <Button variant="ghost" disabled>Cancel</Button>
300
+ ```
301
+
302
+ ---
303
+
304
+ ### Card
305
+
306
+ Compound component for floating-pill surface content.
307
+
308
+ | Sub-component | Description |
309
+ |---|---|
310
+ | `Card` (root) | The floating surface. Variant controls fill + shadow. |
311
+ | `Card.Header` | Row with title + optional action. |
312
+ | `Card.Title` | `<h3>` heading. |
313
+ | `Card.Description` | Supporting paragraph. |
314
+ | `Card.Action` | Right-aligned controls inside the header. |
315
+ | `Card.Body` | Main content area. |
316
+ | `Card.Footer` | Bordered footer row with right-aligned controls. |
317
+
318
+ **`Card` props:**
319
+
320
+ | Prop | Type | Default | Description |
321
+ |---|---|---|---|
322
+ | `variant` | `"elevated" \| "muted"` | `"elevated"` | `elevated` = white surface with shadow. `muted` = `bg-secondary`, no shadow. |
323
+ | `className` | `string` | — | Extra classes. |
324
+
325
+ ```tsx
326
+ <Card>
327
+ <Card.Header>
328
+ <div>
329
+ <Card.Title>Soft-pill surface</Card.Title>
330
+ <Card.Description>White card floating on a gray page.</Card.Description>
331
+ </div>
332
+ <Card.Action>
333
+ <Button size="sm" variant="ghost">Manage</Button>
334
+ </Card.Action>
335
+ </Card.Header>
336
+ <Card.Body>…</Card.Body>
337
+ <Card.Footer>
338
+ <Button size="sm" variant="ghost">Cancel</Button>
339
+ <Button size="sm">Save</Button>
340
+ </Card.Footer>
341
+ </Card>
342
+ ```
343
+
344
+ ---
345
+
346
+ ### Checkbox
347
+
348
+ Wraps `@base-ui/react/checkbox`. Supports checked, unchecked, and indeterminate states with built-in SVG indicators.
349
+
350
+ | Prop | Type | Default | Description |
351
+ |---|---|---|---|
352
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 18 / 20 px. |
353
+ | `className` | `string` | — | Extra classes on the root button. |
354
+ | `...props` | Base UI `Checkbox.Root` props | — | `checked`, `defaultChecked`, `indeterminate`, `onCheckedChange`, etc. |
355
+
356
+ ```tsx
357
+ <Checkbox defaultChecked />
358
+ <Checkbox indeterminate />
359
+ <Checkbox disabled />
360
+ ```
361
+
362
+ ---
363
+
364
+ ### Container + Grid
365
+
366
+ Layout primitives. `Container` centers content and applies page margins. `Grid` is a 12-column grid; `Grid.Col` spans columns with optional responsive overrides.
367
+
368
+ ```tsx
369
+ <Container size="lg">
370
+ <Grid>
371
+ <Grid.Col span={12}>full row</Grid.Col>
372
+ <Grid.Col span={6} lgSpan={4}>half on mobile, third on lg</Grid.Col>
373
+ <Grid.Col span={6} lgSpan={4}>…</Grid.Col>
374
+ <Grid.Col span={12} lgSpan={4}>…</Grid.Col>
375
+ </Grid>
376
+ </Container>
377
+ ```
378
+
379
+ **`Container` props:** `size` = `"sm" | "md" | "lg" | "xl" | "prose" | "fluid"` (default `"xl"`).
380
+
381
+ **`Grid` props:** `gutter` = `"default" | "tight" | "flush"` (default `"default"`).
382
+
383
+ **`Grid.Col` props:** `span`, `smSpan`, `mdSpan`, `lgSpan` = `1 | 2 | … | 12 | "full"`. Default `span` is `12`.
384
+
385
+ ---
386
+
387
+ ### Divider
388
+
389
+ Horizontal or vertical separator with a semantic role from `@base-ui/react/separator`.
390
+
391
+ | Prop | Type | Default | Description |
392
+ |---|---|---|---|
393
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction. |
394
+ | `className` | `string` | — | Extra classes. |
395
+ | `...props` | Base UI `Separator` props | — | Spread to the underlying element. |
396
+
397
+ ```tsx
398
+ <Divider />
399
+ <div style={{ display: "flex", height: "2rem", alignItems: "center" }}>
400
+ <span>A</span><Divider orientation="vertical" /><span>B</span>
401
+ </div>
402
+ ```
403
+
404
+ ---
405
+
406
+ ### DropdownMenu
407
+
408
+ Compound component built on `@base-ui/react/menu`. Supports items, checkbox items, radio groups, labels, and separators.
409
+
410
+ | Sub-component | Description |
411
+ |---|---|
412
+ | `DropdownMenu.Root` | The state container. |
413
+ | `DropdownMenu.Trigger` | The element that opens the menu. |
414
+ | `DropdownMenu.Content` | The portalled popup. |
415
+ | `DropdownMenu.Item` | A selectable row. |
416
+ | `DropdownMenu.CheckboxItem` | A toggleable row with a check indicator. |
417
+ | `DropdownMenu.RadioGroup` | Wrapper for radio items. |
418
+ | `DropdownMenu.RadioItem` | A single radio choice. |
419
+ | `DropdownMenu.Group` | Logical group of items. |
420
+ | `DropdownMenu.Label` | Uppercase group label. |
421
+ | `DropdownMenu.Separator` | Thin horizontal divider. |
422
+
423
+ **`DropdownMenu.Content` props:**
424
+
425
+ | Prop | Type | Default | Description |
426
+ |---|---|---|---|
427
+ | `sideOffset` | `number` | `6` | Distance in px from the trigger. |
428
+ | `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger. |
429
+ | `className` | `string` | — | Extra classes on the popup. |
430
+ | `...props` | Base UI `Menu.Popup` props | — | Spread to the popup. |
431
+
432
+ ```tsx
433
+ <DropdownMenu.Root>
434
+ <DropdownMenu.Trigger render={<Button variant="secondary">Options</Button>} />
435
+ <DropdownMenu.Content>
436
+ <DropdownMenu.Label>Actions</DropdownMenu.Label>
437
+ <DropdownMenu.Item>Edit</DropdownMenu.Item>
438
+ <DropdownMenu.Item>Duplicate</DropdownMenu.Item>
439
+ <DropdownMenu.Separator />
440
+ <DropdownMenu.CheckboxItem checked>Pinned</DropdownMenu.CheckboxItem>
441
+ </DropdownMenu.Content>
442
+ </DropdownMenu.Root>
443
+ ```
444
+
445
+ ---
446
+
447
+ ### Input
448
+
449
+ Wraps `@base-ui/react/input`. Includes built-in invalid styling via `aria-invalid`.
450
+
451
+ | Prop | Type | Default | Description |
452
+ |---|---|---|---|
453
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Density-aware control height. |
454
+ | `invalid` | `boolean` | — | Sets `aria-invalid` and applies the danger border + focus ring. |
455
+ | `className` | `string` | — | Extra classes. |
456
+ | `...props` | Base UI `Input` props (minus `size`) | — | All native input attributes: `value`, `placeholder`, `type`, `onChange`, `disabled`, etc. |
457
+
458
+ ```tsx
459
+ <Input placeholder="Email" />
460
+ <Input size="lg" invalid value={email} onChange={(e) => setEmail(e.target.value)} />
461
+ ```
462
+
463
+ ---
464
+
465
+ ### Modal
466
+
467
+ Compound component built on `@base-ui/react/dialog`. Renders into a portal, with backdrop blur and scale-in animation.
468
+
469
+ | Sub-component | Description |
470
+ |---|---|
471
+ | `Modal.Root` | Open-state container. |
472
+ | `Modal.Trigger` | Element that opens the modal. |
473
+ | `Modal.Close` | Element that closes the modal. |
474
+ | `Modal.Content` | The portalled popup with backdrop. |
475
+ | `Modal.Title` | Heading text. |
476
+ | `Modal.Description` | Sub-text under the title. |
477
+
478
+ **`Modal.Content` props:**
479
+
480
+ | Prop | Type | Default | Description |
481
+ |---|---|---|---|
482
+ | `className` | `string` | — | Extra classes on the popup. |
483
+ | `backdropClassName` | `string` | — | Extra classes on the backdrop. |
484
+ | `...props` | Base UI `Dialog.Popup` props | — | Spread to the popup. |
485
+
486
+ `Modal.Title` and `Modal.Description` accept their Base UI props plus `className`.
487
+
488
+ ```tsx
489
+ <Modal.Root>
490
+ <Modal.Trigger render={<Button>Open</Button>} />
491
+ <Modal.Content>
492
+ <Modal.Title>Delete project?</Modal.Title>
493
+ <Modal.Description>This action cannot be undone.</Modal.Description>
494
+ <div style={{ marginTop: "1rem", display: "flex", justifyContent: "flex-end", gap: "0.5rem" }}>
495
+ <Modal.Close render={<Button variant="secondary">Cancel</Button>} />
496
+ <Button variant="destructive">Delete</Button>
497
+ </div>
498
+ </Modal.Content>
499
+ </Modal.Root>
500
+ ```
501
+
502
+ ---
503
+
504
+ ### Radio / RadioGroup
505
+
506
+ Wraps `@base-ui/react/radio` and `@base-ui/react/radio-group`.
507
+
508
+ **`Radio` props:**
509
+
510
+ | Prop | Type | Default | Description |
511
+ |---|---|---|---|
512
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 18 / 20 px. |
513
+ | `className` | `string` | — | Extra classes. |
514
+ | `...props` | Base UI `Radio.Root` props | — | `value`, `disabled`, etc. |
515
+
516
+ **`RadioGroup` props:**
517
+
518
+ | Prop | Type | Default | Description |
519
+ |---|---|---|---|
520
+ | `className` | `string` | — | Override the default vertical stack. |
521
+ | `...props` | Base UI `RadioGroup` props | — | `value`, `defaultValue`, `onValueChange`. |
522
+
523
+ ```tsx
524
+ <RadioGroup defaultValue="email">
525
+ <label><Radio value="email" /> Email</label>
526
+ <label><Radio value="sms" /> SMS</label>
527
+ </RadioGroup>
528
+ ```
529
+
530
+ ---
531
+
532
+ ### Segmented
533
+
534
+ Compound component built on `@base-ui/react/toggle-group` (single-select). The selected item rises as a white floating pill.
535
+
536
+ | Sub-component | Description |
537
+ |---|---|
538
+ | `Segmented` (root) | The toggle group. |
539
+ | `Segmented.Item` | A single segment. |
540
+
541
+ **`Segmented` props:**
542
+
543
+ | Prop | Type | Default | Description |
544
+ |---|---|---|---|
545
+ | `value` | `string` | — | Controlled selected value. |
546
+ | `defaultValue` | `string` | — | Uncontrolled initial value. |
547
+ | `onValueChange` | `(value: string) => void` | — | Fired with the new value. |
548
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | 24 / 28 / 32 px. |
549
+
550
+ ```tsx
551
+ <Segmented value={range} onValueChange={setRange}>
552
+ <Segmented.Item value="day">Day</Segmented.Item>
553
+ <Segmented.Item value="week">Week</Segmented.Item>
554
+ <Segmented.Item value="month">Month</Segmented.Item>
555
+ </Segmented>
556
+ ```
557
+
558
+ ---
559
+
560
+ ### Select
561
+
562
+ Compound component built on `@base-ui/react/select`.
563
+
564
+ | Sub-component | Description |
565
+ |---|---|
566
+ | `Select.Root` | State container (`value`, `onValueChange`). |
567
+ | `Select.Trigger` | The clickable trigger; renders a chevron icon. |
568
+ | `Select.Value` | The selected value's display. |
569
+ | `Select.Content` | The portalled popup. |
570
+ | `Select.Item` | A selectable row with a check indicator when selected. |
571
+
572
+ **`Select.Trigger` props:** `size` = `"sm" | "md" | "lg"` (default `"md"`), plus `className`.
573
+
574
+ **`Select.Value` props:** `placeholder?: ReactNode`, plus `className`.
575
+
576
+ **`Select.Content` props:** `sideOffset?: number` (default `6`), plus `className`.
577
+
578
+ ```tsx
579
+ <Select.Root value={fruit} onValueChange={setFruit}>
580
+ <Select.Trigger size="md">
581
+ <Select.Value placeholder="Pick one" />
582
+ </Select.Trigger>
583
+ <Select.Content>
584
+ <Select.Item value="apple">Apple</Select.Item>
585
+ <Select.Item value="banana">Banana</Select.Item>
586
+ <Select.Item value="cherry">Cherry</Select.Item>
587
+ </Select.Content>
588
+ </Select.Root>
589
+ ```
590
+
591
+ ---
592
+
593
+ ### Skeleton
594
+
595
+ Loading placeholder with a pulse animation.
596
+
597
+ | Prop | Type | Default | Description |
598
+ |---|---|---|---|
599
+ | `shape` | `"line" \| "circle" \| "block"` | `"line"` | Default dimensions and radius. |
600
+ | `className` | `string` | — | Override width/height/radius via your own CSS. |
601
+ | `...props` | `HTMLAttributes<HTMLDivElement>` | — | Native div attributes. |
602
+
603
+ ```tsx
604
+ <Skeleton />
605
+ <Skeleton shape="circle" style={{ width: "2.5rem", height: "2.5rem" }} />
606
+ <Skeleton shape="block" style={{ height: "6rem" }} />
607
+ ```
608
+
609
+ ---
610
+
611
+ ### StatusPill
612
+
613
+ Compound floating-pill component for status indicators. Intent drives chip + label color via CSS custom properties.
614
+
615
+ | Sub-component | Description |
616
+ |---|---|
617
+ | `StatusPill` (root) | The pill surface. |
618
+ | `StatusPill.Indicator` | The leading colored chip. Children render an optional icon. |
619
+ | `StatusPill.Label` | The colored text label. |
620
+
621
+ **`StatusPill` props:** `intent` = `"neutral" | "success" | "warning" | "danger" | "info"` (default `"neutral"`).
622
+
623
+ ```tsx
624
+ <StatusPill intent="success">
625
+ <StatusPill.Indicator />
626
+ <StatusPill.Label>On track</StatusPill.Label>
627
+ </StatusPill>
628
+ ```
629
+
630
+ ---
631
+
632
+ ### Switch
633
+
634
+ Wraps `@base-ui/react/switch` — a thumb that slides on `data-[checked]`.
635
+
636
+ | Prop | Type | Default | Description |
637
+ |---|---|---|---|
638
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | 16 / 20 / 24 px tall. |
639
+ | `className` | `string` | — | Extra classes on the root. |
640
+ | `...props` | Base UI `Switch.Root` props | — | `checked`, `defaultChecked`, `onCheckedChange`, `disabled`. |
641
+
642
+ ```tsx
643
+ <Switch defaultChecked />
644
+ <Switch size="sm" />
645
+ ```
646
+
647
+ ---
648
+
649
+ ### Tabs
650
+
651
+ Compound component built on `@base-ui/react/tabs`. The list renders an animated indicator that slides between active tabs.
652
+
653
+ | Sub-component | Description |
654
+ |---|---|
655
+ | `Tabs` (root) | State container; pass `value`, `defaultValue`, `onValueChange`. |
656
+ | `Tabs.List` | Horizontal tab strip with a sliding indicator. |
657
+ | `Tabs.Tab` | A single tab button. |
658
+ | `Tabs.Panel` | The panel paired with a tab `value`. |
659
+
660
+ All sub-components accept their Base UI props plus `className`.
661
+
662
+ ```tsx
663
+ <Tabs defaultValue="profile">
664
+ <Tabs.List>
665
+ <Tabs.Tab value="profile">Profile</Tabs.Tab>
666
+ <Tabs.Tab value="account">Account</Tabs.Tab>
667
+ <Tabs.Tab value="billing">Billing</Tabs.Tab>
668
+ </Tabs.List>
669
+ <Tabs.Panel value="profile">Profile content</Tabs.Panel>
670
+ <Tabs.Panel value="account">Account content</Tabs.Panel>
671
+ <Tabs.Panel value="billing">Billing content</Tabs.Panel>
672
+ </Tabs>
673
+ ```
674
+
675
+ ---
676
+
677
+ ### Textarea
678
+
679
+ A thin styled `<textarea>` mirroring Input's API.
680
+
681
+ | Prop | Type | Default | Description |
682
+ |---|---|---|---|
683
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Density-aware control padding. |
684
+ | `invalid` | `boolean` | — | Sets `aria-invalid` and applies the danger border + focus ring. |
685
+ | `className` | `string` | — | Extra classes. |
686
+ | `...props` | `TextareaHTMLAttributes` (minus `size`) | — | All native textarea attributes. |
687
+
688
+ ```tsx
689
+ <Textarea placeholder="Notes" rows={4} />
690
+ <Textarea invalid value={text} onChange={(e) => setText(e.target.value)} />
691
+ ```
692
+
693
+ ---
694
+
695
+ ### Toast
696
+
697
+ Built on `@base-ui/react/toast`. Provides a `<Toast.Provider>` boundary, a `<Toast.Viewport>` for positioning, a pre-built `<Toast.Toaster>` that renders the queue, and the `useToast()` hook to push toasts.
698
+
699
+ | Sub-component | Description |
700
+ |---|---|
701
+ | `Toast.Provider` | Wrap your app to enable toasts. |
702
+ | `Toast.Viewport` | Positioned region where toasts mount (bottom-right by default). |
703
+ | `Toast.Toaster` | Pre-styled queue renderer — drop this inside `Provider`. |
704
+ | `useToast()` | Hook returning Base UI's toast manager (`.add({ title, description })`). |
705
+
706
+ ```tsx
707
+ import { Toast, useToast, Button } from "@bubble-design-system/ui";
708
+
709
+ function Root({ children }) {
710
+ return (
711
+ <Toast.Provider>
712
+ {children}
713
+ <Toast.Toaster />
714
+ </Toast.Provider>
715
+ );
716
+ }
717
+
718
+ function SaveButton() {
719
+ const toast = useToast();
720
+ return (
721
+ <Button onClick={() => toast.add({ title: "Saved", description: "Changes are live." })}>
722
+ Save
723
+ </Button>
724
+ );
725
+ }
726
+ ```
727
+
728
+ ---
729
+
730
+ ### Tooltip
731
+
732
+ Compound component built on `@base-ui/react/tooltip`. Requires a `Tooltip.Provider` ancestor (typically once at the app root).
733
+
734
+ | Sub-component | Description |
735
+ |---|---|
736
+ | `Tooltip.Provider` | App-level provider. |
737
+ | `Tooltip.Root` | Single tooltip state container. |
738
+ | `Tooltip.Trigger` | The hovered/focused element. |
739
+ | `Tooltip.Content` | The portalled popup. |
740
+
741
+ **`Tooltip.Content` props:**
742
+
743
+ | Prop | Type | Default | Description |
744
+ |---|---|---|---|
745
+ | `side` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Preferred side. |
746
+ | `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the side. |
747
+ | `sideOffset` | `number` | `6` | Distance from the trigger. |
748
+ | `className` | `string` | — | Extra classes on the popup. |
749
+
750
+ ```tsx
751
+ <Tooltip.Provider>
752
+ <Tooltip.Root>
753
+ <Tooltip.Trigger render={<Button variant="ghost">?</Button>} />
754
+ <Tooltip.Content side="top">Helpful hint</Tooltip.Content>
755
+ </Tooltip.Root>
756
+ </Tooltip.Provider>
757
+ ```
758
+
759
+ ---
760
+
761
+ ### `cn()` utility
762
+
763
+ Re-exported `clsx` wrapper for composing class names — useful when building your own components against the design tokens.
764
+
765
+ ```tsx
766
+ import { cn } from "@bubble-design-system/ui";
767
+
768
+ cn("my-card", isActive && "my-card--active", className);
769
+ // → "my-card my-card--active <consumer className>"
770
+ ```
771
+
772
+ ---
773
+
774
+ ## Design tokens
775
+
776
+ All tokens are CSS custom properties defined in `src/tokens.css`. Reference them directly in your CSS with `var(--…)`. The semantic tokens (those prefixed `--color-bg-*`, `--color-text-*`, `--color-border-*`) re-resolve automatically when an ancestor `data-*` attribute changes.
777
+
778
+ ### Color tokens
779
+
780
+ Semantic colors map to primitive palettes and are remapped by `[data-theme]`, `[data-gray]`, and `[data-brand]`.
781
+
782
+ | Token | Purpose |
783
+ |---|---|
784
+ | `--color-bg-primary` | Page background. |
785
+ | `--color-bg-secondary` | Secondary surface (cards on page). |
786
+ | `--color-bg-tertiary` | Tertiary surface (inset wells). |
787
+ | `--color-bg-inverse` | Inverted surface (tooltip, toast). |
788
+ | `--color-bg-brand` | Brand primary fill. |
789
+ | `--color-bg-brand-hover` | Brand hover state. |
790
+ | `--color-bg-brand-active` | Brand pressed state. |
791
+ | `--color-bg-brand-subtle` | Tinted brand surface (info backgrounds, badges). |
792
+ | `--color-bg-success` | Soft success surface. |
793
+ | `--color-bg-success-strong` | Solid success fill. |
794
+ | `--color-bg-warning` | Soft warning surface. |
795
+ | `--color-bg-warning-strong` | Solid warning fill. |
796
+ | `--color-bg-danger` | Soft danger surface. |
797
+ | `--color-bg-danger-strong` | Solid danger fill (destructive buttons). |
798
+ | `--color-bg-danger-hover` | Danger hover state. |
799
+ | `--color-bg-info` | Info alert surface. |
800
+ | `--color-bg-hover` | Neutral hover. |
801
+ | `--color-bg-pressed` | Neutral pressed. |
802
+ | `--color-bg-disabled` | Disabled surface. |
803
+ | `--color-text-primary` | Body text. |
804
+ | `--color-text-secondary` | Supporting text. |
805
+ | `--color-text-tertiary` | Placeholder, hints. |
806
+ | `--color-text-disabled` | Disabled text. |
807
+ | `--color-text-inverse` | Text on `bg-inverse`. |
808
+ | `--color-text-brand` | Brand-colored text (links). |
809
+ | `--color-text-success` | Success copy. |
810
+ | `--color-text-warning` | Warning copy. |
811
+ | `--color-text-danger` | Error copy. |
812
+ | `--color-text-on-brand` | Text on `bg-brand`. |
813
+ | `--color-text-on-danger` | Text on `bg-danger-strong`. |
814
+ | `--color-text-on-success` | Text on `bg-success-strong`. |
815
+ | `--color-border-primary` | Default form/control border. |
816
+ | `--color-border-secondary` | Section dividers, cards. |
817
+ | `--color-border-tertiary` | Subtle inner dividers. |
818
+ | `--color-border-brand` | Selected/active accent. |
819
+ | `--color-border-success` | Success accent. |
820
+ | `--color-border-warning` | Warning accent. |
821
+ | `--color-border-danger` | Error accent (invalid inputs). |
822
+ | `--color-border-focus` | Focus ring color. |
823
+
824
+ ### Primitive palettes
825
+
826
+ These are the raw color swatches that the semantic tokens reference. You usually shouldn't touch them directly, but they're exposed if you need to.
827
+
828
+ | Family | Stops | Notes |
829
+ |---|---|---|
830
+ | `--slate-*` | 50–950 | Default gray family. |
831
+ | `--neutral-*` | 50–950 | True neutral (no temperature). |
832
+ | `--stone-*` | 50–950 | Warm gray. |
833
+ | `--blue-*` | 50–950 | Brand option. |
834
+ | `--violet-*` | 50–950 | Brand option. |
835
+ | `--emerald-*` | 50–950 | Brand option. |
836
+ | `--orange-*` | 50–950 | Brand option. |
837
+ | `--green-*` | 50–950 | Success palette. |
838
+ | `--amber-*` | 50–950 | Warning palette. |
839
+ | `--red-*` | 50–950 | Danger palette. |
840
+ | `--white`, `--black` | — | Pure values. |
841
+
842
+ Aliases follow the active `data-*` attribute: `--gray-*` resolves to whichever gray family is selected, `--brand-*` to whichever brand. The `mono` and `teal` brands have special-case treatment for contrast on light/dark themes.
843
+
844
+ ### Radius
845
+
846
+ Selected by `[data-radius]`. Each scale rewrites the same custom properties.
847
+
848
+ | Token | default | sharp | soft | pill |
849
+ |---|---|---|---|---|
850
+ | `--radius-xs` | 2px | 0px | 4px | 4px |
851
+ | `--radius-sm` | 4px | 1px | 8px | 9999px |
852
+ | `--radius-md` | 6px | 2px | 12px | 9999px |
853
+ | `--radius-lg` | 8px | 3px | 14px | 9999px |
854
+ | `--radius-xl` | 12px | 4px | 18px | 18px |
855
+ | `--radius-2xl` | 16px | 6px | 24px | 22px |
856
+ | `--radius-full` | 9999px | 9999px | 9999px | 9999px |
857
+
858
+ Plus `--ctrl-radius` — the control radius used by Button/Input/Select. Pills under `[data-tone="soft"]`, `--radius-md` elsewhere.
859
+
860
+ ### Shadow
861
+
862
+ Light theme uses cool slate tints; dark theme uses opaque black. The `soft` tone adds an inset white top-highlight on md/lg/xl. The focus ring tracks the brand color.
863
+
864
+ | Token | Purpose |
865
+ |---|---|
866
+ | `--shadow-xs` | Hairline lift. |
867
+ | `--shadow-sm` | Subtle card. |
868
+ | `--shadow-md` | Standard card. |
869
+ | `--shadow-lg` | Popover, dropdown. |
870
+ | `--shadow-xl` | Modal. |
871
+ | `--shadow-focus` | Focus ring (3–4px brand-tinted). |
872
+
873
+ ### Typography
874
+
875
+ | Token | Value |
876
+ |---|---|
877
+ | `--font-size-xs` | 0.75rem |
878
+ | `--font-size-sm` | 0.875rem |
879
+ | `--font-size-md` | 1rem |
880
+ | `--font-size-lg` | 1.125rem |
881
+ | `--font-size-xl` | 1.25rem |
882
+ | `--font-size-2xl` | 1.5rem |
883
+ | `--font-size-3xl` | 1.875rem |
884
+ | `--font-size-4xl` | 2.25rem |
885
+ | `--font-size-5xl` | 3rem |
886
+ | `--font-size-6xl` | 3.75rem |
887
+ | `--line-height-tight` / `snug` / `normal` / `relaxed` | 1.15 / 1.3 / 1.5 / 1.65 |
888
+ | `--letter-tight` / `snug` / `normal` / `wide` | -0.022em / -0.012em / 0 / 0.04em |
889
+ | `--font-weight-regular` / `medium` / `semibold` / `bold` | 400 / 500 / 600 / 700 |
890
+ | `--font-sans` / `--font-mono` | Set by `[data-font]` |
891
+
892
+ ### Spacing
893
+
894
+ `--space-0` through `--space-24` follow a 0.25rem (4px) scale, doubling to 0.5rem after 4. Reference them with `var(--space-4)`, etc. — they have no utility-class shorthand because the library doesn't ship one.
895
+
896
+ ### Density (control sizing)
897
+
898
+ Selected by `[data-density]`.
899
+
900
+ | Token | default | compact | comfortable |
901
+ |---|---|---|---|
902
+ | `--control-h-sm` | 28px | 24px | 32px |
903
+ | `--control-h-md` | 36px | 30px | 42px |
904
+ | `--control-h-lg` | 44px | 38px | 52px |
905
+ | `--control-px-sm` | 10px | 8px | 12px |
906
+ | `--control-px-md` | 14px | 12px | 18px |
907
+ | `--control-px-lg` | 18px | 16px | 22px |
908
+ | `--card-p` | 24px | 16px | 32px |
909
+ | `--row-gap` | 16px | 12px | 20px |
910
+
911
+ ### Motion
912
+
913
+ | Token | Value |
914
+ |---|---|
915
+ | `--duration-instant` | 50ms |
916
+ | `--duration-fast` | 120ms |
917
+ | `--duration-normal` | 200ms |
918
+ | `--duration-slow` | 320ms |
919
+ | `--duration-slower` | 500ms |
920
+ | `--ease-linear` | `linear` |
921
+ | `--ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` |
922
+ | `--ease-in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` |
923
+ | `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` |
924
+
925
+ ### Browsing tokens visually
926
+
927
+ Clone the repository and run the docs app — `/tokens` renders live swatches that react to the `data-*` attribute switches:
928
+
929
+ ```bash
930
+ pnpm install
931
+ pnpm -C packages/ui build # build the lib first
932
+ pnpm -C apps/docs dev
933
+ # open http://localhost:3000/tokens
934
+ ```
935
+
936
+ ---
937
+
938
+ ## Overriding styles
939
+
940
+ Every component emits stable, low-specificity BEM class names. Override them from your own CSS with a single-class selector:
941
+
942
+ ```css
943
+ /* Bump up button padding globally */
944
+ .pds-btn--md {
945
+ padding-inline: 1.25rem;
946
+ }
947
+
948
+ /* Give your app's brand button a different shadow */
949
+ .my-app .pds-btn--primary {
950
+ box-shadow: 0 6px 18px -4px rgba(0, 200, 200, 0.4);
951
+ }
952
+ ```
953
+
954
+ Or pass `className` directly to a component — it's appended after the library's own classes:
955
+
956
+ ```tsx
957
+ <Button className="my-special-button">Action</Button>
958
+ ```
959
+
960
+ ```css
961
+ .my-special-button {
962
+ letter-spacing: 0.05em;
963
+ }
964
+ ```
965
+
966
+ The full BEM block list lives in `dist/styles.css` if you want to grep it; the canonical authoring source is `packages/ui/src/components/*.css` in the repository.
967
+
968
+ ---
969
+
970
+ ## Local development
971
+
972
+ All commands run from the repo root. Package manager is `pnpm@10.33.0` (Node ≥ 20).
973
+
974
+ ```bash
975
+ pnpm install # install all workspaces
976
+
977
+ # Library
978
+ pnpm -C packages/ui typecheck # tsc --noEmit
979
+ pnpm -C packages/ui build # tsup → dist/, then build-css.mjs concatenates styles.css
980
+ pnpm -C packages/ui dev # tsup --watch (CSS changes need a fresh `pnpm -C packages/ui build`)
981
+ pnpm -C packages/ui clean # rm -rf dist
982
+
983
+ # Docs app
984
+ pnpm -C apps/docs typecheck
985
+ pnpm -C apps/docs dev # Next.js 16 Turbopack on :3000
986
+ pnpm -C apps/docs build # next build
987
+ ```
988
+
989
+ There is no test runner configured yet, and no root-level lint. Typecheck is the only static gate.
990
+
991
+ The docs app imports `@bubble-design-system/ui/styles.css` directly from the lib's `dist/`, so any CSS change in the lib needs a fresh `pnpm -C packages/ui build` to show up in the docs dev server. (Component prop / JSX changes hot-reload normally.)
992
+
993
+ ---
994
+
995
+ ## Contributing
996
+
997
+ 1. **Read `PROGRESS.md` first.** It is the source of truth for project state, decision history (with rationale), the next-step task list, and verification commands. Update it after any non-trivial change.
998
+
999
+ 2. **Component authoring rules** (enforced by convention, not lint):
1000
+
1001
+ - Content goes through `children`, not `label`/`title`/`text` props. The exception is semantically distinct slots like `Toast` title + description.
1002
+ - Always `forwardRef` to the Base UI primitive.
1003
+ - Spread `...props` so consumers get every native HTML attribute for free.
1004
+ - `Omit<BaseProps, "className">` then re-add `className?: string` to narrow Base UI's union type.
1005
+ - Variant / size are discriminated string enums with sensible defaults.
1006
+ - `cn("pds-block", `pds-block--${variant}`, `pds-block--${size}`, className)` — user `className` **last**, so it appears after the library's defaults in the output string.
1007
+ - Handle both `disabled:` and `[data-disabled]` (Base UI uses the data-attr form) in the component's CSS file.
1008
+ - Export the component and its `Props` type from `src/index.ts`.
1009
+
1010
+ 3. **CSS authoring rules:**
1011
+
1012
+ - One CSS file per component under `src/components/`. The block name is `pds-<component>` (kebab-case for multi-word components, e.g. `pds-status-pill`).
1013
+ - Use BEM: `.pds-btn`, `.pds-btn__icon`, `.pds-btn--primary`. Avoid descendant or nested selectors that raise specificity above (0, 1, 0).
1014
+ - Reference tokens directly: `background-color: var(--color-bg-brand)`. Never hard-code a value that has a token.
1015
+ - The build pipeline concatenates all component CSS plus `tokens.css` + `base.css` into one shipped `dist/styles.css`.
1016
+
1017
+ 4. **Token rules:**
1018
+
1019
+ - Token names are part of the design spec and round-trip 1:1 with code. Do **not** rename tokens for cosmetic reasons.
1020
+ - Component CSS reads `var(--…)` at use-site so runtime `data-*` switching keeps working. If a rule resolves a token to a literal value, it freezes — never inline a token's resolved value.
1021
+
1022
+ 5. **Stack gotchas:**
1023
+
1024
+ - TypeScript 6 needs `ignoreDeprecations: "6.0"` (already set in `tsconfig.base.json`) because `tsup`'s `rollup-plugin-dts` still uses the deprecated `baseUrl`. Leave the flag until that toolchain catches up.
1025
+ - `@base-ui/react` was renamed from `@base-ui-components/react` at the 1.0 stable release (2025-12-11). Import paths use the new scope: `@base-ui/react/<primitive>`.
1026
+ - The library ships a global `box-sizing: border-box` in `base.css`. Component CSS assumes it.
1027
+
1028
+ 6. **Before opening a PR:**
1029
+
1030
+ - `pnpm -C packages/ui typecheck`
1031
+ - `pnpm -C packages/ui build`
1032
+ - `pnpm -C apps/docs typecheck`
1033
+ - Spot-check the docs gallery and verify toggling `data-theme` / `data-brand` / `data-radius` / `data-density` re-skins live (no rebuild needed). If live switching stops working, the most likely cause is a CSS rule that hard-coded a value instead of referencing a token.
1034
+
1035
+ ---
1036
+
1037
+ ## License
1038
+
1039
+ MIT