Skip to content

Commit 8c7b6b2

Browse files
feat: typography workspace selection.
1 parent 6f83b8d commit 8c7b6b2

16 files changed

Lines changed: 532 additions & 2 deletions

playwright/rendering-modes/core.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page
128128

129129
return {
130130
renderMode: typeof latest.renderMode === 'string' ? latest.renderMode : '',
131+
fontCssUrl: typeof latest.fontCssUrl === 'string' ? latest.fontCssUrl : '',
131132
styleLanguage:
132133
typeof primaryStylesTab?.language === 'string' ? primaryStylesTab.language : '',
133134
}
@@ -147,6 +148,12 @@ const readPreviewUserStyleText = async (page: import('@playwright/test').Page) =
147148
})
148149
}
149150

151+
const readPreviewBodyFontFamily = async (page: import('@playwright/test').Page) => {
152+
return getPreviewFrame(page)
153+
.locator('html')
154+
.evaluate(() => getComputedStyle(document.body).fontFamily || '')
155+
}
156+
150157
test.beforeEach(async ({ page }) => {
151158
await resetWorkbenchStorage(page)
152159
})
@@ -228,6 +235,46 @@ test('reactJsx tag interpolation renders memo and forwardRef components', async
228235
.toBe(0)
229236
})
230237

