From 032cff0f546242aa3daf09a3f64b90250cdae840 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 22 Mar 2026 13:54:59 -0500 Subject: [PATCH 1/4] feat: enhanced byot ux. --- src/app.js | 73 ++++++++++++++++--- src/index.html | 26 +++++-- src/modules/github-byot-controls.js | 28 +++++++- src/styles/ai-controls.css | 104 +++++++++++++++++++++++++--- 4 files changed, 206 insertions(+), 25 deletions(-) diff --git a/src/app.js b/src/app.js index e65d091..8dd27a4 100644 --- a/src/app.js +++ b/src/app.js @@ -21,6 +21,7 @@ const appGrid = document.querySelector('.app-grid') const githubAiControls = document.getElementById('github-ai-controls') const githubTokenInput = document.getElementById('github-token-input') const githubTokenInfo = document.getElementById('github-token-info') +const githubTokenInfoPanel = document.getElementById('github-token-info-panel') const githubTokenAdd = document.getElementById('github-token-add') const githubTokenDelete = document.getElementById('github-token-delete') const githubRepoWrap = document.getElementById('github-repo-wrap') @@ -110,6 +111,7 @@ const compactViewportMediaQuery = window.matchMedia('(max-width: 900px)') const stackedRailMediaQuery = window.matchMedia('(max-width: 1090px)') let stackedRailViewControlsOpen = false let compactAiControlsOpen = false +let githubTokenInfoOpen = false const isStackedRailViewport = () => stackedRailMediaQuery.matches @@ -139,6 +141,22 @@ const setStackedRailViewControlsOpen = isOpen => { viewControlsDrawer.setAttribute('hidden', '') } +const setGitHubTokenInfoOpen = isOpen => { + if (!(githubTokenInfo instanceof HTMLButtonElement) || !githubTokenInfoPanel) { + return + } + + githubTokenInfoOpen = Boolean(isOpen) + githubTokenInfo.setAttribute('aria-expanded', githubTokenInfoOpen ? 'true' : 'false') + + if (githubTokenInfoOpen) { + githubTokenInfoPanel.removeAttribute('hidden') + return + } + + githubTokenInfoPanel.setAttribute('hidden', '') +} + const setCompactAiControlsOpen = isOpen => { if (!(aiControlsToggle instanceof HTMLButtonElement) || !githubAiControls) { return @@ -146,6 +164,7 @@ const setCompactAiControlsOpen = isOpen => { if (!aiAssistantFeatureEnabled) { compactAiControlsOpen = false + setGitHubTokenInfoOpen(false) aiControlsToggle.setAttribute('hidden', '') aiControlsToggle.setAttribute('aria-expanded', 'false') githubAiControls.removeAttribute('data-compact-open') @@ -157,6 +176,7 @@ const setCompactAiControlsOpen = isOpen => { if (!isCompactViewport()) { compactAiControlsOpen = false + setGitHubTokenInfoOpen(false) aiControlsToggle.setAttribute('aria-expanded', 'false') githubAiControls.removeAttribute('data-compact-open') githubAiControls.removeAttribute('hidden') @@ -166,6 +186,10 @@ const setCompactAiControlsOpen = isOpen => { compactAiControlsOpen = Boolean(isOpen) aiControlsToggle.setAttribute('aria-expanded', compactAiControlsOpen ? 'true' : 'false') githubAiControls.dataset.compactOpen = compactAiControlsOpen ? 'true' : 'false' + + if (!compactAiControlsOpen) { + setGitHubTokenInfoOpen(false) + } } const getCurrentLayout = () => { @@ -508,6 +532,15 @@ const byotControls = createGitHubByotControls({ githubAiContextState.selectedRepository = repository chatDrawerController.setSelectedRepository(repository) }, + onTokenDeleteRequest: onConfirm => { + confirmAction({ + title: 'Remove saved GitHub token?', + copy: 'This action removes the token from browser storage. You can add another token at any time.', + fallbackConfirmText: + 'Remove saved GitHub token? This action removes the token from browser storage.', + onConfirm, + }) + }, onTokenChange: token => { githubAiContextState.token = token syncAiChatTokenVisibility(token) @@ -943,17 +976,13 @@ const clearStylesSource = () => { maybeRender() } -const confirmClearSource = ({ label, onConfirm }) => { +const confirmAction = ({ title, copy, fallbackConfirmText, onConfirm }) => { const supportsModalDialog = clearConfirmDialog instanceof HTMLDialogElement && typeof clearConfirmDialog.showModal === 'function' if (!supportsModalDialog) { - if ( - window.confirm( - `Clear ${label.toLowerCase()} source? This action will remove all text from the editor.`, - ) - ) { + if (window.confirm(fallbackConfirmText)) { onConfirm() } return @@ -964,18 +993,26 @@ const confirmClearSource = ({ label, onConfirm }) => { } if (clearConfirmTitle) { - clearConfirmTitle.textContent = `Clear ${label} source?` + clearConfirmTitle.textContent = title } if (clearConfirmCopy) { - clearConfirmCopy.textContent = - 'This action will remove all text from the editor. This cannot be undone.' + clearConfirmCopy.textContent = copy } pendingClearAction = onConfirm clearConfirmDialog.showModal() } +const confirmClearSource = ({ label, onConfirm }) => { + confirmAction({ + title: `Clear ${label} source?`, + copy: 'This action will remove all text from the editor. This cannot be undone.', + fallbackConfirmText: `Clear ${label.toLowerCase()} source? This action will remove all text from the editor.`, + onConfirm, + }) +} + const copyTextToClipboard = async text => { if (!clipboardSupported) { throw new Error('Clipboard API is not available in this browser context.') @@ -1186,6 +1223,13 @@ if (aiControlsToggle instanceof HTMLButtonElement) { }) } +if (githubTokenInfo instanceof HTMLButtonElement && githubTokenInfoPanel) { + githubTokenInfo.addEventListener('click', event => { + event.preventDefault() + setGitHubTokenInfoOpen(!githubTokenInfoOpen) + }) +} + document.addEventListener('click', event => { const clickTarget = event.target if (!(clickTarget instanceof Node)) { @@ -1209,6 +1253,15 @@ document.addEventListener('click', event => { setCompactAiControlsOpen(false) } } + + if (githubTokenInfoOpen) { + if ( + !githubTokenInfo?.contains(clickTarget) && + !githubTokenInfoPanel?.contains(clickTarget) + ) { + setGitHubTokenInfoOpen(false) + } + } }) document.addEventListener('keydown', event => { @@ -1218,6 +1271,7 @@ document.addEventListener('keydown', event => { setStackedRailViewControlsOpen(false) setCompactAiControlsOpen(false) + setGitHubTokenInfoOpen(false) }) for (const button of editorToolsButtons) { @@ -1277,6 +1331,7 @@ applyEditorToolsVisibility() applyPanelCollapseState() setStackedRailViewControlsOpen(false) setCompactAiControlsOpen(false) +setGitHubTokenInfoOpen(false) syncAiChatTokenVisibility(githubAiContextState.token) updateRenderButtonVisibility() diff --git a/src/index.html b/src/index.html index bd828ea..17ad38e 100644 --- a/src/index.html +++ b/src/index.html @@ -42,15 +42,33 @@

>
+ { @@ -114,6 +115,22 @@ export const createGitHubByotControls = ({ const hasProvidedToken = typeof savedToken === 'string' && savedToken.trim().length > 0 + if (tokenInfoButton instanceof HTMLButtonElement) { + tokenInfoButton.dataset.tokenState = hasProvidedToken ? 'present' : 'missing' + tokenInfoButton.textContent = hasProvidedToken ? 'i' : '?' + tokenInfoButton.setAttribute( + 'aria-label', + hasProvidedToken + ? 'About GitHub token privacy' + : 'About GitHub token features and privacy', + ) + + const tokenControlWrap = tokenInfoButton.closest('.github-token-control-wrap') + if (tokenControlWrap instanceof HTMLElement) { + tokenControlWrap.dataset.tokenState = hasProvidedToken ? 'present' : 'missing' + } + } + if (tokenAddButton instanceof HTMLButtonElement) { tokenAddButton.hidden = hasProvidedToken } @@ -413,7 +430,7 @@ export const createGitHubByotControls = ({ void persistAndLoadToken(tokenInput.value) }) - tokenDeleteButton?.addEventListener('click', () => { + const removeSavedToken = () => { abortInFlightRepoRequest() clearAddButtonResetTimer() setTokenAddButtonState('idle') @@ -426,6 +443,15 @@ export const createGitHubByotControls = ({ onRepositoryChange?.(null) syncSavedTokenUi() setStatus('GitHub token removed', 'neutral') + } + + tokenDeleteButton?.addEventListener('click', () => { + if (typeof onTokenDeleteRequest === 'function') { + onTokenDeleteRequest(removeSavedToken) + return + } + + removeSavedToken() }) tokenInfoButton?.setAttribute('aria-expanded', 'false') diff --git a/src/styles/ai-controls.css b/src/styles/ai-controls.css index b746fbe..aba1fd0 100644 --- a/src/styles/ai-controls.css +++ b/src/styles/ai-controls.css @@ -28,6 +28,16 @@ gap: 8px; } +.github-token-control-wrap[data-token-state='missing'] + .github-token-info-message--has-token { + display: none; +} + +.github-token-control-wrap[data-token-state='present'] + .github-token-info-message--missing-token { + display: none; +} + .github-token-input { width: 170px; min-width: 0; @@ -79,7 +89,85 @@ } .github-token-info { - align-self: flex-start; + display: inline-grid; + place-content: center; + cursor: pointer; + border-radius: 999px; + border: 1px solid var(--border-strong); + color: var(--hint-icon); + font-weight: 700; + line-height: 1; + opacity: 0.9; + background: transparent; + padding: 0; + width: 24px; + height: 24px; + font-size: 0.74rem; + transition: + border-color 140ms ease, + color 140ms ease, + background 140ms ease, + box-shadow 140ms ease; +} + +.github-token-info[data-token-state='missing'] { + border-color: color-mix(in srgb, #f59e0b 70%, var(--border-strong)); + color: color-mix(in srgb, #fbbf24 86%, var(--panel-text)); + background: color-mix(in srgb, #f59e0b 14%, transparent); +} + +.github-token-info[data-token-state='present'] { + border-color: color-mix(in srgb, #22c55e 56%, var(--border-strong)); + color: color-mix(in srgb, #4ade80 82%, var(--panel-text)); + background: color-mix(in srgb, #22c55e 12%, transparent); +} + +.github-token-info[aria-expanded='true'] { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-ring) 70%, transparent); +} + +.github-token-info:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; +} + +.github-token-info-panel { + position: absolute; + top: calc(100% + 10px); + left: 0; + width: min(360px, calc(100vw - 36px)); + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--border-tooltip); + background: var(--surface-tooltip); + color: var(--tooltip-text); + font-size: 0.78rem; + line-height: 1.35; + text-align: left; + box-shadow: 0 12px 24px var(--shadow-elev-1); + z-index: 80; +} + +.github-token-info-panel[hidden] { + display: none !important; +} + +.github-token-info-message { + margin: 0; +} + +.github-token-info-message a { + color: color-mix(in srgb, var(--focus-ring) 82%, white 18%); + text-underline-offset: 2px; +} + +.github-token-info-message a:hover { + color: color-mix(in srgb, var(--focus-ring) 92%, white 8%); +} + +.github-token-info-message a:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 1px; } .github-token-delete svg { @@ -92,12 +180,6 @@ height: 14px; } -.github-token-info.shadow-hint::after { - right: auto; - left: 0; - z-index: 80; -} - .github-token-add[data-state='loading'] svg { opacity: 0.75; } @@ -443,19 +525,19 @@ width: auto; max-width: none; justify-content: flex-start; - flex-wrap: nowrap; + flex-wrap: wrap; + row-gap: 10px; border: 1px solid var(--border-subtle); border-radius: 12px; padding: 10px 12px; background: color-mix(in srgb, var(--surface-panel-header) 88%, transparent); box-shadow: 0 14px 30px var(--shadow-elev-1); backdrop-filter: blur(6px); - overflow-x: auto; - overflow-y: hidden; + overflow: visible; z-index: 60; } - .github-token-info.shadow-hint::after { + .github-token-info-panel { position: fixed; top: auto; right: 12px; From fd194fed8482440ff844b493dd5be8a524f0a70a Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 22 Mar 2026 13:59:27 -0500 Subject: [PATCH 2/4] test: add specs. --- playwright/app.spec.ts | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index 6a3205c..9b7e02c 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -201,6 +201,78 @@ test('BYOT controls render when feature flag is enabled by query param', async ( await expect(page.locator('#github-ai-controls #ai-chat-toggle')).toBeHidden() }) +test('GitHub token info panel reflects missing and present token states', async ({ + page, +}) => { + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + + const infoButton = page.locator('#github-token-info') + const infoPanel = page.locator('#github-token-info-panel') + const missingMessage = page.locator('.github-token-info-message--missing-token') + const presentMessage = page.locator('.github-token-info-message--has-token') + + await expect(infoButton).toHaveText('?') + await expect(infoButton).toHaveAttribute('data-token-state', 'missing') + + await infoButton.click() + await expect(infoPanel).toBeVisible() + await expect(missingMessage).toBeVisible() + await expect(missingMessage).toContainText('Provide a GitHub PAT') + await expect(missingMessage.getByRole('link', { name: 'docs' })).toHaveAttribute( + 'href', + 'https://github.com/knightedcodemonkey/develop/blob/main/docs/byot.md', + ) + await expect(presentMessage).toBeHidden() + + await connectByotWithSingleRepo(page) + await expect(infoButton).toHaveText('i') + await expect(infoButton).toHaveAttribute('data-token-state', 'present') + + await infoButton.click() + await expect(infoPanel).toBeVisible() + await expect(presentMessage).toBeVisible() + await expect(presentMessage).toContainText( + 'Use the trash icon to remove it from storage.', + ) + await expect(missingMessage).toBeHidden() +}) + +test('deleting saved GitHub token requires confirmation modal', async ({ page }) => { + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await connectByotWithSingleRepo(page) + + const dialog = page.locator('#clear-confirm-dialog') + const tokenDelete = page.locator('#github-token-delete') + const tokenAdd = page.locator('#github-token-add') + const tokenInput = page.locator('#github-token-input') + + await expect(tokenDelete).toBeVisible() + + await tokenDelete.click() + await expect(dialog).toHaveAttribute('open', '') + await expect(page.locator('#clear-confirm-title')).toHaveText( + 'Remove saved GitHub token?', + ) + await expect(page.locator('#clear-confirm-copy')).toHaveText( + 'This action removes the token from browser storage. You can add another token at any time.', + ) + + await dialog.getByRole('button', { name: 'Cancel' }).click() + await expect(dialog).not.toHaveAttribute('open', '') + await expect(tokenDelete).toBeVisible() + await expect(tokenAdd).toBeHidden() + + await tokenDelete.click() + await expect(dialog).toHaveAttribute('open', '') + await dialog.getByRole('button', { name: 'Clear' }).click() + await expect(dialog).not.toHaveAttribute('open', '') + + await expect(page.locator('#status')).toHaveText('GitHub token removed') + await expect(tokenAdd).toBeVisible() + await expect(tokenDelete).toBeHidden() + await expect(tokenInput).toHaveValue('') +}) + test('AI chat drawer opens and closes when feature flag is enabled', async ({ page }) => { await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) await connectByotWithSingleRepo(page) From d953982a442141c09b78cfab2dc5004e9e9adbda Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 22 Mar 2026 14:15:57 -0500 Subject: [PATCH 3/4] chore: lint html. --- eslint.config.js | 30 ++++++ package-lock.json | 216 +++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- playwright/app.spec.ts | 3 +- src/app.js | 21 +++- src/index.html | 2 + 6 files changed, 274 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b368ba7..dc28846 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,9 @@ import playwright from 'eslint-plugin-playwright' import tsParser from '@typescript-eslint/parser' +import htmlPlugin from '@html-eslint/eslint-plugin' const playwrightConfig = playwright.configs['flat/recommended'] +const htmlRecommendedConfig = htmlPlugin.configs['flat/recommended'] export default [ { @@ -28,4 +30,32 @@ export default [ ...playwrightConfig, files: ['playwright/**/*.{ts,tsx,js,jsx}'], }, + { + ...htmlRecommendedConfig, + files: ['src/**/*.html'], + }, + { + files: ['src/**/*.html'], + plugins: { + '@html-eslint': htmlPlugin, + }, + rules: { + // Formatting is delegated to Prettier; keep html-eslint focused on semantics and a11y. + '@html-eslint/indent': 'off', + '@html-eslint/attrs-newline': 'off', + '@html-eslint/element-newline': 'off', + '@html-eslint/no-extra-spacing-attrs': 'off', + '@html-eslint/require-closing-tags': 'off', + '@html-eslint/use-baseline': 'off', + '@html-eslint/require-input-label': 'error', + '@html-eslint/require-button-type': 'error', + '@html-eslint/no-accesskey-attrs': 'error', + '@html-eslint/no-positive-tabindex': 'error', + '@html-eslint/no-invalid-role': 'error', + '@html-eslint/no-redundant-role': 'error', + '@html-eslint/no-abstract-roles': 'error', + '@html-eslint/no-aria-hidden-body': 'error', + '@html-eslint/no-aria-hidden-on-focusable': 'error', + }, + }, ] diff --git a/package-lock.json b/package-lock.json index 1552293..7299d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.2.0", "license": "MIT", "devDependencies": { + "@html-eslint/eslint-plugin": "^0.58.1", + "@html-eslint/parser": "^0.58.1", "@playwright/test": "^1.58.2", "@types/node": "^25.5.0", "@typescript-eslint/parser": "^8.57.1", @@ -1027,6 +1029,116 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@html-eslint/core": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/core/-/core-0.58.1.tgz", + "integrity": "sha512-GHYDt2Q3ws9aa0/bmMhkv21ExQJnrjKY/iByjdBVp3lBq49wlzIzvAfcx4Bsp+RMV3oPZhzlnLhPpXLuVYt2mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@html-eslint/types": "^0.58.1", + "html-standard": "^0.0.13" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@html-eslint/eslint-plugin": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.58.1.tgz", + "integrity": "sha512-aizTTKbNF2sW+lXWP+uWBoo5Ud9xtUkr70+0pYhItwJF0yhRqLQ91PhW+9afC0daymQjn13MunzDPwGPG0seDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/plugin-kit": "^0.4.1", + "@html-eslint/core": "^0.58.1", + "@html-eslint/parser": "^0.58.1", + "@html-eslint/template-parser": "^0.58.1", + "@html-eslint/template-syntax-parser": "^0.58.1", + "@html-eslint/types": "^0.58.1", + "@rviscomi/capo.js": "^2.1.0", + "html-standard": "^0.0.13" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.0.0 || ^10.0.0-0" + } + }, + "node_modules/@html-eslint/eslint-plugin/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@html-eslint/eslint-plugin/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@html-eslint/parser": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.58.1.tgz", + "integrity": "sha512-a87peH9HcVDrKZZIYdfMlPZ+72nIktAitKcdoHQevuaXWsgvDtClKihJyy5dZS9md6hIbCh62Og5gQRhl85ZMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@html-eslint/template-syntax-parser": "^0.58.1", + "@html-eslint/types": "^0.58.1", + "css-tree": "^3.1.0", + "es-html-parser": "0.3.1" + } + }, + "node_modules/@html-eslint/template-parser": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/template-parser/-/template-parser-0.58.1.tgz", + "integrity": "sha512-qo6jTc4Y6vVgwPc2w+EQigH7uCAn+LExxE5oG1URRT98UiJ7dItX0Qk44r/+5XQwSS1TsdvBNLxM2NAktETSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@html-eslint/types": "^0.58.1", + "es-html-parser": "0.3.1" + } + }, + "node_modules/@html-eslint/template-syntax-parser": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/template-syntax-parser/-/template-syntax-parser-0.58.1.tgz", + "integrity": "sha512-P1ZhxIPm9qFWSees2/EZ7Etg1OXziqzRZEuI9goO91fJS6dmdT4JnHLugN06FLL706RwpvenBUlE0iZA9/MXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@html-eslint/types": "^0.58.1" + } + }, + "node_modules/@html-eslint/types": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@html-eslint/types/-/types-0.58.1.tgz", + "integrity": "sha512-1F2A5XXpgfHQ8dm14E/EztyERoVldT91VGMZCJECZpidf5Cbc21vxeHLT6/POTJm0ICJOmyBlocF62i/rkoVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/css-tree": "^2.3.11", + "@types/estree": "^1.0.6", + "es-html-parser": "0.3.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1562,6 +1674,20 @@ "node": ">=18" } }, + "node_modules/@rviscomi/capo.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@rviscomi/capo.js/-/capo.js-2.1.0.tgz", + "integrity": "sha512-y6J+KJqsrY8AcDswLKkvd8KdpFindjS4Q9rSuK8CIpsQOepEjgRaMR4S8OtuLOQoVYLCROT3ffMQqRWrUMQdQA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@types/css-tree": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.11.tgz", + "integrity": "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1748,6 +1874,13 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2300,6 +2433,20 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2431,6 +2578,13 @@ "node": ">= 0.4" } }, + "node_modules/es-html-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.3.1.tgz", + "integrity": "sha512-YTEasG4xt7FEN4b6qJIPbFo/fzQ5kjRMEQ33QMqSXTvfXqAbC2rHxo32x2/1Rhq7Mlu6wI3MIpM5Kf2VHPXrUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3107,6 +3261,17 @@ "node": ">=14" } }, + "node_modules/html-standard": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/html-standard/-/html-standard-0.0.13.tgz", + "integrity": "sha512-6oNfW3c1t44O7jVXu0tp4E5MbHifWlXrHlZBPt6y7vFdgLOUUh8hyzoRhfUgozlBUK6oLLYhqP1uIqbZ8ggcBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.9", + "vscode-languageserver-textdocument": "^1.0.12" + } + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -3839,6 +4004,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4670,6 +4842,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -5002,6 +5184,40 @@ "dev": true, "license": "MIT" }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index 243e1f0..6e7fd23 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "realtime", "browser", "development", + "ide", "jsx", "css", "importmap", @@ -34,7 +35,7 @@ "build:importmap-mode": "KNIGHTED_PRIMARY_CDN=importMap npm run build", "preview": "npm run build && http-server dist -a localhost -p 8081 -c-1 -o index.html", "check-types": "tsc -p tsconfig.json", - "lint:playwright": "eslint playwright playwright.config.ts", + "lint:playwright": "eslint playwright playwright.config.ts src/index.html", "test:e2e": "npm run build && PLAYWRIGHT_WEB_SERVER_MODE=preview PLAYWRIGHT_PORT=8081 playwright test", "test:e2e:dev": "playwright test", "test:e2e:headed": "npm run build && PLAYWRIGHT_WEB_SERVER_MODE=preview PLAYWRIGHT_PORT=8081 playwright test --headed", @@ -43,6 +44,8 @@ "lint": "oxlint src scripts && npm run lint:playwright" }, "devDependencies": { + "@html-eslint/eslint-plugin": "^0.58.1", + "@html-eslint/parser": "^0.58.1", "@playwright/test": "^1.58.2", "@types/node": "^25.5.0", "@typescript-eslint/parser": "^8.57.1", diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index 9b7e02c..bd07b85 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -256,6 +256,7 @@ test('deleting saved GitHub token requires confirmation modal', async ({ page }) await expect(page.locator('#clear-confirm-copy')).toHaveText( 'This action removes the token from browser storage. You can add another token at any time.', ) + await expect(dialog.getByRole('button', { name: 'Remove' })).toBeVisible() await dialog.getByRole('button', { name: 'Cancel' }).click() await expect(dialog).not.toHaveAttribute('open', '') @@ -264,7 +265,7 @@ test('deleting saved GitHub token requires confirmation modal', async ({ page }) await tokenDelete.click() await expect(dialog).toHaveAttribute('open', '') - await dialog.getByRole('button', { name: 'Clear' }).click() + await dialog.getByRole('button', { name: 'Remove' }).click() await expect(dialog).not.toHaveAttribute('open', '') await expect(page.locator('#status')).toHaveText('GitHub token removed') diff --git a/src/app.js b/src/app.js index 8dd27a4..32c2d02 100644 --- a/src/app.js +++ b/src/app.js @@ -75,6 +75,7 @@ const previewBgColorInput = document.getElementById('preview-bg-color') const clearConfirmDialog = document.getElementById('clear-confirm-dialog') const clearConfirmTitle = document.getElementById('clear-confirm-title') const clearConfirmCopy = document.getElementById('clear-confirm-copy') +const clearConfirmButton = clearConfirmDialog?.querySelector('button[value="confirm"]') jsxEditor.value = defaultJsx cssEditor.value = defaultCss @@ -536,6 +537,8 @@ const byotControls = createGitHubByotControls({ confirmAction({ title: 'Remove saved GitHub token?', copy: 'This action removes the token from browser storage. You can add another token at any time.', + confirmButtonText: 'Remove', + confirmButtonAriaLabel: 'Confirm remove saved GitHub token', fallbackConfirmText: 'Remove saved GitHub token? This action removes the token from browser storage.', onConfirm, @@ -976,7 +979,14 @@ const clearStylesSource = () => { maybeRender() } -const confirmAction = ({ title, copy, fallbackConfirmText, onConfirm }) => { +const confirmAction = ({ + title, + copy, + confirmButtonText = 'Clear', + confirmButtonAriaLabel, + fallbackConfirmText, + onConfirm, +}) => { const supportsModalDialog = clearConfirmDialog instanceof HTMLDialogElement && typeof clearConfirmDialog.showModal === 'function' @@ -1000,6 +1010,15 @@ const confirmAction = ({ title, copy, fallbackConfirmText, onConfirm }) => { clearConfirmCopy.textContent = copy } + if (clearConfirmButton instanceof HTMLButtonElement) { + clearConfirmButton.textContent = confirmButtonText + if (typeof confirmButtonAriaLabel === 'string' && confirmButtonAriaLabel) { + clearConfirmButton.setAttribute('aria-label', confirmButtonAriaLabel) + } else { + clearConfirmButton.removeAttribute('aria-label') + } + } + pendingClearAction = onConfirm clearConfirmDialog.showModal() } diff --git a/src/index.html b/src/index.html index 17ad38e..9020995 100644 --- a/src/index.html +++ b/src/index.html @@ -640,6 +640,7 @@

Clear source?