jamdesk 1.1.211 → 1.1.213

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 (59) hide show
  1. package/dist/__tests__/unit/deploy.test.js +13 -0
  2. package/dist/__tests__/unit/deploy.test.js.map +1 -1
  3. package/dist/__tests__/unit/docs-config.test.js +26 -0
  4. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  5. package/dist/__tests__/unit/dusk-embargo.test.d.ts +2 -0
  6. package/dist/__tests__/unit/dusk-embargo.test.d.ts.map +1 -0
  7. package/dist/__tests__/unit/dusk-embargo.test.js +25 -0
  8. package/dist/__tests__/unit/dusk-embargo.test.js.map +1 -0
  9. package/dist/__tests__/unit/migrate-resolve-theme.test.js +11 -0
  10. package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
  11. package/dist/commands/deploy.js +4 -4
  12. package/dist/commands/deploy.js.map +1 -1
  13. package/dist/commands/migrate/index.d.ts.map +1 -1
  14. package/dist/commands/migrate/index.js +7 -3
  15. package/dist/commands/migrate/index.js.map +1 -1
  16. package/dist/commands/migrate/types.d.ts +4 -1
  17. package/dist/commands/migrate/types.d.ts.map +1 -1
  18. package/dist/commands/migrate/types.js +13 -1
  19. package/dist/commands/migrate/types.js.map +1 -1
  20. package/dist/index.js +8 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/docs-config.d.ts +1 -1
  23. package/dist/lib/docs-config.d.ts.map +1 -1
  24. package/dist/lib/docs-config.js +1 -1
  25. package/dist/lib/docs-config.js.map +1 -1
  26. package/dist/lib/dusk-embargo.d.ts +15 -0
  27. package/dist/lib/dusk-embargo.d.ts.map +1 -0
  28. package/dist/lib/dusk-embargo.js +15 -0
  29. package/dist/lib/dusk-embargo.js.map +1 -0
  30. package/dist/lib/normalize-config.js +1 -1
  31. package/dist/lib/normalize-config.js.map +1 -1
  32. package/package.json +1 -1
  33. package/vendored/app/(unlock)/jd/unlock/page.tsx +2 -2
  34. package/vendored/components/layout/LayoutWrapper.tsx +7 -3
  35. package/vendored/components/navigation/DefaultLogo.tsx +25 -18
  36. package/vendored/components/navigation/Header.tsx +78 -29
  37. package/vendored/components/navigation/Sidebar.tsx +5 -3
  38. package/vendored/components/navigation/SocialFooter.tsx +5 -6
  39. package/vendored/components/navigation/ThemePreviewChips.tsx +3 -2
  40. package/vendored/lib/branding-url.ts +9 -0
  41. package/vendored/lib/build/error-parser.ts +1 -1
  42. package/vendored/lib/docs-types.ts +5 -1
  43. package/vendored/lib/dusk-embargo.ts +16 -0
  44. package/vendored/lib/extract-highlights.ts +1 -1
  45. package/vendored/lib/layout-helpers.tsx +19 -2
  46. package/vendored/lib/normalize-config.ts +1 -1
  47. package/vendored/lib/r2-cleanup.ts +14 -10
  48. package/vendored/lib/static-artifacts.ts +84 -30
  49. package/vendored/lib/static-file-route.ts +8 -7
  50. package/vendored/lib/theme-preview.ts +16 -8
  51. package/vendored/lib/validate-config.ts +4 -4
  52. package/vendored/schema/docs-schema.json +124 -0
  53. package/vendored/themes/dusk/variables.css +426 -0
  54. package/vendored/themes/index.ts +28 -0
  55. package/vendored/themes/jam/variables.css +6 -5
  56. package/vendored/themes/nebula/variables.css +1 -1
  57. package/vendored/themes/pulsar/variables.css +4 -3
  58. package/vendored/themes/types.ts +8 -0
  59. package/vendored/workspace-package-lock.json +9 -9
@@ -2124,6 +2124,130 @@
2124
2124
  "navigation"
2125
2125
  ],
2126
2126
  "additionalProperties": false