238+
test('workspace font CSS URL applies to preview and persists per workspace', async ({
239+
page,
240+
}) => {
241+
await waitForInitialRender(page)
242+
243+
await page.getByRole('button', { name: 'Workspaces' }).click()
244+
245+
const fontCssUrlInput = page.getByRole('textbox', {
246+
name: 'Workspace Font CSS URL',
247+
})
248+
await fontCssUrlInput.fill(
249+
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
250+
)
251+
await fontCssUrlInput.press('Tab')
252+
253+
await expect
254+
.poll(async () => (await readPreviewBodyFontFamily(page)).toLowerCase())
255+
.toContain('ibm plex sans')
256+
257+
await expect
258+
.poll(async () => {
259+
const snapshot = await readLatestWorkspaceSnapshot(page)
260+
return snapshot?.fontCssUrl ?? ''
261+
})
262+
.toBe(
263+
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
264+
)
265+
266+
await page.reload()
267+
await waitForInitialRender(page)
268+
await page.getByRole('button', { name: 'Workspaces' }).click()
269+
270+
await expect(fontCssUrlInput).toHaveValue(
271+
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
272+
)
273+
await expect
274+
.poll(async () => (await readPreviewBodyFontFamily(page)).toLowerCase())
275+
.toContain('ibm plex sans')
276+
})
277+
231278
test('react mode keeps App.ts entry but surfaces rename guidance until compatible', async ({
232279
page,
233280
}) => {

src/app.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ import { createGitHubPrDrawer } from './modules/github/pr/drawer/controller/crea
6666
import { createLayoutThemeController } from './modules/ui/layout-theme.js'
6767
import { createLintDiagnosticsController } from './modules/diagnostics/lint-diagnostics.js'
6868
import { createPreviewBackgroundController } from './modules/preview/preview-background.js'
69+
import {
70+
createPreviewFontController,
71+
defaultPreviewFontCssUrl,
72+
normalizePreviewFontCssUrl,
73+
} from './modules/preview/preview-font.js'
6974
import { getReactEntryTabCompatibilityError } from './modules/preview/preview-entry-resolver.js'
7075
import { createRenderRuntimeController } from './modules/preview/render-runtime.js'
7176
import { createTypeDiagnosticsController } from './modules/diagnostics/type-diagnostics.js'
@@ -151,6 +156,8 @@ const workspacesInitialize = document.getElementById('workspaces-initialize')
151156
const workspacesShare = document.getElementById('workspaces-share')
152157
const workspacesNew = document.getElementById('workspaces-new')
153158
const workspacesSelect = document.getElementById('workspaces-select')
159+
const workspacesFontCssUrlInput = document.getElementById('workspaces-font-css-url')
160+
const workspacesFontCssUrlLoad = document.getElementById('workspaces-font-css-url-load')
154161
const workspacesOpen = document.getElementById('workspaces-open')
155162
const workspacesRename = document.getElementById('workspaces-rename')
156163
const workspacesRemove = document.getElementById('workspaces-remove')
@@ -349,6 +356,20 @@ const previewBackground = createPreviewBackgroundController({
349356
},
350357
})
351358

359+
const previewFont = createPreviewFontController({
360+
previewFontCssUrlInput: workspacesFontCssUrlInput,
361+
getDefaultPreviewFontCssUrl: () => defaultPreviewFontCssUrl,
362+
onFontConfigChange: ({ fontCssUrl, fontFamily }) => {
363+
if (renderRuntime && typeof renderRuntime.updatePreviewFont === 'function') {
364+
renderRuntime.updatePreviewFont({ fontCssUrl, fontFamily })
365+
}
366+
367+
if (typeof queueWorkspaceSave === 'function') {
368+
queueWorkspaceSave()
369+
}
370+
},
371+
})
372+
352373
const layoutTheme = createLayoutThemeController({
353374
appThemeButtons,
354375
syncPreviewBackgroundPickerFromTheme: () =>
@@ -757,7 +778,9 @@ const workspaceSyncController = createWorkspaceSyncController({
757778
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
758779
getActiveWorkspaceCreatedAt: () => activeWorkspaceCreatedAt,
759780
getRenderModeValue: () => renderMode.value,
781+
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
760782
normalizeRenderMode: mode => normalizeRenderMode(mode),
783+
normalizePreviewFontCssUrl,
761784
})
762785

763786
const getTypecheckSourcePath = () =>
@@ -891,10 +914,15 @@ const {
891914
workspaceTabsState,
892915
resolveWorkspaceActiveTabId,
893916
normalizeRenderMode: mode => normalizeRenderMode(mode),
917+
normalizePreviewFontCssUrl,
894918
getRenderModeValue: () => renderMode.value,
919+
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
895920
setRenderModeValue: value => {
896921
renderMode.value = value
897922
},
923+
setPreviewFontCssUrlValue: (value, options) => {
924+
previewFont.applyPreviewFontCssUrl(value, options)
925+
},
898926
getActiveWorkspaceTab,
899927
onActiveWorkspaceTabChange: (_tab, { changed } = {}) => {
900928
syncDiagnosticsDrawerLayout()
@@ -1201,6 +1229,8 @@ const githubWorkflows = createGitHubWorkflowsSetup({
12011229
workspacesShare,
12021230
workspacesNew,
12031231
workspacesSelect,
1232+
workspacesFontCssUrlInput,
1233+
workspacesFontCssUrlLoad,
12041234
workspacesOpen,
12051235
workspacesRename,
12061236
workspacesRemove,
@@ -1220,6 +1250,14 @@ const githubWorkflows = createGitHubWorkflowsSetup({
12201250
listLocalContextRecords,
12211251
refreshLocalContextOptions,
12221252
applyWorkspaceRecord,
1253+
applyWorkspaceFontCssUrl: async fontCssUrl => {
1254+
previewFont.applyPreviewFontCssUrl(fontCssUrl, {
1255+
emitChange: true,
1256+
syncInputValue: true,
1257+
})
1258+
await flushWorkspaceSave({ preserveRecordId: true })
1259+
return true
1260+
},
12231261
syncActiveWorkspaceRepositoryScope,
12241262
forkWorkspaceFromCurrentState,
12251263
flushWorkspaceSave,
@@ -1424,6 +1462,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
14241462
getRenderRuntime: () => renderRuntime,
14251463
getPreviewHost: () => previewHost,
14261464
previewBackground,
1465+
previewFont,
14271466
clearDiagnosticsScope,
14281467
clearConfirmDialog,
14291468
clearConfirmTitle,
@@ -1602,6 +1641,7 @@ bindAppEventsAndStart({
16021641
typeDiagnostics,
16031642
clipboardSupported,
16041643
previewBackground,
1644+
previewFont,
16051645
initializeCodeEditors,
16061646
},
16071647
})

src/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,26 @@ <h2 id="workspaces-title">Workspaces</h2>
806806
</div>
807807
</div>
808808

809+
<div class="workspaces-drawer__font-row">
810+
<label
811+
class="github-pr-field github-pr-field--full"
812+
for="workspaces-font-css-url"
813+
>
814+
<span>Font CSS URL</span>
815+
<input
816+
id="workspaces-font-css-url"
817+
type="text"
818+
autocomplete="off"
819+
spellcheck="false"
820+
aria-label="Workspace Font CSS URL"
821+
placeholder="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap"
822+
/>
823+
</label>
824+
<button class="render-button" id="workspaces-font-css-url-load" type="button">
825+
Load
826+
</button>
827+
</div>
828+
809829
<label class="github-pr-field github-pr-field--full" for="workspaces-select">
810830
<span>Workspace</span>
811831
<select id="workspaces-select" aria-label="Stored workspace">

src/modules/app-core/app-bindings-startup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const bindAppEventsAndStart = ({
120120
typeDiagnostics,
121121
clipboardSupported,
122122
previewBackground,
123+
previewFont,
123124
initializeCodeEditors,
124125
} = startup
125126

@@ -495,6 +496,7 @@ const bindAppEventsAndStart = ({
495496
syncDiagnosticsDrawerLayout()
496497
renderRuntime.setStyleCompiling(false)
497498
setCdnLoading(true)
499+
previewFont.initializePreviewFontInput()
498500
previewBackground.initializePreviewBackgroundPicker()
499501
const workspaceRestoreReady = (async () => {
500502
try {

src/modules/app-core/app-composition-options.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const createRuntimeCoreOptions = ({
3636
getRenderRuntime,
3737
getPreviewHost,
3838
previewBackground,
39+
previewFont,
3940
clearDiagnosticsScope,
4041
clearConfirmDialog,
4142
clearConfirmTitle,
@@ -99,6 +100,8 @@ const createRuntimeCoreOptions = ({
99100
getWorkspaceTabs: () => buildWorkspaceTabsSnapshot(),
100101
getPreviewHost,
101102
getPreviewBackgroundColor: () => previewBackground.getPreviewBackgroundColor(),
103+
getPreviewFontCssUrl: () => previewFont.getPreviewFontCssUrl(),
104+
getPreviewFontFamily: () => previewFont.getPreviewFontFamily(),
102105
clearStyleDiagnostics: () => clearDiagnosticsScope('styles'),
103106
setStyleDiagnosticsDetails,
104107
setStatus,
@@ -280,6 +283,7 @@ const createAppStartupBindingsOptions = ({
280283
setTypeDiagnosticsDetails,
281284
setCdnLoading,
282285
previewBackground,
286+
previewFont,
283287
loadPreferredWorkspaceContext,
284288
initializeCodeEditors,
285289
getActiveWorkspaceTab,
@@ -376,6 +380,7 @@ const createAppStartupBindingsOptions = ({
376380
setTypeDiagnosticsDetails,
377381
setCdnLoading,
378382
previewBackground,
383+
previewFont,
379384
loadPreferredWorkspaceContext,
380385
initializeCodeEditors,
381386
getActiveWorkspaceTab,

src/modules/app-core/github-workflows-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const createGitHubWorkflowsSetup = ({
6060
getWorkspaceTabContexts: actions.getWorkspaceTabContexts,
6161
applyWorkspaceTabContent: actions.applyWorkspaceTabContent,
6262
scheduleRender: actions.scheduleRender,
63+
applyWorkspaceFontCssUrl: workspace.applyWorkspaceFontCssUrl,
6364
})
6465

6566
export { createGitHubWorkflowsSetup }

src/modules/app-core/github-workflows.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const initializeGitHubWorkflows = ({
4646
workspacesShare,
4747
workspacesNew,
4848
workspacesSelect,
49+
workspacesFontCssUrlInput,
50+
workspacesFontCssUrlLoad,
4951
workspacesOpen,
5052
workspacesRename,
5153
workspacesRemove,
@@ -59,6 +61,7 @@ const initializeGitHubWorkflows = ({
5961
listLocalContextRecords,
6062
refreshLocalContextOptions,
6163
applyWorkspaceRecord,
64+
applyWorkspaceFontCssUrl,
6265
syncActiveWorkspaceRepositoryScope,
6366
forkWorkspaceFromCurrentState,
6467
flushWorkspaceSave,
@@ -432,6 +435,8 @@ const initializeGitHubWorkflows = ({
432435
shareButton: workspacesShare,
433436
newButton: workspacesNew,
434437
selectInput: workspacesSelect,
438+
fontCssUrlInput: workspacesFontCssUrlInput,
439+
fontCssUrlLoadButton: workspacesFontCssUrlLoad,
435440
openButton: workspacesOpen,
436441
renameButton: workspacesRename,
437442
removeButton: workspacesRemove,
@@ -540,8 +545,30 @@ const initializeGitHubWorkflows = ({
540545
return false
541546
}
542547
},
548+
onLoadFontCssUrl: async fontCssUrl => {
549+
if (typeof applyWorkspaceFontCssUrl !== 'function') {
550+
return false
551+
}
552+
553+
try {
554+
await applyWorkspaceFontCssUrl(fontCssUrl)
555+
if (typeof scheduleRender === 'function') {
556+
await Promise.resolve(scheduleRender())
557+
}
558+
return true
559+
} catch {
560+
workspacesDrawerController?.setStatus('Could not load font CSS URL.', 'error')
561+
return false
562+
}
563+
},
543564
onOpenSelected: async workspaceId => {
544565
try {
566+
try {
567+
await flushWorkspaceSave?.({ preserveRecordId: true })
568+
} catch {
569+
/* Save failures are surfaced via saver onError; continue with open attempt. */
570+
}
571+
545572
const record = await workspaceStorage.getWorkspaceById(workspaceId)
546573
if (!record) {
547574
await refreshLocalContextOptions()

src/modules/app-core/workspace-context-controller.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ const createWorkspaceContextController = ({
1818
workspaceTabsState,
1919
resolveWorkspaceActiveTabId,
2020
normalizeRenderMode,
21+
normalizePreviewFontCssUrl,
2122
getRenderModeValue,
23+
getPreviewFontCssUrlValue,
2224
setRenderModeValue,
25+
setPreviewFontCssUrlValue,
2326
onWorkspaceRecordApplied,
2427
getActiveWorkspaceTab,
2528
loadWorkspaceTabIntoEditor,
@@ -140,6 +143,21 @@ const createWorkspaceContextController = ({
140143
setRenderModeValue(nextRenderMode)
141144
}
142145

146+
if (typeof setPreviewFontCssUrlValue === 'function') {
147+
const nextPreviewFontCssUrl = normalizePreviewFontCssUrl(
148+
workspace.fontCssUrl ?? workspace.previewFontCssUrl,
149+
)
150+
const currentPreviewFontCssUrl = normalizePreviewFontCssUrl(
151+
getPreviewFontCssUrlValue?.(),
152+
)
153+
if (currentPreviewFontCssUrl !== nextPreviewFontCssUrl) {
154+
setPreviewFontCssUrlValue(nextPreviewFontCssUrl, {
155+
emitChange: true,
156+
syncInputValue: true,
157+
})
158+
}
159+
}
160+
143161
const activeTab = getActiveWorkspaceTab()
144162
if (activeTab) {
145163
loadWorkspaceTabIntoEditor(activeTab)

src/modules/app-core/workspace-controllers-setup.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ const createWorkspaceControllersSetup = ({
2929
workspaceTabsState,
3030
resolveWorkspaceActiveTabId,
3131
normalizeRenderMode,
32+
normalizePreviewFontCssUrl,
3233
getRenderModeValue,
34+
getPreviewFontCssUrlValue,
3335
setRenderModeValue,
36+
setPreviewFontCssUrlValue,
3437
onWorkspaceRecordApplied,
3538
getActiveWorkspaceTab,
3639
onActiveWorkspaceTabChange,
@@ -222,8 +225,11 @@ const createWorkspaceControllersSetup = ({
222225
workspaceTabsState,
223226
resolveWorkspaceActiveTabId,
224227
normalizeRenderMode,
228+
normalizePreviewFontCssUrl,
225229
getRenderModeValue,
230+
getPreviewFontCssUrlValue,
226231
setRenderModeValue,
232+
setPreviewFontCssUrlValue,
227233
onWorkspaceRecordApplied,
228234
getActiveWorkspaceTab,
229235
loadWorkspaceTabIntoEditor,

src/modules/app-core/workspace-sync-controller.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ const createWorkspaceSyncController = ({
1717
getActiveWorkspaceRecordId,
1818
getActiveWorkspaceCreatedAt,
1919
getRenderModeValue,
20+
getPreviewFontCssUrlValue = () => '',
2021
normalizeRenderMode,
22+
normalizePreviewFontCssUrl = value => value,
2123
}) => {
2224
const removedWorkspaceTabPathsByWorkspaceKey = new Map()
2325

@@ -399,6 +401,7 @@ const createWorkspaceSyncController = ({
399401
prTitle: normalizedPrTitle,
400402
prContextState: requestedPrContextState,
401403
renderMode: normalizeRenderMode(getRenderModeValue()),
404+
fontCssUrl: normalizePreviewFontCssUrl(getPreviewFontCssUrlValue()),
402405
tabs: buildWorkspaceTabsSnapshot(),
403406
activeTabId: workspaceTabsState.getActiveTabId(),
404407
createdAt: getActiveWorkspaceCreatedAt() ?? Date.now(),

0 commit comments

Comments
 (0)