eidotter 0.40.2 → 0.41.1

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 (104) hide show
  1. package/README.md +14 -6
  2. package/dist/components/Accordion/components/AccordionFill.js +1 -1
  3. package/dist/components/Accordion/components/Section.js +1 -1
  4. package/dist/components/Alert/components/Alert.js +3 -2
  5. package/dist/components/Brand/components/Logo.d.ts +7 -7
  6. package/dist/components/Brand/components/Logo.js +10 -15
  7. package/dist/components/Brand/components/LogoGeometry.d.ts +28 -0
  8. package/dist/components/Brand/components/LogoGeometry.js +78 -0
  9. package/dist/components/Brand/components/Mark.d.ts +13 -6
  10. package/dist/components/Brand/components/Mark.js +14 -10
  11. package/dist/components/Brand/components/Wordmark.js +1 -1
  12. package/dist/components/Brand/components/markGeometry.d.ts +15 -0
  13. package/dist/components/Brand/components/markGeometry.js +20 -7
  14. package/dist/components/Breadcrumb/components/Breadcrumb.js +2 -2
  15. package/dist/components/Button/components/Button.js +1 -1
  16. package/dist/components/Card/components/Card.d.ts +5 -0
  17. package/dist/components/Card/components/Card.js +8 -2
  18. package/dist/components/CmdPalette/components/CmdPalette.d.ts +4 -3
  19. package/dist/components/CmdPalette/components/CmdPalette.js +17 -12
  20. package/dist/components/DecayStrip/components/DecayStrip.js +1 -1
  21. package/dist/components/DosDialog/components/DosDialog.d.ts +90 -0
  22. package/dist/components/DosDialog/components/DosDialog.js +68 -0
  23. package/dist/components/DosDialog/components/index.d.ts +2 -0
  24. package/dist/components/DosDialog/components/index.js +1 -0
  25. package/dist/components/DosDialog/index.d.ts +2 -0
  26. package/dist/components/DosDialog/index.js +1 -0
  27. package/dist/components/DosFigure/components/DosFigure.d.ts +2 -2
  28. package/dist/components/DosFigure/components/DosFigure.js +14 -7
  29. package/dist/components/DosWindow/components/DosWindow.d.ts +39 -0
  30. package/dist/components/DosWindow/components/DosWindow.js +23 -0
  31. package/dist/components/DosWindow/components/index.d.ts +2 -0
  32. package/dist/components/DosWindow/components/index.js +1 -0
  33. package/dist/components/DosWindow/index.d.ts +2 -0
  34. package/dist/components/DosWindow/index.js +1 -0
  35. package/dist/components/FilterBar/components/FilterBar.d.ts +7 -4
  36. package/dist/components/Footer/components/Footer.js +1 -1
  37. package/dist/components/Gauge/components/Gauge.js +13 -2
  38. package/dist/components/Header/components/Header.d.ts +29 -1
  39. package/dist/components/Header/components/Header.js +20 -5
  40. package/dist/components/Icon/components/Icon.d.ts +9 -0
  41. package/dist/components/Icon/components/Icon.js +7 -3
  42. package/dist/components/IconButton/components/IconButton.d.ts +44 -0
  43. package/dist/components/IconButton/components/IconButton.js +19 -0
  44. package/dist/components/IconButton/components/index.d.ts +2 -0
  45. package/dist/components/IconButton/components/index.js +1 -0
  46. package/dist/components/IconButton/index.d.ts +2 -0
  47. package/dist/components/IconButton/index.js +1 -0
  48. package/dist/components/Input/components/Input.js +1 -1
  49. package/dist/components/Lightbox/components/Lightbox.d.ts +10 -4
  50. package/dist/components/Lightbox/components/Lightbox.js +19 -11
  51. package/dist/components/Meter/components/Meter.js +7 -3
  52. package/dist/components/Meter/components/MeterGroup.js +3 -3
  53. package/dist/components/Modal/components/Modal.d.ts +18 -6
  54. package/dist/components/Modal/components/Modal.js +18 -9
  55. package/dist/components/Nav/components/Nav.js +5 -4
  56. package/dist/components/Notification/components/Notification.js +3 -2
  57. package/dist/components/Progress/components/Progress.js +8 -6
  58. package/dist/components/RangeBar/components/RangeBar.js +8 -5
  59. package/dist/components/SeriesBars/components/SeriesBars.d.ts +9 -1
  60. package/dist/components/SeriesBars/components/SeriesBars.js +23 -11
  61. package/dist/components/Stat/components/Stat.js +1 -1
  62. package/dist/components/Tag/components/Tag.d.ts +8 -1
  63. package/dist/components/Tag/components/Tag.js +5 -5
  64. package/dist/components/VitalsStrip/components/VitalsStrip.js +9 -7
  65. package/dist/components/registry.js +230 -19
  66. package/dist/eidotter.css +3 -2
  67. package/dist/hooks/useAnimatedDismiss.js +5 -0
  68. package/dist/index.d.ts +9 -1
  69. package/dist/index.es.js +2414 -1747
  70. package/dist/index.umd.js +1 -1
  71. package/dist/utils/cn.d.ts +2 -0
  72. package/dist/utils/cn.js +33 -1
  73. package/dist/utils/dosFontSizes.d.ts +2 -0
  74. package/dist/utils/dosFontSizes.js +17 -0
  75. package/llms.txt +15 -7
  76. package/migrations/0.41.0.md +883 -0
  77. package/migrations/README.md +46 -0
  78. package/package.json +11 -3
  79. package/public/favicon.svg +1 -1
  80. package/src/styles/dos-utilities.css +9 -1
  81. package/src/styles/shadcn.css +63 -27
  82. package/src/styles/theme.amber-mono.css +4 -2
  83. package/src/styles/theme.amber-paper.css +106 -0
  84. package/src/styles/theme.cga-amber.css +3 -1
  85. package/src/styles/theme.cga-mode4-p0.css +3 -1
  86. package/src/styles/theme.cga-mode4-p1.css +3 -1
  87. package/src/styles/theme.cga-mode5.css +3 -1
  88. package/src/styles/theme.light.css +3 -1
  89. package/src/styles/tokens.css +5 -1
  90. package/tailwind.preset.cjs +23 -1
  91. package/dist/components/Brand/components/markGeometry.test.d.ts +0 -1
  92. package/dist/components/Connector/components/connectorGeometry.test.d.ts +0 -1
  93. package/dist/components/DecayStrip/components/resample.test.d.ts +0 -1
  94. package/dist/components/TimelineContainer/components/timelineUtils.test.d.ts +0 -1
  95. package/dist/components/TimelineContainer/components/useDrillDown.test.d.ts +0 -1
  96. package/dist/components/TimelineContainer/components/useSelection.test.d.ts +0 -1
  97. package/dist/components/TimelineContainer/components/useZoom.test.d.ts +0 -1
  98. package/dist/hooks/useTextScramble.test.d.ts +0 -1
  99. package/dist/hooks/useThemePortal.test.d.ts +0 -1
  100. package/dist/utils/health/formatValue.test.d.ts +0 -1
  101. package/dist/utils/health/healthPatterns.test.d.ts +0 -1
  102. package/dist/utils/health/resolveBand.test.d.ts +0 -1
  103. package/dist/utils/isSafeHref.test.d.ts +0 -1
  104. package/dist/utils/isSafeUrl.test.d.ts +0 -1
