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