@mozaic-ds/vue 2.19.2 → 2.20.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.
Files changed (88) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +512 -296
  3. package/dist/mozaic-vue.js +2005 -1758
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +6 -6
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +5 -2
  8. package/src/components/Catalog.scss +84 -0
  9. package/src/components/Catalog.stories.ts +146 -0
  10. package/src/components/GettingStarted.mdx +258 -2
  11. package/src/components/Migration.mdx +4135 -532
  12. package/src/components/Welcome.mdx +1 -1
  13. package/src/components/carousel/MCarousel.spec.ts +174 -55
  14. package/src/components/carousel/MCarousel.vue +106 -33
  15. package/src/components/checkboxgroup/MCheckboxGroup.vue +2 -9
  16. package/src/components/combobox/MCombobox.spec.ts +144 -0
  17. package/src/components/combobox/MCombobox.stories.ts +2 -1
  18. package/src/components/combobox/MCombobox.vue +68 -38
  19. package/src/components/combobox/README.md +6 -0
  20. package/src/components/datatable/DataTable.stories.ts +2 -2
  21. package/src/components/datatable/DataTableCells.stories.ts +1 -0
  22. package/src/components/datatable/DataTableEmpty.stories.ts +1 -0
  23. package/src/components/datatable/DataTableExpandable.stories.ts +1 -0
  24. package/src/components/datatable/DataTableNested.stories.ts +1 -0
  25. package/src/components/datatable/DataTablePageable.stories.ts +194 -0
  26. package/src/components/datatable/DataTableSelectable.stories.ts +1 -0
  27. package/src/components/datatable/DataTableSortable.stories.ts +1 -0
  28. package/src/components/datepicker/MDatepicker.stories.ts +2 -0
  29. package/src/components/datepicker/MDatepicker.vue +17 -11
  30. package/src/components/divider/README.md +2 -0
  31. package/src/components/drawer/MDrawer.vue +7 -5
  32. package/src/components/link/MLink.stories.ts +18 -0
  33. package/src/components/link/MLink.vue +20 -4
  34. package/src/components/link/README.md +2 -1
  35. package/src/components/optionListbox/MOptionListbox.spec.ts +78 -1
  36. package/src/components/optionListbox/MOptionListbox.types.ts +7 -0
  37. package/src/components/optionListbox/MOptionListbox.vue +31 -217
  38. package/src/components/optionListbox/README.md +2 -0
  39. package/src/components/optionListbox/use-listbox-navigation.composable.ts +147 -0
  40. package/src/components/optionListbox/use-listbox-selection.composable.ts +149 -0
  41. package/src/components/pageheader/MPageHeader.spec.ts +17 -3
  42. package/src/components/pageheader/MPageHeader.stories.ts +37 -41
  43. package/src/components/pageheader/MPageHeader.vue +26 -16
  44. package/src/components/pageheader/README.md +2 -1
  45. package/src/components/pageheadercontext/MPageHeaderContext.spec.ts +155 -0
  46. package/src/components/pageheadercontext/MPageHeaderContext.vue +126 -0
  47. package/src/components/pageheadercontext/README.md +33 -0
  48. package/src/components/pageheaderfeature/MPageHeaderFeature.spec.ts +39 -0
  49. package/src/components/pageheaderfeature/MPageHeaderFeature.vue +40 -0
  50. package/src/components/pageheaderfeature/README.md +17 -0
  51. package/src/components/radiogroup/MRadioGroup.stories.ts +6 -0
  52. package/src/components/radiogroup/MRadioGroup.vue +2 -9
  53. package/src/components/sidebar/MSidebar.const.ts +2 -0
  54. package/src/components/sidebar/MSidebar.stories.ts +33 -0
  55. package/src/components/sidebar/MSidebar.vue +10 -1
  56. package/src/components/sidebar/README.md +1 -0
  57. package/src/components/sidebar/stories/DefaultCase.stories.vue +12 -11
  58. package/src/components/sidebar/use-floating-item.composable.ts +1 -1
  59. package/src/components/sidebarexpandableitem/MSidebarExpandableItem.vue +13 -7
  60. package/src/components/sidebarexpandableitem/README.md +2 -1
  61. package/src/components/sidebarfooter/MSidebarFooter.vue +16 -9
  62. package/src/components/sidebarfooter/_MSidebarFooterMenu.vue +10 -2
  63. package/src/components/sidebarnavitem/MSidebarNavItem.vue +36 -5
  64. package/src/components/sidebarnavitem/README.md +1 -0
  65. package/src/components/sidebarshortcutitem/MSidebarShortcutItem.vue +24 -3
  66. package/src/components/sidebarshortcutitem/README.md +3 -1
  67. package/src/components/sidebarshortcuts/MSidebarShortcuts.vue +6 -1
  68. package/src/components/sidebarshortcuts/README.md +12 -0
  69. package/src/components/statusbadge/MStatusBadge.stories.ts +5 -1
  70. package/src/components/statusbadge/MStatusBadge.vue +7 -1
  71. package/src/components/statusbadge/MstatusBadge.spec.ts +31 -0
  72. package/src/components/statusbadge/README.md +1 -0
  73. package/src/components/statusmessage/MStatusMessage.spec.ts +4 -4
  74. package/src/components/statusmessage/MStatusMessage.vue +5 -10
  75. package/src/components/statusnotification/MStatusNotification.spec.ts +9 -9
  76. package/src/components/statusnotification/MStatusNotification.vue +9 -8
  77. package/src/components/tabs/MTabs.stories.ts +30 -1
  78. package/src/components/tabs/MTabs.vue +12 -3
  79. package/src/components/tabs/Mtabs.spec.ts +19 -0
  80. package/src/components/tabs/README.md +1 -0
  81. package/src/components/tileselectable/MTileSelectable.spec.ts +69 -0
  82. package/src/components/tileselectable/MTileSelectable.stories.ts +8 -2
  83. package/src/components/tileselectable/MTileSelectable.vue +18 -1
  84. package/src/components/tileselectable/README.md +1 -0
  85. package/src/main.ts +3 -0
  86. package/src/utils/debounce.ts +15 -0
  87. package/src/components/BrandPresets.mdx +0 -146
  88. package/src/components/DarkMode.mdx +0 -115
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozaic-ds/vue",
3
- "version": "2.19.2",
3
+ "version": "2.20.0",
4
4
  "type": "module",
