Skip to content

Commit ed96a82

Browse files
feat: theming.
1 parent fd9e261 commit ed96a82

5 files changed

Lines changed: 432 additions & 124 deletions

File tree

docs/next-steps.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,3 @@ Focused follow-up work for `@knighted/develop`.
99
2. **Preview UX polish**
1010
- Keep tooltip affordances for mode-specific behavior.
1111
- Continue tightening panel control alignment and spacing without introducing extra markup.
12-
13-
3. **Theming (light + dark)**
14-
- Keep the existing dark mode as the baseline and add a first-class light theme.
15-
- Move key colors to semantic CSS variables and define both theme palettes.
16-
- Ensure component panels, controls, editor chrome, preview shell, and tooltips all have complete light-mode coverage.
17-
- Verify contrast/accessibility across both themes and preserve visual hierarchy parity.

src/app.js

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { defaultCss, defaultJsx } from './defaults.js'
55
const statusNode = document.getElementById('status')
66
const appGrid = document.querySelector('.app-grid')
77
const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]')
8+
const appThemeButtons = document.querySelectorAll('[data-app-theme]')
89
const renderMode = document.getElementById('render-mode')
910
const autoRenderToggle = document.getElementById('auto-render')
1011
const renderButton = document.getElementById('render-button')
@@ -45,8 +46,10 @@ let compiledStylesCache = {
4546
let pendingClearAction = null
4647
let hasCompletedInitialRender = false
4748
let previewBackgroundColor = null
49+
let previewBackgroundCustomized = false
4850
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
4951
const appGridLayoutStorageKey = 'knighted-develop:app-grid-layout'
52+
const appThemeStorageKey = 'knighted-develop:theme'
5053

5154
const styleLabels = {
5255
css: 'Native CSS',
@@ -182,6 +185,41 @@ const getInitialAppGridLayout = () => {
182185
return 'default'
183186
}
184187

188+
const applyTheme = (theme, { persist = true } = {}) => {
189+
if (!['dark', 'light'].includes(theme)) {
190+
return
191+
}
192+
193+
document.documentElement.dataset.theme = theme
194+
syncPreviewBackgroundPickerFromTheme()
195+
196+
for (const button of appThemeButtons) {
197+
const isActive = button.dataset.appTheme === theme
198+
button.setAttribute('aria-pressed', isActive ? 'true' : 'false')
199+
}
200+
201+
if (persist) {
202+
try {
203+
localStorage.setItem(appThemeStorageKey, theme)
204+
} catch {
205+
/* Ignore storage write errors in restricted browsing modes. */
206+
}
207+
}
208+
}
209+
210+
const getInitialTheme = () => {
211+
try {
212+
const value = localStorage.getItem(appThemeStorageKey)
213+
if (value === 'dark' || value === 'light') {
214+
return value
215+
}
216+
} catch {
217+
/* Ignore storage read errors in restricted browsing modes. */
218+
}
219+
220+
return 'dark'
221+
}
222+
185223
const setCdnLoading = isLoading => {
186224
if (!cdnLoading) return
187225
cdnLoading.hidden = !isLoading
@@ -326,7 +364,24 @@ const applyPreviewBackgroundColor = color => {
326364
return
327365
}
328366

329-
previewHost.style.backgroundColor = color
367+
if (typeof color === 'string' && color.length > 0) {
368+
previewHost.style.backgroundColor = color
369+
return
370+
}
371+
372+
previewHost.style.removeProperty('background-color')
373+
}
374+
375+
const syncPreviewBackgroundPickerFromTheme = () => {
376+
if (!previewBgColorInput || !previewHost || previewBackgroundCustomized) {
377+
return
378+
}
379+
380+
previewBackgroundColor = null
381+
applyPreviewBackgroundColor(null)
382+
previewBgColorInput.value = normalizeColorToHex(
383+
getComputedStyle(previewHost).backgroundColor,
384+
)
330385
}
331386

332387
const initializePreviewBackgroundPicker = () => {
@@ -335,12 +390,14 @@ const initializePreviewBackgroundPicker = () => {
335390
}
336391

337392
const initialColor = normalizeColorToHex(getComputedStyle(previewHost).backgroundColor)
338-
previewBackgroundColor = initialColor
393+
previewBackgroundColor = null
394+
previewBackgroundCustomized = false
339395
previewBgColorInput.value = initialColor
340-
applyPreviewBackgroundColor(initialColor)
396+
applyPreviewBackgroundColor(null)
341397

342398
previewBgColorInput.addEventListener('input', () => {
343399
previewBackgroundColor = previewBgColorInput.value
400+
previewBackgroundCustomized = true
344401
applyPreviewBackgroundColor(previewBackgroundColor)
345402
})
346403
}
@@ -352,9 +409,7 @@ const recreatePreviewHost = () => {
352409
previewHost.replaceWith(nextHost)
353410
previewHost = nextHost
354411

355-
if (previewBackgroundColor) {
356-
applyPreviewBackgroundColor(previewBackgroundColor)
357-
}
412+
applyPreviewBackgroundColor(previewBackgroundColor)
358413
}
359414

360415
const getRenderTarget = () => {
@@ -942,7 +997,18 @@ for (const button of appGridLayoutButtons) {
942997
})
943998
}
944999

1000+
for (const button of appThemeButtons) {
1001+
button.addEventListener('click', () => {
1002+
const nextTheme = button.dataset.appTheme
1003+
if (!nextTheme) {
1004+
return
1005+
}
1006+
applyTheme(nextTheme)
1007+
})
1008+
}
1009+
9451010
applyAppGridLayout(getInitialAppGridLayout(), { persist: false })
1011+
applyTheme(getInitialTheme(), { persist: false })
9461012

9471013
updateRenderButtonVisibility()
9481014
setStyleCompiling(false)

src/editor-codemirror.js

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -118,33 +118,65 @@ export const createCodeMirrorEditor = async ({
118118
onFocus,
119119
}) => {
120120
const runtime = await ensureCodeMirrorRuntime()
121+
const editorColors = {
122+
keyword: 'var(--cm-keyword)',
123+
name: 'var(--cm-name)',
124+
property: 'var(--cm-property)',
125+
fn: 'var(--cm-function)',
126+
constant: 'var(--cm-constant)',
127+
definition: 'var(--cm-definition)',
128+
type: 'var(--cm-type)',
129+
number: 'var(--cm-number)',
130+
operator: 'var(--cm-operator)',
131+
string: 'var(--cm-string)',
132+
comment: 'var(--cm-comment)',
133+
link: 'var(--cm-link)',
134+
heading: 'var(--cm-heading)',
135+
atom: 'var(--cm-atom)',
136+
invalid: 'var(--cm-invalid)',
137+
text: 'var(--cm-text)',
138+
caret: 'var(--cm-caret)',
139+
gutterBg: 'var(--cm-gutter-bg)',
140+
gutterBorder: 'var(--cm-gutter-border)',
141+
gutterText: 'var(--cm-gutter-text)',
142+
selection: 'var(--cm-selection)',
143+
activeLine: 'var(--cm-active-line)',
144+
focusRing: 'var(--cm-focus-ring)',
145+
tooltipBg: 'var(--cm-tooltip-bg)',
146+
tooltipText: 'var(--cm-tooltip-text)',
147+
tooltipBorder: 'var(--cm-tooltip-border)',
148+
tooltipItem: 'var(--cm-tooltip-item)',
149+
tooltipItemSelectedBg: 'var(--cm-tooltip-item-selected-bg)',
150+
tooltipItemSelectedText: 'var(--cm-tooltip-item-selected-text)',
151+
}
152+
121153
const languageCompartment = new runtime.Compartment()
122154
const editorHighlightStyle = runtime.HighlightStyle.define([
123-
{ tag: runtime.tags.keyword, color: '#ff7fb3', fontWeight: '600' },
124-
{ tag: [runtime.tags.name, runtime.tags.deleted], color: '#e7ecf9' },
155+
{ tag: runtime.tags.keyword, color: editorColors.keyword, fontWeight: '600' },
156+
{ tag: [runtime.tags.name, runtime.tags.deleted], color: editorColors.name },
125157
{
126158
tag: [runtime.tags.character, runtime.tags.propertyName, runtime.tags.macroName],
127-
color: '#3fd6a6',
159+
color: editorColors.property,
128160
},
129161
{
130162
tag: [runtime.tags.function(runtime.tags.variableName), runtime.tags.labelName],
131-
color: '#8dc8ff',
163+
color: editorColors.fn,
132164
},
133165
{
134166
tag: [
135167
runtime.tags.color,
136168
runtime.tags.constant(runtime.tags.name),
137169
runtime.tags.standard(runtime.tags.name),
138170
],
139-
color: '#7fd7ff',
171+
color: editorColors.constant,
140172
},
141173
{
142174
tag: [runtime.tags.definition(runtime.tags.name), runtime.tags.separator],
143-
color: '#dce4f6',
175+
color: editorColors.definition,
144176
},
145177
{
146178
tag: [runtime.tags.className, runtime.tags.typeName],
147-
color: '#8eb8ff',
179+
color: editorColors.type,
148180
fontWeight: '600',
149181
},
150182
{
@@ -156,19 +188,19 @@ export const createCodeMirrorEditor = async ({
156188
runtime.tags.self,
157189
runtime.tags.namespace,
158190
],
159-
color: '#ffcb82',
191+
color: editorColors.number,
160192
},
161193
{
162194
tag: [runtime.tags.operator, runtime.tags.operatorKeyword],
163-
color: '#d5def0',
195+
color: editorColors.operator,
164196
},
165197
{
166198
tag: [runtime.tags.string, runtime.tags.special(runtime.tags.string)],
167-
color: '#ffd38e',
199+
color: editorColors.string,
168200
},
169201
{
170202
tag: [runtime.tags.meta, runtime.tags.comment],
171-
color: '#94a2bb',
203+
color: editorColors.comment,
172204
fontStyle: 'italic',
173205
},
174206
{
@@ -181,12 +213,12 @@ export const createCodeMirrorEditor = async ({
181213
},
182214
{
183215
tag: runtime.tags.link,
184-
color: '#88b6ff',
216+
color: editorColors.link,
185217
textDecoration: 'underline',
186218
},
187219
{
188220
tag: runtime.tags.heading,
189-
color: '#f2f5ff',
221+
color: editorColors.heading,
190222
fontWeight: '700',
191223
},
192224
{
@@ -195,19 +227,19 @@ export const createCodeMirrorEditor = async ({
195227
runtime.tags.bool,
196228
runtime.tags.special(runtime.tags.variableName),
197229
],
198-
color: '#b8a8ff',
230+
color: editorColors.atom,
199231
},
200232
{
201233
tag: runtime.tags.invalid,
202-
color: '#ff8fa1',
203-
textDecoration: 'underline wavy #ff8fa1',
234+
color: editorColors.invalid,
235+
textDecoration: `underline wavy ${editorColors.invalid}`,
204236
},
205237
])
206238
const editorTheme = runtime.EditorView.theme({
207239
'&': {
208240
height: '100%',
209241
backgroundColor: 'transparent',
210-
color: '#edf2ff',
242+
color: editorColors.text,
211243
fontSize: '0.9rem',
212244
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
213245
},
@@ -218,39 +250,39 @@ export const createCodeMirrorEditor = async ({
218250
'.cm-content': {
219251
padding: '16px 18px',
220252
minHeight: '100%',
221-
caretColor: '#f1f5ff',
253+
caretColor: editorColors.caret,
222254
},
223255
'.cm-gutters': {
224-
backgroundColor: 'rgba(255, 255, 255, 0.045)',
225-
borderRight: '1px solid rgba(255, 255, 255, 0.13)',
226-
color: '#98a8c4',
256+
backgroundColor: editorColors.gutterBg,
257+
borderRight: `1px solid ${editorColors.gutterBorder}`,
258+
color: editorColors.gutterText,
227259
},
228260
'.cm-lineNumbers .cm-gutterElement': {
229261
padding: '0 10px 0 14px',
230262
},
231263
'&.cm-focused .cm-cursor': {
232-
borderLeftColor: '#f1f5ff',
264+
borderLeftColor: editorColors.caret,
233265
},
234266
'&.cm-focused .cm-selectionBackground, ::selection': {
235-
backgroundColor: 'rgba(122, 107, 255, 0.36)',
267+
backgroundColor: editorColors.selection,
236268
},
237269
'&.cm-focused .cm-activeLine': {
238-
backgroundColor: 'rgba(255, 255, 255, 0.08)',
270+
backgroundColor: editorColors.activeLine,
239271
},
240272
'&.cm-focused': {
241-
outline: '1px solid rgba(122, 107, 255, 0.62)',
273+
outline: `1px solid ${editorColors.focusRing}`,
242274
},
243275
'.cm-tooltip': {
244-
backgroundColor: '#1b2233',
245-
color: '#edf2ff',
246-
border: '1px solid rgba(152, 168, 196, 0.32)',
276+
backgroundColor: editorColors.tooltipBg,
277+
color: editorColors.tooltipText,
278+
border: `1px solid ${editorColors.tooltipBorder}`,
247279
},
248280
'.cm-tooltip-autocomplete > ul > li': {
249-
color: '#dce6fa',
281+
color: editorColors.tooltipItem,
250282
},
251283
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
252-
backgroundColor: 'rgba(122, 107, 255, 0.34)',
253-
color: '#f4f7ff',
284+
backgroundColor: editorColors.tooltipItemSelectedBg,
285+
color: editorColors.tooltipItemSelectedText,
254286
},
255287
})
256288
const updateListener = runtime.EditorView.updateListener.of(update => {

src/index.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,43 @@ <h1>
7070
<rect x="13" y="13" width="8" height="8" rx="1"></rect>
7171
</svg>
7272
</button>
73+
74+
<div class="app-grid-theme-controls" role="group" aria-label="Theme">
75+
<button
76+
class="layout-toggle"
77+
id="theme-dark"
78+
type="button"
79+
data-app-theme="dark"
80+
aria-pressed="true"
81+
title="Use dark theme"
82+
aria-label="Use dark theme"
83+
>
84+
<svg viewBox="0 0 24 24" aria-hidden="true">
85+
<path d="M21 13.5A8.5 8.5 0 1 1 10.5 3 6.8 6.8 0 0 0 21 13.5z"></path>
86+
</svg>
87+
</button>
88+
<button
89+
class="layout-toggle"
90+
id="theme-light"
91+
type="button"
92+
data-app-theme="light"
93+
aria-pressed="false"
94+
title="Use light theme"
95+
aria-label="Use light theme"
96+
>
97+
<svg viewBox="0 0 24 24" aria-hidden="true">
98+
<circle cx="12" cy="12" r="4"></circle>
99+
<path d="M12 2v3"></path>
100+
<path d="M12 19v3"></path>
101+
<path d="M2 12h3"></path>
102+
<path d="M19 12h3"></path>
103+
<path d="m4.93 4.93 2.12 2.12"></path>
104+
<path d="m16.95 16.95 2.12 2.12"></path>
105+
<path d="m19.07 4.93-2.12 2.12"></path>
106+
<path d="m7.05 16.95-2.12 2.12"></path>
107+
</svg>
108+
</button>
109+
</div>
73110
</div>
74111

75112
<section class="panel component-panel">

0 commit comments

Comments
 (0)