2127
+ },
2128
+ {
2129
+ "type": "object",
2130
+ "properties": {
2131
+ "theme": {
2132
+ "type": "string",
2133
+ "const": "dusk"
2134
+ },
2135
+ "$schema": {
2136
+ "$ref": "#/anyOf/0/properties/$schema"
2137
+ },
2138
+ "projectId": {
2139
+ "$ref": "#/anyOf/0/properties/projectId"
2140
+ },
2141
+ "name": {
2142
+ "$ref": "#/anyOf/0/properties/name"
2143
+ },
2144
+ "description": {
2145
+ "$ref": "#/anyOf/0/properties/description"
2146
+ },
2147
+ "colors": {
2148
+ "$ref": "#/definitions/colorsSchema"
2149
+ },
2150
+ "logo": {
2151
+ "$ref": "#/anyOf/0/properties/logo"
2152
+ },
2153
+ "favicon": {
2154
+ "$ref": "#/anyOf/0/properties/favicon"
2155
+ },
2156
+ "api": {
2157
+ "$ref": "#/anyOf/0/properties/api"
2158
+ },
2159
+ "appearance": {
2160
+ "$ref": "#/anyOf/0/properties/appearance"
2161
+ },
2162
+ "background": {
2163
+ "$ref": "#/anyOf/0/properties/background"
2164
+ },
2165
+ "navbar": {
2166
+ "$ref": "#/anyOf/0/properties/navbar"
2167
+ },
2168
+ "tabsPosition": {
2169
+ "$ref": "#/anyOf/0/properties/tabsPosition"
2170
+ },
2171
+ "anchors": {
2172
+ "$ref": "#/anyOf/0/properties/anchors"
2173
+ },
2174
+ "navigation": {
2175
+ "$ref": "#/definitions/navigationSchema"
2176
+ },
2177
+ "footer": {
2178
+ "$ref": "#/anyOf/0/properties/footer"
2179
+ },
2180
+ "search": {
2181
+ "$ref": "#/anyOf/0/properties/search"
2182
+ },
2183
+ "seo": {
2184
+ "$ref": "#/anyOf/0/properties/seo"
2185
+ },
2186
+ "fonts": {
2187
+ "$ref": "#/anyOf/0/properties/fonts"
2188
+ },
2189
+ "icons": {
2190
+ "$ref": "#/anyOf/0/properties/icons"
2191
+ },
2192
+ "styling": {
2193
+ "$ref": "#/anyOf/0/properties/styling"
2194
+ },
2195
+ "redirects": {
2196
+ "$ref": "#/anyOf/0/properties/redirects",
2197
+ "description": "URL redirect rules for your documentation site. Each redirect maps a source path to a destination URL"
2198
+ },
2199
+ "integrations": {
2200
+ "$ref": "#/anyOf/0/properties/integrations"
2201
+ },
2202
+ "banner": {
2203
+ "$ref": "#/anyOf/0/properties/banner"
2204
+ },
2205
+ "errors": {
2206
+ "$ref": "#/anyOf/0/properties/errors"
2207
+ },
2208
+ "contextual": {
2209
+ "$ref": "#/anyOf/0/properties/contextual"
2210
+ },
2211
+ "thumbnails": {
2212
+ "$ref": "#/anyOf/0/properties/thumbnails"
2213
+ },
2214
+ "interaction": {
2215
+ "$ref": "#/anyOf/0/properties/interaction"
2216
+ },
2217
+ "metadata": {
2218
+ "$ref": "#/anyOf/0/properties/metadata"
2219
+ },
2220
+ "localization": {
2221
+ "$ref": "#/anyOf/0/properties/localization"
2222
+ },
2223
+ "chat": {
2224
+ "$ref": "#/anyOf/0/properties/chat"
2225
+ },
2226
+ "analytics": {
2227
+ "$ref": "#/anyOf/0/properties/analytics"
2228
+ },
2229
+ "modeToggle": {
2230
+ "$ref": "#/anyOf/0/properties/modeToggle"
2231
+ },
2232
+ "layout": {
2233
+ "$ref": "#/anyOf/0/properties/layout"
2234
+ },
2235
+ "spellcheck": {
2236
+ "$ref": "#/anyOf/0/properties/spellcheck"
2237
+ },
2238
+ "auth": {
2239
+ "$ref": "#/anyOf/0/properties/auth"
2240
+ },
2241
+ "images": {
2242
+ "$ref": "#/anyOf/0/properties/images"
2243
+ }
2244
+ },
2245
+ "required": [
2246
+ "theme",
2247
+ "name",
2248
+ "navigation"
2249
+ ],
2250
+ "additionalProperties": false
2127
2251
  }