5
5
  "description": "Mozaic-Vue is the Vue.js implementation of ADEO Design system",
6
6
  "author": "ADEO - ADEO Design system",
@@ -43,7 +43,7 @@
43
43
  "*.d.ts"
44
44
  ],
45
45
  "dependencies": {
46
- "@mozaic-ds/styles": "^2.23.0",
46
+ "@mozaic-ds/styles": "^2.26.0",
47
47
  "@mozaic-ds/web-fonts": "^1.65.0",
48
48
  "postcss-scss": "^4.0.9"
49
49
  },
@@ -98,6 +98,9 @@
98
98
  "vue-eslint-parser": "^10.1.1",
99
99
  "vue-tsc": "^3.1.8"
100
100
  },
101
+ "overrides": {
102
+ "undici": "^7.28.0"
103
+ },
101
104
  "sideEffects": false,
102
105
  "publishConfig": {
103
106
  "access": "public"
@@ -0,0 +1,84 @@
1
+ #anchor--catalog--default,
2
+ #import,
3
+ #import-1,
4
+ #stories,
5
+ #stories-1,
6
+ .import-description {
7
+ display: none;
8
+ }
9
+
10
+ .catalog-grid {
11
+ display: grid;
12
+ grid-template-columns: repeat(2, minmax(0, 1fr));
13
+ gap: 1rem;
14
+ }
15
+
16
+ @media (max-width: 56rem) {
17
+ .catalog-grid {
18
+ grid-template-columns: 1fr;
19
+ }
20
+ }
21
+
22
+ .catalog-card {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 0.75rem;
26
+ padding: 1rem;
27
+ border: 1px solid var(--color-border-default, #d7d9dd);
28
+ border-radius: 0.75rem;
29
+ background: var(--color-background-default, #fff);
30
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05);
31
+ overflow: hidden;
32
+ contain: layout paint;
33
+ }
34
+
35
+ .catalog-preview {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ height: 12rem;
40
+ padding: 0.75rem;
41
+ border-radius: 0.5rem;
42
+ background: linear-gradient(
43
+ 135deg,
44
+ rgba(242, 244, 248, 0.9),
45
+ rgba(255, 255, 255, 0.95)
46
+ );
47
+ position: relative;
48
+ overflow: hidden;
49
+ isolation: isolate;
50
+ contain: layout paint;
51
+ transform: translateZ(0);
52
+ }
53
+
54
+ .catalog-preview > * {
55
+ max-width: 100%;
56
+ max-height: 100%;
57
+ }
58
+
59
+ .catalog-title {
60
+ margin: 0;
61
+ font-size: 1rem;
62
+ font-weight: 700;
63
+ color: var(--color-text-primary, #111827);
64
+ }
65
+
66
+ .catalog-description {
67
+ margin: 0;
68
+ color: var(--color-text-secondary, #4b5563);
69
+ font-size: 0.95rem;
70
+ line-height: 1.4;
71
+ display: -webkit-box;
72
+ -webkit-line-clamp: 3;
73
+ -webkit-box-orient: vertical;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .catalog-link {
78
+ color: inherit;
79
+ text-decoration: none;
80
+ }
81
+
82
+ .catalog-link:hover {
83
+ text-decoration: underline;
84
+ }
@@ -0,0 +1,146 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { defineComponent } from 'vue';
3
+ import './Catalog.scss';
4
+
5
+ type StoryModule = {
6
+ default?: {
7
+ title?: string;
8
+ args?: Record<string, unknown>;
9
+ parameters?: {
10
+ docs?: {
11
+ description?: {
12
+ component?: string;
13
+ story?: string;
14
+ };
15
+ };
16
+ };
17
+ render?: (...args: unknown[]) => unknown;
18
+ };
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ const storyModules = import.meta.glob('../components/*/*.stories.ts', {
23
+ eager: true,
24
+ }) as Record<string, StoryModule>;
25
+
26
+ const humanizeComponentName = (value: string) =>
27
+ value
28
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
29
+ .replace(/[-_]+/g, ' ')
30
+ .replace(/\b\w/g, (char) => char.toUpperCase())
31
+ .trim();
32
+
33
+ const slugify = (value: string) =>
34
+ value
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, '-')
37
+ .replace(/^-+|-+$/g, '');
38
+
39
+ const getStoryHref = (title: string) => `/?path=/docs/${slugify(title)}--docs`;
40
+
41
+ const catalogStories = Object.entries(storyModules)
42
+ .filter(([, module]) => Boolean(module.default?.title))
43
+ .map(([filePath, module]) => {
44
+ const meta = module.default ?? {};
45
+ const storyName = Object.keys(module).find(
46
+ (name) => name !== 'default' && name !== '__esModule',
47
+ );
48
+ const story = storyName ? (module[storyName] as StoryModule) : undefined;
49
+ const render =
50
+ typeof story?.render === 'function'
51
+ ? story.render
52
+ : typeof meta.render === 'function'
53
+ ? meta.render
54
+ : undefined;
55
+ const args = {
56
+ ...(meta.args ?? {}),
57
+ ...(story?.args ?? {}),
58
+ };
59
+ const componentName =
60
+ filePath.split('/components/')[1]?.split('/')[0] ??
61
+ meta.title ??
62
+ 'Component';
63
+ const rawTitle =
64
+ (meta.title ?? componentName).split('/').pop() ?? componentName;
65
+
66
+ return {
67
+ filePath,
68
+ title: humanizeComponentName(rawTitle),
69
+ description:
70
+ meta.parameters?.docs?.description?.component ??
71
+ meta.parameters?.docs?.description?.story ??
72
+ 'Component documentation and examples.',
73
+ storyHref: getStoryHref(meta.title ?? componentName),
74
+ previewComponent:
75
+ render && typeof render === 'function'
76
+ ? defineComponent(render(args as never))
77
+ : undefined,
78
+ };
79
+ })
80
+ .sort((left, right) => left.title.localeCompare(right.title));
81
+
82
+ const meta: Meta = {
83
+ title: 'Catalog',
84
+ parameters: {
85
+ controls: {
86
+ disable: true,
87
+ },
88
+ docs: {
89
+ controls: {
90
+ disable: true,
91
+ },
92
+ description: {
93
+ component: 'A live catalog of the library components.',
94
+ },
95
+ toc: false,
96
+ canvas: { sourceState: 'none' },
97
+ },
98
+ },
99
+ };
100
+
101
+ export default meta;
102
+
103
+ type Story = StoryObj;
104
+
105
+ export const Default: Story = {
106
+ render: () => ({
107
+ setup() {
108
+ return { catalogStories };
109
+ },
110
+ template: `
111
+ <section
112
+ class="catalog-grid"
113
+ >
114
+ <article
115
+ v-for="entry in catalogStories"
116
+ :key="entry.filePath"
117
+ class="catalog-card"
118
+ >
119
+ <div
120
+ class="catalog-preview"
121
+ >
122
+ <component :is="entry.previewComponent" v-if="entry.previewComponent" />
123
+ <span v-else>{{ entry.title }}</span>
124
+ </div>
125
+ <h3
126
+ class="catalog-title"
127
+ >
128
+ <a class="catalog-link" :href="entry.storyHref">{{ entry.title }}</a>
129
+ </h3>
130
+ <p
131
+ class="catalog-description"
132
+ >
133
+ {{ entry.description }}
134
+ </p>
135
+ </article>
136
+ </section>
137
+ `,
138
+ }),
139
+ parameters: {
140
+ docs: {
141
+ canvas: {
142
+ withToolbar: false,
143
+ },
144
+ },
145
+ },
146
+ };
@@ -6,7 +6,7 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';
6
6
 
7
7
  **Mozaic-Vue** is the [Vue.js](https://vuejs.org/) implementation of ADEO Design system.
8
8
 
9
- ## 📦 Installation
9
+ ### 📦 Installation
10
10
 
11
11
  In order to use **Mozaic-Vue** in your **Vue.js** project, you must first install the [npm package](https://www.npmjs.com/package/@mozaic-ds/vue):
12
12
 
@@ -16,7 +16,7 @@ Or with **Yarn**:
16
16
 
17
17
  <Source language="bash" dark code="yarn add @mozaic-ds/vue" />
18
18
 
19
- ## 📝 Usage
19
+ ### 📝 Usage
20
20
 
21
21
  Import the styles
22
22
 
@@ -43,3 +43,259 @@ To import and use a component, you can proceed as follows:
43
43
  <MButton>Button Label</MButton>
44
44
  </template>
45
45
  `} />
46
+
47
+ ### 🎨 Brand Presets
48
+
49
+ **Mozaic** is a **multi-brand** design system.<br/>
50
+ This means that it is fully customisable so that its constituent elements _(foundations, components, etc.)_ can be adapted to the graphic charter of the brand that uses it.
51
+
52
+ To simplify this aspect of customising **Mozaic** for you, we have created themes _(which we also call **"presets"**)_ ready to use according to your context of use.
53
+
54
+ Currently **Mozaic** can be customized with the following presets:
55
+
56
+ - Preset **Leroy Merlin**: this is the default preset/theme configured when **Mozaic** was first installed
57
+ - Preset **Adeo**: dedicated to the **Adeo Group's** internal interfaces and products
58
+ - Preset **Bricoman**: dedicated to the interfaces and products of the **Bricoman** brand
59
+
60
+ The rest of this documentation shows you how to use/install the Adeo preset in your project.
61
+
62
+ > Note that the procedure remains the same for all other presets.<br/> You just need to replace all references to "Adeo" with the brand name of your choice.
63
+
64
+ #### Using the Adeo's preset
65
+
66
+ Before anything else, make sure you have followed the **Mozaic-Vue** installation procedure as described in the [Getting Started](?path=/docs/getting-started--docs) page.
67
+
68
+ Once **Mozaic-Vue** is installed as shown, we can make the following changes:
69
+
70
+ #### Loading preset
71
+
72
+ All it has to do is insert the following code into its main Sass file (entrypoint stylesheet):
73
+
74
+ <Source
75
+ language="css"
76
+ dark
77
+ code={`
78
+ // Entrypoint stylesheet
79
+ @use "@mozaic-ds/tokens/<presetName>/theme" as *;
80
+ `}
81
+ />
82
+
83
+ > [!NOTE]
84
+ > The `<presetName>` string should be replaced by the name of the preset you want, one of the following values: `adeo | mbrand`.
85
+ > As the `leroymerlin` preset is the default preset, you don't need to use this syntax to use it.
86
+
87
+ For example, for ADEO
88
+
89
+ <Source
90
+ language="css"
91
+ dark
92
+ code={`
93
+ // Entrypoint stylesheet
94
+ @use "@mozaic-ds/tokens/adeo/theme" as *;
95
+ `}
96
+ />
97
+
98
+ #### Font by brand
99
+
100
+ Each brand is distinguished not only by different styles but also by a different font.
101
+
102
+ The table below summarises which font to use depending on the brand.
103
+
104
+ <table>
105
+ <tr>
106
+ <td>Brand</td>
107
+ <td>Font</td>
108
+ </tr>
109
+ <tr>
110
+ <td>Leroy Merlin</td>
111
+ <td>
112
+ [LeroyMerlinSans](https://mozaic.adeo.cloud/foundations/typography/font-families/)
113
+ </td>
114
+ </tr>
115
+ <tr>
116
+ <td>Adeo</td>
117
+ <td>[Roboto](https://fonts.google.com/specimen/Roboto)</td>
118
+ </tr>
119
+ <tr>
120
+ <td>Bricoman</td>
121
+ <td>[Inter](https://fonts.google.com/specimen/Inter)</td>
122
+ </tr>
123
+ </table>
124
+
125
+ For example, here is how to include the Roboto font in your HTML for the Adeo brand:
126
+
127
+ <Source
128
+ language="html"
129
+ dark
130
+ code={`
131
+ <head>
132
+ <meta charset="UTF-8" />
133
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
134
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
135
+ <link
136
+ href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
137
+ rel="stylesheet"
138
+ />
139
+ </head>
140
+ `}
141
+ />
142
+
143
+ From there, we can update the main style sheet of your project, in order to import the right font.
144
+
145
+ <Source
146
+ language='css'
147
+ dark
148
+ code={`
149
+ @use '@mozaic-ds/tokens/adeo/theme' as *;
150
+
151
+ body {
152
+ font-family: var(--font-family, 'Roboto', sans-serif);
153
+ }
154
+ `} />
155
+
156
+ Specific for LM fonts
157
+
158
+ > This section describes the use of fonts for the **Leroy Merlin** preset, but if you are using another preset, please see [the associated documentation](?path=/docs/using-presets--docs).
159
+
160
+ The theme defined by default when **Mozaic-Vue** is first installed/used is the theme corresponding to the **Leroy Merlin** charter.
161
+
162
+ For any use of the **Leroy Merlin** theme, it is recommended to use the font [LeroyMerlinSans](/typography/font-families/) which has been specially designed for **Leroy Merlin** products.
163
+
164
+ To use the `LeroyMerlinSans` font in your project, you must copy it into your project so that it is present in your `assets`.
165
+
166
+ You can do this as follows:
167
+
168
+ 1. Create a folder in your project where you can save the fonts, for example: `static/assets/fonts`
169
+
170
+ 2. Copy fonts from Mozaic dependencies:
171
+
172
+ <Source
173
+ language="bash"
174
+ dark
175
+ code="cp node_modules/@mozaic-ds/web-fonts/*.{woff,woff2} static/assets/fonts"
176
+ />
177
+
178
+ #### Usage
179
+
180
+ At this stage, the integration of the **Mozaic-Vue** components should normally be customised with the preset values.
181
+
182
+ All that remains is to use the components as usual:
183
+
184
+ <Source
185
+ language="html"
186
+ dark
187
+ code='<MButton label="This is a Mozaic Button" />'
188
+ />
189
+
190
+ ### 🌑 Dark Mode
191
+
192
+ A concise guide explaining **how dark mode works** with your CSS variables and **how to use it** in Storybook.
193
+
194
+ ---
195
+
196
+ ## What dark mode is (high‑level)
197
+
198
+ Dark mode is implemented with **two sets of CSS variables** (tokens):
199
+
200
+ - **Light** values live under `:root`.
201
+ - **Dark** values override under `:root[data-theme="dark"]`.
202
+
203
+ Components only reference tokens with `var(--token-name)` — switching theme is just toggling the `data-theme` attribute (no component code changes).
204
+
205
+ ---
206
+
207
+ #### Token structure (SCSS → CSS)
208
+
209
+ Your presets export SCSS like this:
210
+
211
+ <Source
212
+ language="scss"
213
+ dark
214
+ code={`
215
+ $root-selector: ':root' !default;
216
+ $dark-selector: '[data-theme="dark"]' !default;
217
+
218
+ #{$root-selector} {
219
+ /_ Light tokens _/
220
+ --color-background-primary: #ffffff;
221
+ --color-text-primary: #000000;
222
+ /_ … all your light variables … _/
223
+ }
224
+
225
+ #{$root-selector}#{$dark-selector} {
226
+ /_ Dark tokens _/
227
+ --color-background-primary: #191919;
228
+ --color-text-primary: #d9d9d9;
229
+ /_ … all your dark variables … _/
230
+ }
231
+ `}
232
+ />
233
+
234
+ After compilation, this becomes standard CSS:
235
+
236
+ <Source
237
+ language="css"
238
+ dark
239
+ code={`
240
+ :root {
241
+ /* light tokens */
242
+ }
243
+ :root[data-theme='dark'] {
244
+ /* dark tokens */
245
+ }
246
+ `}
247
+ />
248
+
249
+ > If you can’t (or don’t want to) target `:root`, you can pass a different `$root-selector` when building your theme and apply `data-theme="dark"` on that container instead.
250
+
251
+ ---
252
+
253
+ #### Using tokens inside components
254
+
255
+ To enable the dark mode you have to ensure to:
256
+
257
+ - Add the `data-theme` attribute in your root element with the value `dark`,
258
+ - Use variables — never hard‑code colors or sizes
259
+
260
+ ```html
261
+ <div class="root" data-theme="dark">…</div>
262
+ ```
263
+
264
+ <Source
265
+ language="sass"
266
+ dark
267
+ code={`
268
+ @use "@mozaic-ds/tokens" as *;
269
+
270
+ .mc-component: {
271
+ background-color: $--color-background-primary;
272
+ }
273
+
274
+ `}
275
+ />
276
+
277
+ When the theme changes, these values update automatically via CSS.
278
+
279
+ ---
280
+
281
+ #### Accessibility & good practices
282
+
283
+ - Aim for **WCAG AA** contrast at minimum; verify text vs. background pairs.
284
+ - Prefer **semantic tokens** (`--button-color-…`, `--color-text-…`) over raw color hexes.
285
+ - Keep all component styles expressed in tokens so the **theme switch has zero component logic**.
286
+
287
+ ---
288
+
289
+ #### Troubleshooting
290
+
291
+ - **Dark toggle does nothing** → Ensure the tokens were imported **before** component styles and that `data-theme="dark"` is set on the same selector the tokens target (usually `:root`).
292
+ - **Weird colors** → Search for hard‑coded values and replace them with tokens.
293
+ - **Variables undefined** → Check your build order and that the SCSS was compiled to CSS and loaded by Storybook.
294
+
295
+ ---
296
+
297
+ #### Summary
298
+
299
+ - Light tokens on `:root`, dark overrides on `:root[data-theme="dark"]`.
300
+ - Components read tokens with `var(--$token-name)` — no runtime branching required.
301
+