Skip to content

Commit 8278579

Browse files
feat: typography workspace selection. (#141)
1 parent 6f83b8d commit 8278579

22 files changed

Lines changed: 709 additions & 23 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ Never:
102102
- Edit generated output folders unless explicitly requested.
103103
- Modify node_modules or lockfiles unless explicitly requested.
104104
- Reintroduce cross-workspace overwrite/delete behavior with any changes.
105+
- Use eslint disable comments.

playwright/github-pr-drawer/active-context-sync.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getWorkspaceTabsRecord,
1111
mockRepositoryBranches,
1212
openMostRecentStoredWorkspaceContext,
13+
openStoredWorkspaceContextByHead,
1314
renameWorkspaceTab,
1415
seedActivePrWorkspaceContext,
1516
seedLocalWorkspaceContexts,
@@ -1304,7 +1305,6 @@ test('Active PR context push commit uses Git Database API atomic path by default
13041305
await connectByotWithSingleRepo(page)
13051306
await openMostRecentStoredWorkspaceContext(page)
13061307
await ensureOpenPrDrawerOpen(page)
1307-
13081308
await setComponentEditorSource(page, 'const commitMarker = 2')
13091309
await setStylesEditorSource(page, '.commit-marker { color: blue; }')
13101310
const pushCommitMessage = 'chore: push active context sync (atomic)'
@@ -1563,8 +1563,11 @@ test('Open PR uses module tab paths when stale target file paths collide', async
15631563
},
15641564
])
15651565

1566-
await connectByotWithSingleRepo(page)
1567-
await openMostRecentStoredWorkspaceContext(page)
1566+
await connectByotWithSingleRepo(page, {
1567+
autoOpenWorkspace: false,
1568+
assertPrRepositorySelected: false,
1569+
})
1570+
await openStoredWorkspaceContextByHead(page, 'develop/open-pr-stale-target-paths')
15681571
await ensureOpenPrDrawerOpen(page)
15691572

15701573
const commitMessage = 'chore: open pr with stale module target path metadata'

playwright/helpers/app-test-helpers.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from '@playwright/test'
22
import type { Page } from '@playwright/test'
33

44
const webServerMode = process.env.PLAYWRIGHT_WEB_SERVER_MODE ?? 'dev'
5+
const pagesWithStubbedExternalFonts = new WeakSet<Page>()
56

67
export const appEntryPath =
78
webServerMode === 'preview' ? '/index.html' : '/src/index.html'
@@ -71,7 +72,34 @@ const navigateToApp = async (page: Page, path: string) => {
7172
}
7273
}
7374