2128
2252
  ],
2129
2253
  "definitions": {
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Dusk Theme - CSS Variables
3
+ * Quiet and flat: header, sidebar and content share one tinted ground,
4
+ * separated by 1px borders instead of surface steps. Ink blue accent,
5
+ * Inter body, IBM Plex Serif H1.
6
+ *
7
+ * ARCHITECTURE NOTE: app/globals.css unconditionally @imports jam/variables.css,
8
+ * so Dusk inherits jam's :root — the --prose-* size ladder, the nav/code/UI
9
+ * typography tokens and jam's unscoped mobile rules. This file is injected as a
10
+ * <style> AFTER the globals bundle, so these :root rules win on source order at
11
+ * equal specificity.
12
+ *
13
+ * SPACING NOTE: Dusk uses jam's production spacing. Every spacing rule below is
14
+ * copied from jam/variables.css and re-scoped to dusk — jam scopes its own
15
+ * copies to body[data-theme="jam"], so without these the sidebar gaps, group
16
+ * margins and column alignment would fall back to bare base.css. Change color,
17
+ * type, radius and surfaces here; do not tune spacing.
18
+ *
19
+ * FONT NOTE: never write the serif family by name. next/font self-hosts it
20
+ * under a mangled family (__IBM_Plex_Serif_xxx) exposed as --font-serif on
21
+ * <body> (lib/layout-helpers.tsx); a literal name silently misses.
22
+ */
23
+
24
+ :root {
25
+ /* Radius — restrained, about half of Halo's. */
26
+ --radius-sm: 0.25rem; /* 4px — badges */
27
+ --radius-md: 0.375rem; /* 6px — nav items, buttons */
28
+ --radius-lg: 0.5rem; /* 8px — cards, tables, callouts, inline code */
29
+ --radius-xl: 0.75rem; /* 12px — code panels, modals */
30
+ --radius-2xl: 1rem; /* 16px */
31
+ --radius-full: 9999px;
32
+
33
+ /* Light — one ground for body, header, sidebar and main. Every text token
34
+ is solved against it for the contrast floors pinned in
35
+ __tests__/dusk-theme-gate.test.ts. */
36
+ --color-bg-primary: #F1F2F3;
37
+ --color-bg-secondary: #EBECEF; /* inline code, table header */
38
+ --color-bg-tertiary: #E5E7EB; /* fenced code */
39
+ --color-bg-hover: #E5E7EB;
40
+
41
+ --color-border: #D4D7DD;
42
+ --color-border-hover: #C6CBD3;
43
+ --border-width: 1px;
44
+
45
+ --color-text-primary: #1A1D22; /* 15.1:1 */
46
+ --color-text-secondary: #3C424C; /* 9.0:1 */
47
+ --color-text-tertiary: #515A67; /* 6.2:1 */
48
+ --color-text-muted: #616B7B; /* 4.8:1 */
49
+ --color-marker: #7D8899; /* 3.2:1 — decorative only */
50
+
51
+ --color-accent: #2158A1; /* white label 7.1:1, 6.3:1 as text */
52
+ --color-accent-hover: #1B4984;
53
+ --color-accent-light: #E4EBF5;
54
+ --color-accent-text: #26528D; /* 7.0:1 */
55
+
56
+ /* Primary is ONLY emitted when a project sets colors.primary — there is no
57
+ global default. Without this alias the TOC scroll thumb, active TOC links,
58
+ step chips, Card icons and li::marker compute to transparent and vanish.
59
+ A project's colors.primary still wins: its style block is injected after
60
+ this file. See halo/variables.css for the full reasoning. */
61
+ --color-primary: var(--color-accent);
62
+ --color-primary-dark: var(--color-accent-hover);
63
+
64
+ --color-code-bg: #E5E7EB;
65
+ --color-code-border: #D4D7DD;
66
+ --color-code-text: #1A1D22;
67
+
68
+ /* Callouts — note carries the accent; info is neutral so the two blues
69
+ never sit side by side. Every text color clears 6:1 on its own tint. */
70
+ --color-note-bg: #E4EBF5;
71
+ --color-note-border: #C9D7EB;
72
+ --color-note-text: #26528D;
73
+
74
+ --color-info-bg: #EBECEF;
75
+ --color-info-border: #D4D7DD;
76
+ --color-info-text: #3C424C;
77
+
78
+ --color-warning-bg: #FFF7C2;
79
+ --color-warning-border: #F3D673;
80
+ --color-warning-text: #895000;
81
+
82
+ --color-error-bg: #FEEBEC;
83
+ --color-error-border: #FDBDBE;
84
+ --color-error-text: #AC2529;
85
+
86
+ --color-success-bg: #E9F6E9;
87
+ --color-success-border: #B2DDB5;
88
+ --color-success-text: #236832;
89
+
90
+ /* Same page box and item rhythm as jam. */
91
+ --layout-max-width: 1472px;
92
+ --sidebar-item-spacing: 4px;
93
+
94
+ /* Active sidebar item. A blue wash rather than --color-accent-light, which
95
+ is a callout tint and sits too close to the ground to read as a state. */
96
+ --dusk-nav-active-bg: #DCE5F2;
97
+
98
+ /* Flat. Restated rather than inherited so the intent is on the page. */
99
+ --shadow-sm: none;
100
+ --shadow-md: none;
101
+ --shadow-lg: none;
102
+ --shadow-xl: none;
103
+ --shadow-2xl: none;
104
+
105
+ /* Weights only — sizes come from jam's ladder. Body is 400, not jam's 300:
106
+ a thin weight undoes the contrast the grays above are tuned for. */
107
+ --prose-h1-weight: 600;
108
+ --prose-h2-weight: 600;
109
+ --prose-h3-weight: 600;
110
+ --prose-h4-weight: 600;
111
+ --prose-body-weight: 400;
112
+ --nav-page-weight: 400;
113
+ --nav-page-active-weight: 500;
114
+ }
115
+
116
+ .dark {
117
+ --color-bg-primary: #0F1318;
118
+ --color-bg-secondary: #1B1F23;
119
+ --color-bg-tertiary: #22262B;
120
+ --color-bg-hover: #22262B;
121
+
122
+ --color-border: #333942;
123
+ --color-border-hover: #3E444E;
124
+
125
+ --color-text-primary: #E4E7EA; /* 15.0:1 */
126
+ --color-text-secondary: #B8BFC7; /* 10.0:1 */
127
+ --color-text-tertiary: #909AA7; /* 6.5:1 */
128
+ --color-text-muted: #768394; /* 4.8:1 */
129
+ --color-marker: #5C6674; /* 3.2:1 */
130
+
131
+ /* The accent is used as TEXT in ~38 components as well as a fill, so in
132
+ dark it has to read on the ground (5.0:1). White on it lands at 3.7:1,
133
+ the same trade jam's dark #3b82f6 makes. Links use --color-accent-text. */
134
+ --color-accent: #4A86D4;
135
+ --color-accent-hover: #6A9BDB;
136
+ --color-accent-light: #11253F;
137
+ --color-accent-text: #88ADDC; /* 8.0:1 */
138
+
139
+ --color-code-bg: #22262B;
140
+ --color-code-border: #333942;
141
+ --color-code-text: #E4E7EA;
142
+
143
+ --color-note-bg: #11253F;
144
+ --color-note-border: #1F4475;
145
+ --color-note-text: #88ADDC;
146
+
147
+ --color-info-bg: #1B1F23;
148
+ --color-info-border: #333942;
149
+ --color-info-text: #B8BFC7;
150
+
151
+ --color-warning-bg: #302008;
152
+ --color-warning-border: #5C3D05;
153
+ --color-warning-text: #CB9800;
154
+
155
+ --color-error-bg: #3B1219;
156
+ --color-error-border: #72232D;
157
+ --color-error-text: #FF6F6A;
158
+
159
+ --color-success-bg: #1B2A1E;
160
+ --color-success-border: #2D5736;
161
+ --color-success-text: #3EBB53;
162
+
163
+ --dusk-nav-active-bg: #152E4E;
164
+
165
+ --shadow-sm: none;
166
+ --shadow-md: none;
167
+ --shadow-lg: none;
168
+ --shadow-xl: none;
169
+ --shadow-2xl: none;
170
+ }
171
+
172
+ /* ===== TYPOGRAPHY ===== */
173
+ /* REQUIRED, not polish: base.css hardcodes `.prose h1..h6 { font-weight: 600 }`
174
+ and every --prose-* consumer is scoped to one theme, so without this block
175
+ the ladder is dead CSS and h1 falls back to base.css's raw 2.25rem. */
176
+ body[data-theme="dusk"] .prose {
177
+ font-family: var(--font-sans, var(--font-primary, 'Inter')), sans-serif;
178
+ font-size: var(--prose-body-size);
179
+ line-height: var(--prose-body-line-height);
180
+ font-weight: var(--prose-body-weight);
181
+ letter-spacing: var(--prose-body-tracking);
182
+ }
183
+
184
+ /* The serif H1. The bare `h1` is required: the page title renders outside
185
+ .prose. A customer heading font (--font-heading, from docs.json fonts)
186
+ still wins; otherwise the theme serif. --font-serif is set on <body> by
187
+ next/font, so this must stay scoped to body, never :root. */
188
+ body[data-theme="dusk"] .prose h1,
189
+ body[data-theme="dusk"] h1 {
190
+ font-family: var(--font-heading, var(--font-serif)), 'Times New Roman', serif;
191
+ font-size: var(--prose-h1-size);
192
+ line-height: var(--prose-h1-line-height);
193
+ font-weight: var(--prose-h1-weight);
194
+ letter-spacing: -0.01em;
195
+ color: var(--color-text-primary);
196
+ }
197
+
198
+ body[data-theme="dusk"] .prose h2 {
199
+ font-family: var(--font-heading, var(--font-sans, var(--font-primary, 'Inter'))), sans-serif;
200
+ font-size: var(--prose-h2-size);
201
+ line-height: var(--prose-h2-line-height);
202
+ font-weight: var(--prose-h2-weight);
203
+ letter-spacing: var(--prose-h2-tracking);
204
+ color: var(--color-text-primary);
205
+ }
206
+
207
+ body[data-theme="dusk"] .prose h3 {
208
+ font-family: var(--font-heading, var(--font-sans, var(--font-primary, 'Inter'))), sans-serif;
209
+ font-size: var(--prose-h3-size);
210
+ line-height: var(--prose-h3-line-height);
211
+ font-weight: var(--prose-h3-weight);
212
+ letter-spacing: var(--prose-h3-tracking);
213
+ color: var(--color-text-primary);
214
+ }
215
+
216
+ body[data-theme="dusk"] .prose h4 {
217
+ font-family: var(--font-heading, var(--font-sans, var(--font-primary, 'Inter'))), sans-serif;
218
+ font-size: var(--prose-h4-size);
219
+ line-height: var(--prose-h4-line-height);
220
+ font-weight: var(--prose-h4-weight);
221
+ letter-spacing: var(--prose-h4-tracking);
222
+ color: var(--color-text-primary);
223
+ }
224
+
225
+ /* Inline code — jam's padding, Dusk's surface. */
226
+ body[data-theme="dusk"] .prose code:not([class*="language-"]) {
227
+ background-color: var(--color-bg-secondary);
228
+ color: var(--color-code-text);
229
+ padding: 0.125rem 0.375rem;
230
+ border-radius: var(--radius-sm);
231
+ font-size: var(--code-inline-size);
232
+ }
233
+
234
+ /* Untitled code blocks have no CodePanel border of their own. */
235
+ body[data-theme="dusk"] .prose pre.shiki {
236
+ border: var(--border-width) solid var(--color-code-border);
237
+ }
238
+
239
+ body[data-theme="dusk"] .prose table {
240
+ border-radius: var(--radius-lg);
241
+ overflow: hidden;
242
+ }
243
+
244
+ body[data-theme="dusk"] .prose hr,
245
+ body[data-theme="dusk"] hr {
246
+ border: none;
247
+ border-top: var(--border-width) solid var(--color-border);
248
+ margin: 2rem 0;
249
+ }
250
+
251
+ /* ===== HEADER ===== */
252
+ /* The header-logo <header> has no border of its own. Same construction as
253
+ nebula's divider (see its comment for the inset math), at Dusk's 1px. */
254
+ body[data-theme="dusk"] header[data-has-tabs]::after {
255
+ content: '';
256
+ position: absolute;
257
+ left: -1rem;
258
+ right: -1rem;
259
+ bottom: 0;
260
+ height: 1px;
261
+ background-color: var(--color-border);
262
+ pointer-events: none;
263
+ }
264
+
265
+ @media (min-width: 1024px) {
266
+ body[data-theme="dusk"] header[data-has-tabs]::after {
267
+ left: -1.5rem;
268
+ right: -1.5rem;
269
+ }
270
+ }
271
+
272
+ /* ===== SIDEBAR (jam's spacing, re-scoped) ===== */
273
+ /* The sidebar keeps base's border-r — Dusk separates with borders, so unlike
274
+ jam and halo it does not remove it. */
275
+
276
+ body[data-theme="dusk"] [data-nav-anchor] {
277
+ padding: 0 0 16px 0;
278
+ position: relative;
279
+ margin-left: 24px;
280
+ }
281
+
282
+ body[data-theme="dusk"] [data-nav-anchor]:first-of-type {
283
+ margin-top: 30px;
284
+ }
285
+
286
+ body[data-theme="dusk"] [data-nav-anchor]::before {
287
+ content: '';
288
+ position: absolute;
289
+ top: -8px;
290
+ left: -12px;
291
+ right: 24px;
292
+ bottom: 8px;
293
+ border-radius: var(--radius-md);
294
+ background-color: transparent;
295
+ transition: background-color 0.2s;
296
+ z-index: -1;
297
+ }
298
+
299
+ body[data-theme="dusk"] [data-nav-anchor][data-active="false"]:hover::before {
300
+ background-color: var(--color-bg-hover);
301
+ }
302
+
303
+ body[data-theme="dusk"] .sidebar-scroll ul li a,
304
+ body[data-theme="dusk"] .sidebar-scroll .nav-group-l1 {
305
+ position: relative;
306
+ font-weight: var(--nav-page-weight) !important;
307
+ }
308
+
309
+ /* The highlight is a z-index:-1 pseudo-element inset from the link box, so it
310
+ can bleed left of the text without shifting layout — jam's geometry. */
311
+ body[data-theme="dusk"] .sidebar-scroll ul li a::before,
312
+ body[data-theme="dusk"] .sidebar-scroll .nav-group-l1::before {
313
+ content: '';
314
+ position: absolute;
315
+ top: 0;
316
+ left: -12px;
317
+ right: 16px;
318
+ bottom: 0;
319
+ border-radius: var(--radius-md);
320
+ background-color: transparent;
321
+ z-index: -1;
322
+ transition: background-color 0.15s;
323
+ }
324
+
325
+ body[data-theme="dusk"] .sidebar-scroll ul li a {
326
+ padding-right: 24px !important;
327
+ }
328
+
329
+ body[data-theme="dusk"] .sidebar-scroll ul li a:hover::before,
330
+ body[data-theme="dusk"] .sidebar-scroll .nav-group-l1:hover::before {
331
+ background-color: var(--color-bg-hover);
332
+ }
333
+
334
+ body[data-theme="dusk"] .sidebar-scroll ul li a.nav-active::before {
335
+ background-color: var(--dusk-nav-active-bg);
336
+ }
337
+
338
+ body[data-theme="dusk"] .sidebar-scroll ul li a.nav-active {
339
+ color: var(--color-text-primary) !important;
340
+ font-weight: var(--nav-page-active-weight) !important;
341
+ }
342
+
343
+ body[data-theme="dusk"] .nav-tab-link,
344
+ body[data-theme="dusk"] .toc-link,
345
+ body[data-theme="dusk"] .toc-link-bg {
346
+ border-radius: var(--radius-md) !important;
347
+ }
348
+
349
+ @media (min-width: 1024px) {
350
+ body[data-theme="dusk"] .sidebar-tabs {
351
+ padding-top: calc(2rem + 4px) !important;
352
+ }
353
+ }
354
+
355
+ body[data-theme="dusk"] .sidebar-scroll .sidebar-nav-groups {
356
+ display: flex !important;
357
+ flex-direction: column !important;
358
+ gap: var(--sidebar-item-spacing) !important;
359
+ padding-top: 16px !important;
360
+ padding-bottom: 2rem !important;
361
+ }
362
+
363
+ body[data-theme="dusk"] .sidebar-scroll .sidebar-nav-groups > div {
364
+ display: flex !important;
365
+ flex-direction: column !important;
366
+ gap: var(--sidebar-item-spacing) !important;
367
+ margin-top: 24px !important;
368
+ }
369
+
370
+ body[data-theme="dusk"] .sidebar-scroll .sidebar-nav-groups > div:first-child {
371
+ margin-top: 0 !important;
372
+ }
373
+
374
+ @media (min-width: 1024px) {
375
+ body[data-theme="dusk"] .sidebar-scroll nav > .sidebar-nav-groups:first-child {
376
+ padding-top: 2.5rem !important;
377
+ }
378
+ }
379
+
380
+ /* Height cap — REQUIRED for a tall nav to scroll. Fill the layout row rather
381
+ than measuring 100vh minus a hardcoded header (theme-sidebar-height.test.ts
382
+ fails the old calc form). Pairs with `lg:max-h-full` on the <aside>. */
383
+ @media (min-width: 1024px) {
384
+ body[data-theme="dusk"] .sidebar-scroll {
385
+ position: sticky !important;
386
+ top: 0 !important;
387
+ flex: 1 1 auto !important;
388
+ min-height: 0 !important;
389
+ max-height: none !important;
390
+ }
391
+ }
392
+
393
+ body[data-theme="dusk"] .sidebar-scroll ul {
394
+ display: flex !important;
395
+ flex-direction: column !important;
396
+ gap: var(--sidebar-item-spacing) !important;
397
+ }
398
+
399
+ body[data-theme="dusk"] .sidebar-scroll nav div div {
400
+ display: flex !important;
401
+ flex-direction: column !important;
402
+ gap: var(--sidebar-item-spacing) !important;
403
+ }
404
+
405
+ /* ===== CODE PANELS ===== */
406
+ /* Every theme must pad this — base.css gives it none (REFERENCE.md
407
+ #per-theme-code-panel-gutter-invariant). jam's value. */
408
+ @media (min-width: 1024px) {
409
+ body[data-theme="dusk"] .code-panels-sidebar {
410
+ padding-right: 16px;
411
+ }
412
+ }
413
+
414
+ /* ===== MOBILE ===== */
415
+ @media (max-width: 1023px) {
416
+ body[data-theme="dusk"] header[data-has-tabs] > div {
417
+ margin: 0 !important;
418
+ padding-left: 0.5rem;
419
+ padding-right: 0.5rem;
420
+ }
421
+
422
+ body[data-theme="dusk"] #main-content article {
423
+ padding-left: 0.5rem !important;
424
+ padding-right: 0.5rem !important;
425
+ }
426
+ }
@@ -100,12 +100,40 @@ const haloTheme: ThemeConfig = {
100
100
  cssPath: 'halo/variables.css',
101
101
  };