@@ -0,0 +1,883 @@
1
+ # Upgrading to eidotter 0.41.0
2
+
3
+ This guide is for developers and AI agents working in an app that consumes the
4
+ `eidotter` npm package. It assumes you have not seen the eidotter repository.
5
+ Everything here was checked against the 0.41.0 source and the published
6
+ `dist/` output. Linear ids (DMNC-xxxx) are references only; you do not need
7
+ access to them.
8
+
9
+ You are reading this from `node_modules/eidotter/migrations/0.41.0.md`, or from
10
+ the repository's `migrations/` folder. If you are upgrading across several
11
+ minors, read [`README.md`](./README.md) in this folder first.
12
+
13
+ > **Install 0.41.1 or later.** 0.41.0 was published before the release review fixes. It lacks the eidotter token utilities that section 2 promises, its reset still cancels Tailwind v4 utilities, and `Stat` loses its colours. 0.41.1 fixes all of this with no API change, and everything in this guide applies to it.
14
+
15
+ ## Who must act
16
+
17
+ Every consumer. 0.41.0 is a 0.x **minor**, so it carries visible changes, and a
18
+ caret range on 0.40 does not pick it up.
19
+
20
+ In three lines:
21
+
22
+ 1. Body text has a 16px floor (Tailwind preset `text-xs`/`text-sm` are now
23
+ 1rem), `eidotter/styles` stops leaking its CSS reset (fonts, link colours)
24
+ and its unused utilities into your app, and `text-secondary` is now a
25
+ visible tone, not near-black.
26
+ 2. `Header` shows the logo lockup by default with uppercase nav, and Modal,
27
+ Lightbox, CmdPalette, Card, DosFigure, Alert, Notification, Logo and Mark
28
+ look different. Some overlay CSS classes are gone.
29
+ 3. New shared pieces replace hand-rolled code: `IconButton`, `DosDialog`,
30
+ `DosWindow`, a `Header` `end` slot, the `amber-paper` theme and the
31
+ `eidotter/favicon.svg` asset.
32
+
33
+ No public prop was removed or renamed. A diff of every published `.d.ts` in
34
+ `dist/components` between 0.40.2 and 0.41.0 shows only additions. What can
35
+ break is CSS: your overrides, your reliance on leaked global styles, and
36
+ utility classes you used without having Tailwind yourself.
37
+
38
+ ### Which sections apply to your stack
39
+
40
+ | Your stack | Read especially |
41
+ |---|---|
42
+ | Tailwind 3 or 4 with `eidotter/tailwind.preset` | 1 (16px floor), 3, 5, 6, 7 |
43
+ | Tailwind 4 without the preset | 1 (component sizes only), 3, 5, 6, 7 |
44
+ | No Tailwind (plain CSS, Astro, Vite React) | 2 (utilities removed), 4 (reset no longer leaks), 3, 5, 6, 7 |
45
+ | Next.js App Router (server components) | all of the above plus "RSC deep imports" |
46
+
47
+ ## How to bump
48
+
49
+ 0.x caret ranges stop at the next minor: `^0.40.2` means `>=0.40.2 <0.41.0`.
50
+ Older ranges such as `^0.38.0`, `^0.37.0` or `^0.33.0` are the same: none of
51
+ them reach 0.41.0. Change the range explicitly:
52
+
53
+ ```bash
54
+ npm install eidotter@^0.41.1
55
+ # check what actually resolved
56
+ npm ls eidotter
57
+ ```
58
+
59
+ Install from the npm registry. Do not point the dependency at a local checkout
60
+ (`file:../eidotter`): `npm ci` passes, but CI, Docker and Vercel builds fail
61
+ with "Module not found".
62
+
63
+ Before you bump, save a copy of the old bundle. You need it for the class diff
64
+ in section 2:
65
+
66
+ ```bash
67
+ cp node_modules/eidotter/dist/eidotter.css /tmp/eidotter-old.css
68
+ ```
69
+
70
+ If you are coming from older than 0.40, also read
71
+ "Jumping from older versions" at the end.
72
+
73
+ ---
74
+
75
+ ## Breaking and visible changes
76
+
77
+ Each item is: what changed, how to find it in your code, what to do, how to
78
+ verify. Patterns use `rg` (ripgrep). If you only have grep, `grep -rnE` with the
79
+ same pattern works. Adjust the search roots (`src app components pages`) to
80
+ your repo.
81
+
82
+ ### 1. 16px body-size floor; preset `text-xs` / `text-sm` are 1rem (DMNC-1579)
83
+
84
+ **What changed.** The Tailwind preset (`eidotter/tailwind.preset`) now maps
85
+ Tailwind's `text-xs` and `text-sm` to `1rem` with a `1.25rem` line height. A
86
+ new token `--typography-font-size-text-base` (1rem) and utility
87
+ `text-dos-text-base` exist. Inside eidotter, `Breadcrumb` (was 12px with 0.35em
88
+ tracking, now 16px with 0.1em), `Footer` (was 14px), `Input` label, description
89
+ and error text (were 12px) and the `Header` brand link render at 16px.
90
+ `Progress` sizes `sm`/`md`/`lg` render 16/20/22px.
91
+
92
+ The preset alias only affects apps that load the preset. The bundle
93
+ (`dist/eidotter.css`) no longer ships any stock `.text-xs` … `.text-9xl` rule,
94
+ so it no longer overrides your own Tailwind sizes (0.40 shipped an unlayered
95
+ `.text-xs{font-size:1rem}` that did).
96
+
97
+ **Find it.**
98
+
99
+ ```bash
100
+ # Tailwind classes that will now render at 16px (preset users)
101
+ rg -n '\btext-(xs|sm)\b' src app components pages
102
+ # overrides you added to bump eidotter components up to body size
103
+ rg -n -A4 '\.eidotter-(breadcrumb|footer|input|header__branding)' --glob '*.{css,scss}'
104
+ rg -n 'text-base' src app components | rg -i 'breadcrumb|footer|input|header'
105
+ ```
106
+
107
+ **What to do.**
108
+
109
+ - Preset users: expect `text-xs`/`text-sm` text to grow to 16px. The floor is
110
+ deliberate (the DOS VGA cell is 9×16). Where a label now overflows on mobile,
111
+ shorten the label; do not widen its container.
112
+ - Delete per-element `font-size` / `text-base` overrides you added to
113
+ Breadcrumb, Footer, Input or the Header brand. They are now redundant.
114
+ - Prefer `text-dos-text-base` over `text-base` for body-size text in new code.
115
+
116
+ ```tsx
117
+ // before (0.40): forcing eidotter's small Breadcrumb up to body size
118
+ <div className="[&_.eidotter-breadcrumb]:text-base"><Breadcrumb items={crumbs} /></div>
119
+ // after (0.41): the component is already 16px
120
+ <Breadcrumb items={crumbs} />
121
+ ```
122
+
123
+ **Verify.** In the browser,
124
+ `getComputedStyle(document.querySelector('.eidotter-breadcrumb')).fontSize`
125
+ is `"16px"`. Check that no label wraps or overflows at 390px width.
126
+
127
+ ### 2. Utilities removed from `dist/eidotter.css` (consumers without their own Tailwind)
128
+
129
+ **What changed.** The bundle used to contain every stock Tailwind utility
130
+ that appeared anywhere in eidotter's docs and stories. It now contains only the
131
+ stock utilities used by shipped component source. Apps that used those generic
132
+ classes in their own markup without running Tailwind themselves lose them. Apps
133
+ that run their own Tailwind are not affected, because their build generates the
134
+ classes.
135
+
136
+ eidotter's own token utilities stay available without Tailwind:
137
+ `text-dos-*`, `bg-dos-*`, `border-dos-*`, `shadow-dos-*`, `font-dos-*`
138
+ (including the `font-dos-bold` / `-semibold` / `-regular` aliases), `bg-cga-*`
139
+ and `text-cga-*`.
140
+
141
+ Examples of generic utilities that are gone:
142
+
143
+ - Layout and spacing: `mt-4`, `p-3`, `space-y-2`, `grid-cols-1`,
144
+ `grid-cols-2`, `md:grid-cols-2`, `md:grid-cols-4`, `lg:grid-cols-3`,
145
+ `col-span-2`, `max-w-3xl`, `max-w-5xl`, `min-h-screen`, `flex-grow`,
146
+ `flex-shrink`, `float-left`, `float-right`, `contents`, `isolate`,
147
+ `inline-grid`, `print:hidden`.
148
+ - Borders and radius: `border-t`, `border-r`, `border-l`, `border-x`,
149
+ `border-y`, `border-transparent`, `divide-x`, `divide-y`, `rounded`,
150
+ `rounded-t`, `rounded-b`, `rounded-l`, `rounded-r`.
151
+ - Typography: the stock sizes `text-xs`, `text-sm`, `text-base`, `text-lg`,
152
+ `text-xl`, plus `italic`, `capitalize`, `lowercase`, `antialiased`,
153
+ `text-wrap`.
154
+
155
+ That list is not exhaustive. Compute the exact diff for your installed versions.
156
+
157
+ **Find it.** After the bump, compare the saved old bundle with the new one and
158
+ search your code for each removed class:
159
+
160
+ ```bash
161
+ node -e '
162
+ const fs = require("fs");
163
+ const classes = (f) => new Set([...fs.readFileSync(f, "utf8")
164
+ .matchAll(/\.(-?[A-Za-z_](?:\\.|[A-Za-z0-9_-])*)/g)].map((m) => m[1].replace(/\\/g, "")));
165
+ const before = classes("/tmp/eidotter-old.css");
166
+ const after = classes("node_modules/eidotter/dist/eidotter.css");
167
+ console.log([...before].filter((c) => !after.has(c)).sort().join("\n"));
168
+ ' > /tmp/eidotter-removed-classes.txt
169
+ wc -l /tmp/eidotter-removed-classes.txt
170
+ rg -n -w -F -f /tmp/eidotter-removed-classes.txt src app components pages \
171
+ --glob '*.{tsx,jsx,ts,js,astro,html,mdx,vue,svelte}'
172
+ ```
173
+
174
+ Review the hits by hand. A `-w` match on a short word such as `contents` or
175
+ `rounded` can be a false positive in prose. Ignore any eidotter token utility
176
+ (`*-dos-*`, `*-cga-*`) the diff lists; those remain supported.
177
+
178
+ **What to do.** For each real hit, pick one:
179
+
180
+ - Replace the utility with a semantic class or a plain CSS rule in your own
181
+ stylesheet using eidotter tokens:
182
+
183
+ ```css
184
+ /* before: <p class="grid grid-cols-2 mt-4"> relied on eidotter's bundle */
185
+ /* after: your own CSS */
186
+ .intro { display: grid; grid-template-columns: 1fr 1fr; margin-top: var(--spacing-4); } /* 16px */
187
+ ```
188
+
189
+ All token names are in `node_modules/eidotter/src/styles/tokens.css`.
190
+ - For display or body typography in raw HTML, the `.dos-*` classes from
191
+ `import 'eidotter/utilities'` (`.dos-body`, `.dos-caption`, `.dos-h1` …
192
+ `.dos-h5`, `.dos-label`, `.dos-code`) are another option.
193
+ - Or add Tailwind to the app and load `eidotter/tailwind.preset`, which
194
+ generates any of these classes on demand.
195
+
196
+ **Verify.** Rerun the `rg` command: no hits left that you did not deliberately
197
+ keep. Visually check the pages that had hits.
198
+
199
+ ### 3. `text-secondary` changed meaning; use `text-on-accent` / `text-on-brand` / `text-on-light` (DMNC-1494)
200
+
201
+ **What changed.** `--color-semantic-text-secondary` (Tailwind
202
+ `text-dos-text-secondary`) used to resolve to the darkest CGA value: the same
203
+ near-black as the page background. That made it correct only for dark text on
204
+ an amber or light fill, and invisible everywhere else. It now resolves to a
205
+ visible, de-emphasised tone (the `light-gray` primitive on dark themes; warm
206
+ tan `#b87c1a` under amber-mono; near-black under the light theme). Three new
207
+ tokens name text-on-fill explicitly:
208
+
209
+ | Token | Tailwind | Use for |
210
+ |---|---|---|
211
+ | `--color-semantic-text-on-accent` | `text-dos-text-on-accent` | text/icons on a `background-accent` fill (eidotter uses it for the Button primary label, Badge accent, Tag filled, Tabs pills, FilterBar active, Switch thumb, Accordion header, Terminal title bar). This is the old `text-secondary` value. |
212
+ | `--color-semantic-text-on-brand` | `text-dos-text-on-brand` | text/icons on a brand-amber fill (`background-brand`; eidotter uses it for InlineLink hover/active and the IconButton hover glyph) |
213
+ | `--color-semantic-text-on-light` | `text-dos-text-on-light` | an unfilled element sitting on a light surface; near-black in every theme (eidotter uses it for `Nav` `modern` links) |
214
+
215
+ `text-on-accent` and `text-on-brand` are identical under amber-mono. They
216
+ differ in `cga-amber`, where the accent fill is blue and on-accent text is
217
+ light. So pick by the fill underneath, not by the colour you see today.
218
+
219
+ **Find it.**
220
+
221
+ ```bash
222
+ rg -n 'text-dos-text-secondary|color-semantic-text-secondary|border-dos-text-secondary' \
223
+ src app components pages styles --glob '!node_modules'
224
+ ```
225
+
226
+ **What to do.** Look at what each hit sits on.
227
+
228
+ - Dark text on an accent fill (`bg-dos-bg-accent`): switch to `text-on-accent`.
229
+ On a brand fill (`bg-dos-bg-brand`): `text-on-brand`. Unfilled text on a
230
+ light surface: `text-on-light`.
231
+
232
+ ```tsx
233
+ // before
234
+ <span className="bg-dos-bg-accent text-dos-text-secondary">NEW</span>
235
+ // after
236
+ <span className="bg-dos-bg-accent text-dos-text-on-accent">NEW</span>
237
+ ```
238
+
239
+ ```css
240
+ /* before */ .chip--active { color: var(--color-semantic-text-secondary); }
241
+ /* after */ .chip--active { color: var(--color-semantic-text-on-accent); }
242
+ ```
243
+
244
+ - De-emphasised text on a dark background: leave it. It is now visible, as you
245
+ intended.
246
+
247
+ **Verify.** Every former hit renders legibly in the default theme and in any
248
+ theme you offer (`light`, `amber-paper`). Check colours with
249
+ `getComputedStyle(el).color`; screenshots taken under WSL/Playwright can shift
250
+ amber hues.
251
+
252
+ ### 4. `eidotter/styles` no longer leaks its CSS reset (fonts, link colours) (DMNC-1434)
253
+
254
+ **What changed.** Up to 0.40, `import 'eidotter/styles'` carried Tailwind's
255
+ preflight unlayered. Two parts of it overrode your app's styles whatever their
256
+ specificity: `a { color: inherit; text-decoration: inherit }` turned every link
257
+ the surrounding text colour, and `html, :host { font-family: … }` plus
258
+ Tailwind's default `--font-sans` / `--font-mono` / `--default-font-family` /
259
+ `--default-mono-font-family` variables set a system sans-serif and beat a
260
+ Tailwind v4 `@theme` font. 0.41.0 strips those. eidotter components style their
261
+ own fonts and anchors and render unchanged.
262
+
263
+ Who sees a difference: apps **without a preflight of their own** (no Tailwind)
264
+ that set no font on `html`/`body`. Bare text now shows the browser default font
265
+ (usually a serif), and plain `<a>` outside eidotter components shows the browser
266
+ link colour (blue/purple, underlined). Apps with their own Tailwind keep their
267
+ own preflight, which already resets links; for them the change only removes
268
+ the interference.
269
+
270
+ **Find it.**
271
+
272
+ ```bash
273
+ # do you set a font and link styles yourself?
274
+ rg -n 'font-family' --glob '*.{css,scss,astro}' src app styles
275
+ rg -n '(^|[\s,}])a\s*(,|\{|:)' --glob '*.{css,scss}' src app styles
276
+ ```
277
+
278
+ Then open the app with the new version and look for serif body text or blue
279
+ links.
280
+
281
+ **What to do.** Set what you relied on in your own global stylesheet:
282
+
283
+ ```css
284
+ /* global.css — loaded after eidotter/styles */
285
+ html {
286
+ background: var(--color-semantic-background-primary);
287
+ color: var(--color-semantic-text-primary);
288
+ font-family: var(--typography-font-family-body); /* JetBrains Mono Nerd Font */
289
+ /* or var(--typography-font-family-primary) for Perfect DOS VGA 437 everywhere */
290
+ }
291
+ a { color: var(--color-semantic-text-accent); }
292
+ ```
293
+
294
+ If you want the old "links look like the text around them" behaviour, add
295
+ `a { color: inherit; text-decoration: inherit; }` yourself. With Tailwind v4,
296
+ put such rules in `@layer base { … }` so your utilities still win.
297
+
298
+ **Verify.** `getComputedStyle(document.body).fontFamily` names your intended
299
+ font. A bare link outside eidotter components is not `rgb(0, 0, 238)` (the
300
+ browser default blue), unless you want it to be.
301
+
302
+ ### 5. `Header`: lockup brand by default, quieter retro styling, uppercase nav, `end` slot (DMNC-1584)
303
+
304
+ **What changed.**
305
+
306
+ - **Default brand is the lockup.** When you pass `brandName` and no `children`,
307
+ the header now renders the eiDotter Logo (28px, glow) next to the name in
308
+ uppercase, wide-tracked DOS text. The new prop
309
+ `brand?: 'lockup' | 'mark' | 'text'` (default `'lockup'`) selects it.
310
+ `brand="text"` restores the 0.40 plain-text brand. Headers that pass their
311
+ own `children` as branding render unchanged.
312
+ - **Retro styling.** The strong amber glow becomes a soft glow under a 1px
313
+ dim-amber line. The brand link and the context-row return pill use brand
314
+ amber (`text-brand`) and lift to `text-brand-bright` on hover. Retro `Nav`
315
+ desktop and mobile-panel links and the MENU trigger use the same pair, and
316
+ their labels render uppercase (CSS `text-transform`, so your label strings
317
+ are unchanged). The `modern` variant is unchanged.
318
+ - **New `end` slot.** `end?: ReactNode` renders once: after the desktop nav on
319
+ wide screens, between the brand and the MENU trigger below 768px. Without it
320
+ the DOM is unchanged.
321
+ - **New `skipLinkTarget`.** `skipLinkTarget="#main"` renders a visually hidden
322
+ "Skip to content" link as the header's first child, shown on keyboard focus.
323
+ Off by default.
324
+
325
+ **Find it.**
326
+
327
+ ```bash
328
+ rg -n -A8 '<Header\b' src app components pages # brandName without children?
329
+ rg -n 'eidotter-header|eidotter-nav' --glob '*.{css,scss}' src app styles
330
+ rg -n 'position:\s*absolute' --glob '*.{css,scss}' src app styles | rg -i 'header|toggle|theme'
331
+ ```
332
+
333
+ **What to do.**
334
+
335
+ ```tsx
336
+ // before (0.40): plain-text brand, theme toggle positioned by hand over the header
337
+ <div className="relative">
338
+ <Header brandName="MY SITE" items={nav} activeHref={pathname} />
339
+ <ThemeToggle className="absolute right-16 top-3" />
340
+ </div>
341
+
342
+ // after (0.41): keep the lockup (or brand="text" for the old look), toggle in `end`
343
+ <Header
344
+ brandName="MY SITE"
345
+ items={nav}
346
+ activeHref={pathname}
347
+ end={<ThemeToggle />}
348
+ skipLinkTarget="#main"
349
+ />
350
+ <main id="main">…</main>
351
+ ```
352
+
353
+ - Decide per site: lockup (default), `brand="mark"` (see "Adopt"), or
354
+ `brand="text"`.
355
+ - Delete CSS that recoloured the retro header/nav to brand amber or forced
356
+ uppercase on nav links. That is now the default.
357
+ - Delete CSS that targeted the old strong glow on `.eidotter-header--retro`.
358
+ - Keep `end` content compact: on mobile it clips before the brand name does.
359
+ - The `end` slot is `overflow: hidden` at every width. A dropdown, menu or
360
+ tooltip opened from it must render in a portal (React Aria popovers such as
361
+ `Select` already do); an in-flow popover is clipped.
362
+
363
+ **Verify.** At 390px the brand, the `end` content and the MENU trigger fit on
364
+ one line. At 1280px it reads brand, nav, then `end`. Tabbing from the address
365
+ bar focuses the skip link first, if you set one.
366
+
367
+ ### 6. Overlays rebuilt on `DosDialog`; removed classes (DMNC-1592)
368
+
369
+ **What changed.** `Modal`, `Lightbox` and `CmdPalette` now render inside the
370
+ shared `DosDialog` shell: brand-coloured border with amber glow, square
371
+ corners, a blurred scrim, and a dark title strip (yellow title, `▸` marker,
372
+ uppercase, dim-amber rule). They enter and exit with a shared pop (scale 0.8
373
+ to 1, 150ms). Their z-index is `--z-index-modal` (1040). Modal was 50 and
374
+ Lightbox 60, so opening a Lightbox over an open Modal is no longer supported.
375
+ Props are unchanged except for additions (Modal gained `size` and
376
+ `isDismissable`).
377
+
378
+ Specific visible changes: Modal's yellow frame becomes the brand border and its
379
+ header becomes the strip; long Modal titles wrap. Lightbox gains a strip whose
380
+ title is the `[ n / N ]` counter, spans the viewport minus 32px, and its prev and
381
+ next arrows are pixel chevron `IconButton`s. CmdPalette's `▸ JUMP TO` head loses
382
+ its amber fill and becomes the dark strip; at phone width the box is capped at
383
+ viewport minus 32px.
384
+
385
+ **Removed classes.** CSS that targets these no longer matches anything:
386
+
387
+ | Component | Removed | Target instead |
388
+ |---|---|---|
389
+ | Modal | `eidotter-modal-overlay`, `eidotter-modal-overlay--entering`, `eidotter-modal-overlay--exiting`, `eidotter-modal`, `eidotter-modal--entering`, `eidotter-modal--exiting`, `eidotter-modal__header`, `eidotter-modal__title` | `eidotter-dialog__overlay`, `eidotter-dialog`, `eidotter-dialog__container`, `eidotter-dialog__head`, `eidotter-dialog__title` |
390
+ | Lightbox | `eidotter-lightbox-overlay`, `eidotter-lightbox-overlay--entering`, `eidotter-lightbox-overlay--exiting`, `eidotter-lightbox`, `eidotter-lightbox--entering`, `eidotter-lightbox--exiting` | the `eidotter-dialog*` classes above |
391
+ | CmdPalette | `eidotter-cmdpal-overlay`, `eidotter-cmdpal-overlay--entering`, `eidotter-cmdpal-overlay--exiting`, `eidotter-cmdpal`, `eidotter-cmdpal--entering`, `eidotter-cmdpal--exiting`, `eidotter-cmdpal__head` | the `eidotter-dialog*` classes; the head is `eidotter-dialog__head` |
392
+ | Card | `eidotter-card__header`, `eidotter-card__footer` | `eidotter-window__strip`, `eidotter-window__footer` |
393
+ | DosFigure | `eidotter-dos-figure__title` | `eidotter-window__strip`, `eidotter-window__title` |
394
+ | Mark | `eidotter-mark__blob`, `eidotter-mark__blob--morph` | the root `eidotter-mark` (the drawing is now an SVG) |
395
+
396
+ Still present: `eidotter-modal__container`, `__body`, `__footer`, `__close`;
397
+ `eidotter-lightbox__container`, `__counter`, `__close`, `__figure`, `__image`,
398
+ `__caption`, `__nav`; `eidotter-cmdpal__container`, `__input`, `__results`,
399
+ `__item*`, `__empty`, `__footer`, `__blink`, plus the new
400
+ `eidotter-cmdpal__text--bright` / `--muted`. The close and navigation classes
401
+ (`eidotter-modal__close`, `eidotter-lightbox__close`, `eidotter-lightbox__nav`,
402
+ `eidotter-nav__close`, `eidotter-alert__close`, `eidotter-notification__close`,
403
+ `eidotter-tag__close`) stay on the buttons as hooks, but their old visual
404
+ styles are gone: the buttons are now `IconButton`s (class
405
+ `eidotter-icon-button`).
406
+
407
+ The `eidotter-dialog*` classes are shared by every DosDialog. To restyle one
408
+ kind of overlay only, scope your rule by the component's container class,
409
+ which sits on the same element as `eidotter-dialog__container`:
410
+ `.eidotter-modal__container`, `.eidotter-lightbox__container` or
411
+ `.eidotter-cmdpal__container`. Modal and CmdPalette also merge your
412
+ `className` onto that element.
413
+
414
+ **Find it.**
415
+
416
+ ```bash
417
+ rg -n 'eidotter-(modal-overlay|modal--|modal__header|modal__title|lightbox-overlay|lightbox--|cmdpal-overlay|cmdpal--|cmdpal__head|card__header|card__footer|dos-figure__title|mark__blob)' \
418
+ src app components pages styles
419
+ rg -n '\.eidotter-(modal|lightbox|cmdpal)\b' --glob '*.{css,scss}' src app styles
420
+ rg -n 'eidotter-(modal|lightbox|alert|notification|tag|nav)__close|eidotter-lightbox__nav' \
421
+ --glob '*.{css,scss}' src app styles
422
+ rg -n 'z-index' --glob '*.{css,scss}' src app styles | rg -i 'modal|lightbox|overlay|cmdpal'
423
+ ```
424
+
425
+ **What to do.**
426
+
427
+ - Delete overrides that restyled the old frame, header, scrim or enter/exit
428
+ animation. The new chrome is the house style; first see whether you still
429
+ need anything.
430
+ - Retarget what you still need, using the table above.
431
+ - Delete restyles of the close and prev/next buttons. The shared IconButton
432
+ already has hover, focus and disabled states.
433
+ - If your own fixed or sticky UI must sit above or below dialogs, compare it
434
+ against 1040, not 50 or 60.
435
+
436
+ ```css
437
+ /* before (0.40) */
438
+ .eidotter-modal__header { background: var(--color-semantic-background-secondary); }
439
+ .eidotter-modal-overlay { backdrop-filter: blur(4px); }
440
+ /* after (0.41): usually delete both; if you must keep a tweak, retarget it */
441
+ .eidotter-modal__container .eidotter-dialog__head { /* … */ }
442
+ ```
443
+
444
+ **Verify.** Open each Modal, Lightbox and CmdPalette in your app. Escape and a
445
+ backdrop click close them, focus returns to the trigger, and nothing of yours
446
+ paints over the dialog. A Modal with a long title wraps instead of clipping.
447
+
448
+ ### 7. Other visible restyles
449
+
450
+ Each of these is CSS only, with no prop change. Search for overrides and
451
+ delete or retarget them.
452
+
453
+ - **Card** is built on `DosWindow`. The header becomes the dark strip (yellow
454
+ xs uppercase title, `▸` marker, dim-amber rule), and the footer becomes the
455
+ window footer. `elevated` is now the brand border plus `--shadow-drop`
456
+ instead of a near-black border with a 4px offset shadow. Find:
457
+ `rg -n 'eidotter-card' --glob '*.{css,scss}'`.
458
+ - **DosFigure** is built on `DosWindow`. The amber title bar with dark text
459
+ becomes the dark strip. Find: `rg -n 'eidotter-dos-figure' --glob '*.{css,scss}'`.
460
+ - **Alert and Notification** are square (no 4px radius) and their close is an
461
+ outlined 36×36 IconButton. Find:
462
+ `rg -n 'eidotter-(alert|notification)' --glob '*.{css,scss}'`.
463
+ - **Tag remove** is a bare IconButton with the pixel X instead of `[x]`. The
464
+ accessible name is still `Remove <label>`. Tests that query the text `[x]`
465
+ must query the button by role and name instead:
466
+
467
+ ```ts
468
+ // before
469
+ screen.getByText('[x]');
470
+ // after
471
+ screen.getByRole('button', { name: 'Remove react' });
472
+ ```
473
+
474
+ Find: `rg -n "\[x\]" --glob '*.{test,spec}.{ts,tsx,js,jsx}'`.
475
+ - **Lightbox prev/next** are chevron IconButtons instead of `◀ ▶` text. Find
476
+ tests that query those glyphs: `rg -n '◀|▶' --glob '*.{test,spec}.*'`.
477
+ - **Logo and Mark** share one SVG drawing (DMNC-1586). Logo's flat yolk becomes
478
+ a lit yolk with highlights, and its glow drops from 6px to 5px. Mark renders
479
+ that SVG instead of a CSS blob. The `morph` prop now drives a slow breath
480
+ (scale to 1.045, 4.3s) and is off under reduced motion. Mark gains `glow`
481
+ (default `true`). `size`, `label`, `arms`, the ref and the root class
482
+ `eidotter-mark` are unchanged. Find:
483
+ `rg -n 'eidotter-(logo|mark)' --glob '*.{css,scss}'` and any copied favicon
484
+ (`rg --files | rg -i 'favicon|icon\.svg'`). Re-copy the favicon (see
485
+ "Adopt").
486
+ - **Motion.** Select's popover pops instead of dropping 4px. TimelineContainer
487
+ zoom views and the master-detail pane fade instead of rising. Several
488
+ components share keyframes now (`eidotter-pop-in/out`,
489
+ `eidotter-fade-in/out`, `eidotter-breathe`). If your CSS overrode eidotter's
490
+ internal keyframe names (`modal-crt-enter`, `cmdpal-enter`, `tag-enter`,
491
+ `badge-enter`, `alert-enter`, `notification-enter`, `select-pop-in`,
492
+ `eidotter-mark-morph`, `eidotter-card-enter`, `eidotter-inline-expand-fade`,
493
+ `eidotter-timeline-node-enter`, `eidotter-skeleton-breathe`,
494
+ `eidotter-health-breathe`, …), those names no longer exist. Find:
495
+ `rg -n 'modal-crt|cmdpal-|badge-enter|tag-enter|tag-exit|alert-enter|notification-enter|select-pop|backdrop-open|timeline-fade-in|master-detail-fade|blink-loading|eidotter-(mark-morph|card-enter|inline-expand-fade|timeline-node-enter|skeleton-breathe|health-breathe)' --glob '*.{css,scss}'`.
496
+ - **Progress** sizes are distinct again: `sm`/`md`/`lg` render 16/20/22px (were
497
+ 16/16/18px).
498
+
499
+ ### 8. Focus rings (DMNC-1436)
500
+
501
+ **What changed.** Keyboard focus is now a visible amber ring. `Button`'s ring
502
+ was cancelled under Tailwind v4 and shows again. `Input`, `Select` and the
503
+ Accordion header (`Section`) draw a 2px amber outline at 2px offset on
504
+ `:focus-visible`. Their focus border used to be invisible under amber-mono.
505
+ The Accordion header's flush box-shadow ring is replaced by that outline. The
506
+ CmdPalette input gets the same 2px outline. Under amber-mono the focus colour
507
+ (`--color-semantic-border-focus`) is now amber `#FFB000` (was amber-bright).
508
+
509
+ **Find it.** Look for focus styles you added to fill the gap; they may now
510
+ double up:
511
+
512
+ ```bash
513
+ rg -n -B2 -A4 'focus' --glob '*.{css,scss}' src app styles | rg 'eidotter-'
514
+ rg -n 'focus-visible:(outline|ring)' src app components | rg '<(Button|Input|Select|Section)\b'
515
+ ```
516
+
517
+ **What to do.** Delete your own focus outlines and rings on eidotter Button,
518
+ Input, Select and Accordion. Keep focus styles on your own elements.
519
+
520
+ **Verify.** Tab through a page: each eidotter control shows exactly one amber
521
+ ring.
522
+
523
+ ### 9. Renamed variants
524
+
525
+ None at runtime. 0.41.0 corrects variant keys in the component registry
526
+ metadata used by eidotter's own tooling (DMNC-1459). No component prop or
527
+ variant value changed; Badge, Tag, Alert and Button props are as in 0.40.
528
+
529
+ ---
530
+
531
+ ## Adopt the new pieces
532
+
533
+ Replace hand-rolled patterns with these. They are optional, but they are the
534
+ point of the release: one shared chrome instead of a slightly different copy
535
+ in every site.
536
+
537
+ All imports below use the barrel (`'eidotter'`), which is right for client
538
+ code. For Next.js server components, see "RSC deep imports" at the end of this
539
+ section.
540
+
541
+ ### IconButton: every close, prev/next, remove icon control
542
+
543
+ An icon-only React Aria button: a 1px outlined square with an `Icon` glyph,
544
+ amber fill on hover, a 2px brand outline on focus, half opacity when disabled.
545
+
546
+ | Prop | Type | Default | Notes |
547
+ |---|---|---|---|
548
+ | `icon` | `IconName` | required | e.g. `'Close'`, `'Chevron Left'`, `'Chevron Right'`, `'Add'`, `'Check'` |
549
+ | `label` | `string` | required | becomes the `aria-label` |
550
+ | `size` | `'md' \| 'window'` | `'md'` | md is 36×36 with a 20px glyph; window is 28×28 with a 16px glyph for dense chrome |
551
+ | `variant` | `'outline' \| 'bare'` | `'outline'` | bare has no resting outline |
552
+ | `isDisabled`, `onPress`, `excludeFromTabOrder`, `className` | | | `onPress` receives a React Aria `PressEvent` |
553
+
554
+ Available icon names: `Info`, `Warning`, `Error`, `Done`, `Check`, `Close`,
555
+ `Chevron Up`, `Chevron Down`, `Chevron Left`, `Chevron Right`, `App`,
556
+ `Cancel` (a minus glyph, for minimize), `Fullscreen`, `Add`.
557
+
558
+ ```bash
559
+ rg -n '(aria-label=.(close|dismiss|previous|next|remove)|>\s*(×|✕|x|\[x\]|◀|▶|‹|›)\s*<)' -i src app components
560
+ ```
561
+
562
+ ```tsx
563
+ // before: a hand-rolled close button
564
+ <button className="close-btn" aria-label="Close" onClick={onClose}>×</button>
565
+ // after
566
+ import { IconButton } from 'eidotter';
567
+ <IconButton icon="Close" label="Close" onPress={onClose} />
568
+ <IconButton icon="Chevron Left" label="Previous image" onPress={prev} isDisabled={index === 0} />
569
+ ```
570
+
571
+ IconButton is a client component (it carries `'use client'` in the deep-import
572
+ build).
573
+
574
+ ### DosDialog: custom modal and overlay shells
575
+
576
+ Use `Modal` for an ordinary titled dialog (`isOpen`, `onOpenChange` /
577
+ `onClose`, `title`, `footer`, `size`, `isDismissable`). Use `DosDialog` when
578
+ you are building your own overlay shell, such as a settings sheet, a
579
+ search-results overlay, or a confirm dialog with custom chrome. React Aria
580
+ provides the focus trap, scroll lock, Escape and backdrop dismissal.
581
+
582
+ Props: `open` (required), `onOpenChange`, `placement` (`'center'` default |
583
+ `'top'`), `size` (`'sm'` 24rem, `'md'` 30rem default, `'lg'` 40rem, `'full'`),
584
+ `head` (`{ title, end?, marker?, decorative? }` or any ReactNode), `footer`,
585
+ `isDismissable` (default `true`), `aria-label` / `aria-labelledby`,
586
+ `className`. A non-decorative `head.title` renders as the `<h2>` and names the
587
+ dialog. Otherwise pass `aria-label`; in development an unnamed open dialog logs
588
+ a warning.
589
+
590
+ ```bash
591
+ rg -n 'createPortal|role="dialog"|aria-modal|fixed inset-0' src app components
592
+ ```
593
+
594
+ ```tsx
595
+ import { useState } from 'react';
596
+ import { DosDialog, IconButton, Button } from 'eidotter';
597
+
598
+ function SettingsDialog() {
599
+ const [open, setOpen] = useState(false);
600
+ return (
601
+ <>
602
+ <Button onPress={() => setOpen(true)}>Settings</Button>
603
+ <DosDialog
604
+ open={open}
605
+ onOpenChange={setOpen}
606
+ size="sm"
607
+ head={{
608
+ title: 'SETTINGS',
609
+ end: <IconButton icon="Close" label="Close settings" onPress={() => setOpen(false)} />,
610
+ }}
611
+ footer={<Button onPress={() => setOpen(false)}>Done</Button>}
612
+ >
613
+ {/* your content */}
614
+ </DosDialog>
615
+ </>
616
+ );
617
+ }
618
+ ```
619
+
620
+ DosDialog does not add a close button for you; pass one in `head.end` as
621
+ above. DosDialog is a client component.
622
+
623
+ ### DosWindow: in-page window chrome
624
+
625
+ The same title strip as DosDialog on a brand border with `--shadow-drop`, but
626
+ in page flow, with no glow and no backdrop. Card and DosFigure are built on it.
627
+ Use it for panels you currently frame by hand with a border and a title bar.
628
+
629
+ Props: `title`, `marker` (default `true`, the `▸`), `active` (default `true`;
630
+ inactive dims the title), `controls` (IconButtons for the strip's end),
631
+ `footer`, `className`, plus any `<div>` attribute. With neither `title` nor
632
+ `controls` there is no strip. The strip title is a `<span>`, not a heading;
633
+ pass your own heading element as `title` if the window belongs in the document
634
+ outline.
635
+
636
+ ```tsx
637
+ import { DosWindow, IconButton } from 'eidotter';
638
+
639
+ <DosWindow
640
+ title="SYSTEM.LOG"
641
+ controls={<IconButton icon="Close" label="Hide log" onPress={hideLog} />}
642
+ footer="3 entries"
643
+ >
644
+ <ul>…</ul>
645
+ </DosWindow>
646
+ ```
647
+
648
+ DosWindow is server-safe: no hooks, no `'use client'`. In a Next.js server
649
+ component you can render it directly from `eidotter/components/DosWindow`. Put
650
+ any `controls` with `onPress` handlers in a client component, because a server
651
+ component cannot pass functions.
652
+
653
+ ### CmdPalette: mount once, in the root layout
654
+
655
+ Mount one `<CmdPalette>` per site in the root layout, so ⌘K / Ctrl+K works on
656
+ every page. The default `hotkey` is `'mod+k'`; pass `hotkey={false}` to open it
657
+ yourself. `items` should list only that site's public routes. The palette
658
+ requests close on Escape, backdrop click and item selection through
659
+ `onOpenChange`.
660
+
661
+ ```tsx
662
+ // components/SiteCmdPalette.tsx (Next.js App Router)
663
+ 'use client';
664
+ import { useState } from 'react';
665
+ import { useRouter } from 'next/navigation';
666
+ import { CmdPalette, type CmdPaletteItem } from 'eidotter';
667
+
668
+ const routes = [
669
+ { path: '/', label: 'HOME' },
670
+ { path: '/about', label: 'ABOUT' },
671
+ ];
672
+
673
+ export function SiteCmdPalette() {
674
+ const [open, setOpen] = useState(false);
675
+ const router = useRouter();
676
+ const items: CmdPaletteItem[] = routes.map((r) => ({
677
+ id: r.path,
678
+ label: r.label,
679
+ onSelect: () => router.push(r.path),
680
+ }));
681
+ return <CmdPalette open={open} onOpenChange={setOpen} items={items} />;
682
+ }
683
+ ```
684
+
685
+ ```tsx
686
+ // app/layout.tsx
687
+ import { SiteCmdPalette } from '@/components/SiteCmdPalette';
688
+
689
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
690
+ return (
691
+ <html lang="en">
692
+ <body>
693
+ {children}
694
+ <SiteCmdPalette />
695
+ </body>
696
+ </html>
697
+ );
698
+ }
699
+ ```
700
+
701
+ - Astro: put the same component (with `onSelect: () => { window.location.href = r.path; }`)
702
+ in the base layout as `<SiteCmdPalette client:load />`.
703
+ - Vite with react-router: render it once next to `<Routes>` and navigate with
704
+ `useNavigate()`.
705
+ - Show a ⌘K hint (in the site `Footer` or the Header `end` slot) only where
706
+ the palette is mounted. Not in `footerHint`, which renders only inside the
707
+ open palette.
708
+
709
+ ### `eidotter/favicon.svg`
710
+
711
+ The logomark favicon ships in the package (regenerated in 0.41.0 from the new
712
+ Logo drawing). It is an SVG only. Copy it into your app at build time instead of
713
+ committing a copy that goes stale:
714
+
715
+ ```json
716
+ // package.json (any framework); for Next.js App Router change the target to app/icon.svg
717
+ {
718
+ "scripts": {
719
+ "copy-favicon": "node -e \"require('fs').copyFileSync(require.resolve('eidotter/favicon.svg'), 'public/favicon.svg')\"",
720
+ "predev": "npm run copy-favicon",
721
+ "prebuild": "npm run copy-favicon"
722
+ }
723
+ }
724
+ ```
725
+
726
+ - **Next.js App Router:** copy to `app/icon.svg`. Next generates the
727
+ `<link rel="icon">` itself. Remove any old `app/favicon.ico` or `app/icon.*`
728
+ you no longer want.
729
+ - **Vite:** copy to `public/favicon.svg`, then in `index.html`:
730
+ `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`.
731
+ - **Astro:** copy to `public/favicon.svg`, then in the base layout `<head>`:
732
+ `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`.
733
+
734
+ Add the copied path to `.gitignore` if you generate it on every build.
735
+
736
+ ### `amber-paper` theme and a theme toggle
737
+
738
+ A third theme next to the dark default and `light`: reverse video, with an
739
+ amber ground and near-black ink. Functional colour stays monochrome and there
740
+ is no phosphor glow. Themes are opt-in CSS plus an attribute (or class) on an
741
+ ancestor, usually `<html>`:
742
+
743
+ ```ts
744
+ import 'eidotter/styles';
745
+ import 'eidotter/themes/light.css';
746
+ import 'eidotter/themes/amber-paper.css';
747
+ ```
748
+
749
+ ```html
750
+ <html data-theme="amber-paper"> <!-- or class="theme-amber-paper" -->
751
+ ```
752
+
753
+ No attribute means the dark default. `data-theme="light"` is the light theme
754
+ (`eidotter/themes/light.css`). `data-theme="amber-mono"` is already in
755
+ `eidotter/styles`. The house convention is the amber/dark default, with other
756
+ themes offered through a toggle. A minimal toggle for the Header `end` slot:
757
+
758
+ ```tsx
759
+ 'use client';
760
+ import { useEffect, useState } from 'react';
761
+ import { Button } from 'eidotter';
762
+
763
+ const THEMES = ['default', 'light', 'amber-paper'] as const;
764
+ type Theme = (typeof THEMES)[number];
765
+
766
+ export function ThemeToggle() {
767
+ const [theme, setTheme] = useState<Theme>('default');
768
+ useEffect(() => {
769
+ try {
770
+ const saved = localStorage.getItem('theme') as Theme | null;
771
+ if (saved && THEMES.includes(saved)) setTheme(saved);
772
+ } catch {}
773
+ }, []);
774
+ useEffect(() => {
775
+ const root = document.documentElement;
776
+ if (theme === 'default') delete root.dataset.theme;
777
+ else root.dataset.theme = theme;
778
+ try { localStorage.setItem('theme', theme); } catch {}
779
+ }, [theme]);
780
+ const next = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length];
781
+ return (
782
+ <Button variant="secondary" size="sm" onPress={() => setTheme(next)}>
783
+ THEME: {theme.toUpperCase()}
784
+ </Button>
785
+ );
786
+ }
787
+ ```
788
+
789
+ On server-rendered sites, set the attribute before first paint so a saved
790
+ theme does not flash. A tiny inline script in `<head>` does it:
791
+ `<script>try{var t=localStorage.getItem('theme');if(t&&t!=='default')document.documentElement.dataset.theme=t}catch(e){}</script>`.
792
+ In React, put `suppressHydrationWarning` on `<html>`.
793
+
794
+ Check your own CSS under amber-paper. Anything hard-coded to "dark background,
795
+ light text" breaks there; paint with `var(--color-semantic-*)` roles.
796
+
797
+ ### Mark in the header
798
+
799
+ `<Header brand="mark" brandName="MY SITE" … />` shows the breathing Mark
800
+ (28px) instead of the static Logo. Use one or the other across a site.
801
+ Standalone: `import { Mark } from 'eidotter'`; `<Mark size={48} />`. Pass
802
+ `label=""` when it is decorative, and `morph={false}` for a still mark.
803
+
804
+ ### RSC deep imports (Next.js server components)
805
+
806
+ The `eidotter` barrel is client-only. Importing it from a server component
807
+ fails. In server components, deep-import per component:
808
+
809
+ ```tsx
810
+ import { DosWindow } from 'eidotter/components/DosWindow'; // server-safe
811
+ import { Header } from 'eidotter/components/Header'; // server-safe (its interactive bits are client leaves)
812
+ import { IconButton } from 'eidotter/components/IconButton'; // client reference
813
+ import { DosDialog } from 'eidotter/components/DosDialog'; // client reference
814
+ import { Logo, Mark } from 'eidotter/components/Brand';
815
+ ```
816
+
817
+ Interactive components resolve as client references, so they render, but you
818
+ cannot pass them function props such as `onPress` from a server component;
819
+ wrap those in your own `'use client'` component. CSS still comes from one
820
+ `import 'eidotter/styles'` in the root layout.
821
+
822
+ ---
823
+
824
+ ## Upgrade checklist
825
+
826
+ Work top to bottom. Each step points at the section above that explains it.
827
+
828
+ - [ ] Read every guide in `node_modules/eidotter/migrations/` between your old
829
+ version and 0.41.0, oldest first (see `README.md` in that folder).
830
+ - [ ] `cp node_modules/eidotter/dist/eidotter.css /tmp/eidotter-old.css` (before the bump).
831
+ - [ ] `npm install eidotter@^0.41.0`, then `npm ls eidotter` shows 0.41.x. Commit the lockfile.
832
+ - [ ] No-Tailwind apps: run the class diff from section 2 and fix every real hit.
833
+ - [ ] No-preflight apps: set `html` font, colours and `a` colour yourself (section 4).
834
+ - [ ] Preset apps: review `text-xs` / `text-sm` hits; delete redundant 16px overrides (section 1).
835
+ - [ ] Replace `text-secondary` used on amber or light fills with `text-on-accent` (section 3).
836
+ - [ ] Header: choose `brand` (`lockup` default, `mark`, or `text`), move hand-placed header controls into `end`, consider `skipLinkTarget` (section 5).
837
+ - [ ] Delete or retarget CSS on removed classes (Modal, Lightbox, CmdPalette, Card, DosFigure, Mark) and on the old close and nav buttons (section 6).
838
+ - [ ] Delete overrides fighting the new Card, DosFigure, Alert, Notification, Logo and Mark looks, and references to retired keyframe names (section 7).
839
+ - [ ] Delete your own focus rings on eidotter Button, Input, Select and Accordion (section 8).
840
+ - [ ] Update tests that query `[x]`, `◀`, `▶`, the old classes or the old plain-text brand.
841
+ - [ ] Optional adoption: IconButton for icon controls, DosDialog / DosWindow for hand-rolled shells, one CmdPalette in the root layout, the favicon copy script, an amber-paper option in the theme toggle.
842
+ - [ ] Next.js: server components import from `eidotter/components/<Name>`, not the barrel.
843
+ - [ ] Run the app's lint, typecheck, tests and production build.
844
+ - [ ] Visual check at **390px** and **1280px** wide, on every page template: header (brand, nav, `end`, MENU), body text size and font, link colours, cards and figures, every dialog (open, Escape, backdrop click, focus return), alerts and notifications, keyboard focus rings, and every theme you offer. Verify colours with `getComputedStyle` rather than screenshots.
845
+ - [ ] Open the PR with before/after screenshots at both widths. **A human must
846
+ sign off on the visuals before merge.** Tests passing is not sufficient
847
+ for this release.
848
+
849
+ ---
850
+
851
+ ## Jumping from older versions
852
+
853
+ If your range was below `^0.40`, these also land when you bump. Each one is
854
+ additive or automatic unless noted. Details are in the eidotter
855
+ [CHANGELOG](https://github.com/CinimoDY/eiDotter/blob/main/CHANGELOG.md). It is
856
+ not shipped in the npm package; read it on GitHub.
857
+
858
+ - **0.38.0:** `article` entry kind for `TimelineContainer` (thumbnail strip and
859
+ summary when collapsed; gallery, body and read-more when expanded). Additive.
860
+ - **0.38.0:** `RetroEffects` `boot` no longer flashes content before the
861
+ turn-on; the cover now ships in the server HTML.
862
+ - **0.38.1:** fonts are no longer inlined into `dist/eidotter.css` (5.7MB down
863
+ to about 150KB). Your bundler must resolve the packaged font `url()`s; this
864
+ works out of the box in Vite, Astro and Next.js.
865
+ - **0.39.0:** connector-system navigation: `Header` `context` row, `Mark`,
866
+ `Connector`, `TimelineFeed`, `TimelinePage`. Additive.
867
+ - **0.39.0:** nav link hrefs (`Breadcrumb`, `Nav`, `Footer`, `Header brandHref`)
868
+ are sanitised; `javascript:` and similar render as plain text. `isSafeHref`
869
+ is exported.
870
+ - **0.40.0:** health and metabolism family: `Gauge`, `Meter`/`MeterGroup`,
871
+ `RangeBar`, `SeriesBars`, `DecayStrip`, `VitalsStrip`, plus `Stat`
872
+ `unit`/`note`/`target`. Additive, with no medical defaults.
873
+ - **0.40.0:** `InlineExpand` expands inline (marker-pen highlight, trailing
874
+ `[-]`). This is a visible redesign; recheck pages that use it.
875
+ - **0.40.1:** every prompt cursor (CommandPrompt, ChatInput, streaming
876
+ ChatMessage, Terminal) is the DOS underscore `_`, not a `█` block. Visible.
877
+ - **0.40.2:** `eidotter/favicon.svg` export (see "Adopt").
878
+
879
+ - **0.38.0 entry:** two-tier typography. Display text stays Perfect DOS VGA
880
+ 437 and body text (`.dos-body`, `font-dos-body`) is JetBrains Mono Nerd Font.
881
+
882
+ Coming from 0.37 or older: also read the CHANGELOG entries for 0.33–0.37,
883
+ including the RSC deep imports `eidotter/components/<Name>` (0.37.1, DMNC-1130).