75+
const stubExternalFontRequests = async (page: Page) => {
76+
if (pagesWithStubbedExternalFonts.has(page)) {
77+
return
78+
}
79+
80+
pagesWithStubbedExternalFonts.add(page)
81+
82+
await page.route('https://fonts.googleapis.com/**', async route => {
83+
await route.fulfill({
84+
status: 200,
85+
contentType: 'text/css; charset=utf-8',
86+
body: '',
87+
headers: {
88+
'cache-control': 'public, max-age=31536000, immutable',
89+
},
90+
})
91+
})
92+
93+
await page.route('https://fonts.gstatic.com/**', async route => {
94+
await route.fulfill({
95+
status: 204,
96+
body: '',
97+
})
98+
})
99+
}
100+
74101
export const waitForAppReady = async (page: Page, path = appEntryPath) => {
102+
await stubExternalFontRequests(page)
75103
await navigateToApp(page, path)
76104
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
77105
await expect
@@ -483,8 +511,12 @@ export const connectByotWithSingleRepo = async (
483511
page: Page,
484512
{
485513
branchesByRepo,
514+
autoOpenWorkspace = true,
515+
assertPrRepositorySelected = true,
486516
}: {
487517
branchesByRepo?: BranchesByRepo
518+
autoOpenWorkspace?: boolean
519+
assertPrRepositorySelected?: boolean
488520
} = {},
489521
) => {
490522
await page.route('https://api.github.com/user/repos**', async route => {
@@ -525,33 +557,37 @@ export const connectByotWithSingleRepo = async (
525557
await workspacesRepositoryFilter.selectOption('knightedcodemonkey/develop')
526558
await expect(workspacesRepositoryFilter).toHaveValue('knightedcodemonkey/develop')
527559

528-
const initializeButton = page.getByRole('button', {
529-
name: 'Initialize',
530-
exact: true,
531-
})
560+
if (autoOpenWorkspace) {
561+
const initializeButton = page.getByRole('button', {
562+
name: 'Initialize',
563+
exact: true,
564+
})
532565

533-
if (await initializeButton.isVisible()) {
534-
await initializeButton.click()
535-
} else {
536-
const storedWorkspace = page.getByLabel('Stored workspace')
537-
if (await storedWorkspace.isVisible()) {
538-
const workspaceValue = await storedWorkspace
539-
.locator('option:not([value=""])')
540-
.first()
541-
.getAttribute('value')
542-
543-
if (workspaceValue) {
544-
await storedWorkspace.selectOption(workspaceValue)
545-
await page.getByRole('button', { name: 'Open', exact: true }).click()
566+
if (await initializeButton.isVisible()) {
567+
await initializeButton.click()
568+
} else {
569+
const storedWorkspace = page.getByLabel('Stored workspace')
570+
if (await storedWorkspace.isVisible()) {
571+
const workspaceValue = await storedWorkspace
572+
.locator('option:not([value=""])')
573+
.first()
574+
.getAttribute('value')
575+
576+
if (workspaceValue) {
577+
await storedWorkspace.selectOption(workspaceValue)
578+
await page.getByRole('button', { name: 'Open', exact: true }).click()
579+
}
546580
}
547581
}
548582
}
549583

550584
await ensureWorkspacesDrawerClosed(page)
551585

552-
const repoSelect = page.getByLabel('Pull request repository')
553-
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')
554-
await expect(repoSelect).toBeDisabled()
586+
if (assertPrRepositorySelected) {
587+
const repoSelect = page.getByLabel('Pull request repository')
588+
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')
589+
await expect(repoSelect).toBeDisabled()
590+
}
555591

556592
await expect(
557593
page.getByRole('button', {

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 stylesheet URL',
247+
})
248+
await fontCssUrlInput.fill(
249+
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
250+
)
251+
await page.getByRole('button', { name: 'Load', exact: true }).click()
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: 36 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'
@@ -74,6 +79,8 @@ import { ensureJsxTransformSource } from './modules/preview/jsx-transform-runtim
7479
import { createEditorPoolManager } from './modules/editor/editor-pool-manager.js'
7580
import { createWorkspaceTabsState } from './modules/workspace/workspace-tabs-state.js'
7681
import { createWorkspacesDrawer } from './modules/workspace/workspaces-drawer/drawer.js'
82+
import { createApplyWorkspaceFontCssUrl } from './modules/app-core/workspace-font-css-url-load.js'
83+
import { createPreviewFontSetup } from './modules/app-core/preview-font-setup.js'
7784
import {
7885
createDebouncedWorkspaceSaver,
7986
createWorkspaceStorageAdapter,
@@ -151,6 +158,8 @@ const workspacesInitialize = document.getElementById('workspaces-initialize')
151158
const workspacesShare = document.getElementById('workspaces-share')
152159
const workspacesNew = document.getElementById('workspaces-new')
153160
const workspacesSelect = document.getElementById('workspaces-select')
161+
const workspacesFontCssUrlInput = document.getElementById('workspaces-font-css-url')
162+
const workspacesFontCssUrlLoad = document.getElementById('workspaces-font-css-url-load')
154163
const workspacesOpen = document.getElementById('workspaces-open')
155164
const workspacesRename = document.getElementById('workspaces-rename')
156165
const workspacesRemove = document.getElementById('workspaces-remove')
@@ -349,6 +358,14 @@ const previewBackground = createPreviewBackgroundController({
349358
},
350359
})
351360

361+
const previewFont = createPreviewFontSetup({
362+
createPreviewFontController,
363+
previewFontCssUrlInput: workspacesFontCssUrlInput,
364+
defaultPreviewFontCssUrl,
365+
getRenderRuntime: () => renderRuntime,
366+
queueWorkspaceSave: () => queueWorkspaceSave?.(),
367+
})
368+
352369
const layoutTheme = createLayoutThemeController({
353370
appThemeButtons,
354371
syncPreviewBackgroundPickerFromTheme: () =>
@@ -757,7 +774,9 @@ const workspaceSyncController = createWorkspaceSyncController({
757774
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
758775
getActiveWorkspaceCreatedAt: () => activeWorkspaceCreatedAt,
759776
getRenderModeValue: () => renderMode.value,
777+
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
760778
normalizeRenderMode: mode => normalizeRenderMode(mode),
779+
normalizePreviewFontCssUrl,
761780
})
762781

763782
const getTypecheckSourcePath = () =>
@@ -891,10 +910,15 @@ const {
891910
workspaceTabsState,
892911
resolveWorkspaceActiveTabId,
893912
normalizeRenderMode: mode => normalizeRenderMode(mode),
913+
normalizePreviewFontCssUrl,
894914
getRenderModeValue: () => renderMode.value,
915+
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
895916
setRenderModeValue: value => {
896917
renderMode.value = value
897918
},
919+
setPreviewFontCssUrlValue: (value, options) => {
920+
previewFont.applyPreviewFontCssUrl(value, options)
921+
},
898922
getActiveWorkspaceTab,
899923
onActiveWorkspaceTabChange: (_tab, { changed } = {}) => {
900924
syncDiagnosticsDrawerLayout()
@@ -948,6 +972,13 @@ const {
948972
onWorkspaceRecordApplied: onWorkspaceRecordAppliedWithStatusMetadata,
949973
})
950974

975+
const applyWorkspaceFontCssUrl = createApplyWorkspaceFontCssUrl({
976+
previewFont,
977+
flushWorkspaceSave,
978+
normalizePreviewFontCssUrl,
979+
defaultPreviewFontCssUrl,
980+
})
981+
951982
const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
952983
createWorkspaceScopeForkActions({
953984
toNonEmptyWorkspaceText,
@@ -1201,6 +1232,8 @@ const githubWorkflows = createGitHubWorkflowsSetup({
12011232
workspacesShare,
12021233
workspacesNew,
12031234
workspacesSelect,
1235+
workspacesFontCssUrlInput,
1236+
workspacesFontCssUrlLoad,
12041237
workspacesOpen,
12051238
workspacesRename,
12061239
workspacesRemove,
@@ -1220,6 +1253,7 @@ const githubWorkflows = createGitHubWorkflowsSetup({
12201253
listLocalContextRecords,
12211254
refreshLocalContextOptions,
12221255
applyWorkspaceRecord,
1256+
applyWorkspaceFontCssUrl,
12231257
syncActiveWorkspaceRepositoryScope,
12241258
forkWorkspaceFromCurrentState,
12251259
flushWorkspaceSave,
@@ -1424,6 +1458,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
14241458
getRenderRuntime: () => renderRuntime,
14251459
getPreviewHost: () => previewHost,
14261460
previewBackground,
1461+
previewFont,
14271462
clearDiagnosticsScope,
14281463
clearConfirmDialog,
14291464
clearConfirmTitle,
@@ -1602,6 +1637,7 @@ bindAppEventsAndStart({
16021637
typeDiagnostics,
16031638
clipboardSupported,
16041639
previewBackground,
1640+
previewFont,
16051641
initializeCodeEditors,
16061642
},
16071643
})

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 Stylesheet URL</span>
815+
<input
816+
id="workspaces-font-css-url"
817+
type="text"
818+
autocomplete="off"
819+
spellcheck="false"
820+
aria-label="Workspace font stylesheet URL"
821+
placeholder="https://cdn.example.com/fonts.css"
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 {

0 commit comments

Comments
 (0)