102
102
 
103
+ const duskTheme: ThemeConfig = {
104
+ name: 'dusk',
105
+ displayName: 'Dusk',
106
+ description: 'Quiet and flat — one calm surface, clear borders, high-contrast text and a serif title',
107
+ layout: 'header-logo',
108
+ defaultTabsPosition: 'left',
109
+ fonts: {
110
+ // Matches jam's own Inter config — jam is the source of truth for Inter.
111
+ primary: {
112
+ family: 'Inter',
113
+ googleFontUrl: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
114
+ fallback: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
115
+ },
116
+ mono: {
117
+ family: 'JetBrains Mono',
118
+ googleFontUrl: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap',
119
+ fallback: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
120
+ },
121
+ heading: {
122
+ family: 'IBM Plex Serif',
123
+ googleFontUrl: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@400;600&display=swap',
124
+ fallback: "'Times New Roman', serif",
125
+ },
126
+ },
127
+ cssPath: 'dusk/variables.css',
128
+ };
129
+
103
130
  // Theme registry
104
131
  const themes: Record<ThemeName, ThemeConfig> = {
105
132
  jam: jamTheme,
106
133
  nebula: nebulaTheme,
107
134
  pulsar: pulsarTheme,
108
135
  halo: haloTheme,
136
+ dusk: duskTheme,
109
137
  };
110
138
 
111
139
  /**
@@ -235,11 +235,12 @@ html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-d
235
235
  background-color: transparent !important;
236
236
  }
237
237
 
238
- html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-decoration="grid"]) > div > div.flex:not([role="dialog"]):not(:has([data-chat-panel])) {
239
- background-color: transparent !important;
240
- }
241
-
242
- html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-decoration="grid"]) > div > div.flex > main {
238
+ /* Anchored on [data-jd-layout-root], not a bare `> div`: CodePanelModal
239
+ portals a `div[role="dialog"] > div.flex` into <body>, and the bare form
240
+ stripped the modal panel's own background (a `:not([role="dialog"])` on the
241
+ inner div guarded the wrong level). agent-directive-theme-isolation.test.ts
242
+ keeps body-anchored rules off every other <body> child. */
243
+ html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-decoration="grid"]) > div[data-jd-layout-root] > div.flex:not(:has([data-chat-panel])) {
243
244
  background-color: transparent !important;
244
245
  }
245
246
 
@@ -248,7 +248,7 @@ body[data-theme="nebula"] .sidebar-tabs {
248
248
  /* Header divider. Targets the nav header by its data-has-tabs attribute, not a
249
249
  child-combinator chain from <body>: the old `body > div > header` form broke
250
250
  silently when LayoutWrapper nested the header three levels down, and Nebula
251
- shipped with no divider (nebula-header-divider.test.tsx). The header is
251
+ shipped with no divider (theme-layout-selectors.test.tsx). The header is
252
252
  sticky, which already anchors ::after, so it needs no position of its own.
253
253
  The -1rem / -1.5rem insets span the layout column's `px-4 lg:px-6`: the line
254
254
  runs edge to edge up to --layout-max-width without lifting the column's
@@ -1019,9 +1019,10 @@ body[data-theme="pulsar"] header [data-theme-toggle] {
1019
1019
  background-color: var(--color-bg-primary) !important;
1020
1020
  }
1021
1021
 
1022
- /* Ensure content area starts at top with no gap. :not() spares the
1023
- sr-only agent directive, which is also a body > div. */
1024
- body[data-theme="pulsar"] > div:not([data-jd-agent-directive]) {
1022
+ /* Ensure content area starts at top with no gap. [data-jd-layout-root],
1023
+ not a bare `> div`: <body> also holds the sr-only agent directive and
1024
+ modal portals (theme-layout-selectors.test.tsx). */
1025
+ body[data-theme="pulsar"] > div[data-jd-layout-root] {
1025
1026
  min-height: 100vh;
1026
1027
  min-height: 100dvh; /* Dynamic viewport height for mobile browsers */
1027
1028
  }
@@ -67,6 +67,13 @@ export interface ThemeConfig {
67
67
  googleFontUrl?: string;
68
68
  fallback: string;
69
69
  };
70
+ /** Optional heading face (Dusk's serif H1). Loaded by next/font in
71
+ * production; the dashboard editor preview links googleFontUrl. */
72
+ heading?: {
73
+ family: string;
74
+ googleFontUrl?: string;
75
+ fallback: string;
76
+ };
70
77
  };
71
78
 
72
79
  /** CSS file path relative to themes directory */
@@ -82,6 +89,7 @@ export type ThemeName =
82
89
  | 'nebula'
83
90
  | 'pulsar'
84
91
  | 'halo'
92
+ | 'dusk'
85
93
  // Future themes can be added here:
86
94
  // | 'linden'
87
95
  // | 'willow'