diff --git a/.github/instructions/pr-review.md b/.github/instructions/pr-review.md new file mode 100644 index 0000000..d8ff7af --- /dev/null +++ b/.github/instructions/pr-review.md @@ -0,0 +1,51 @@ +--- +applyTo: '**' +--- + +# Pull Request Review Guidance + +You are reviewing changes for @knighted/develop. Be concise, technical, and specific. Prioritize actionable feedback tied to concrete files and lines. + +## Browser support policy + +- Target evergreen browsers only (current stable Chrome, Edge, Safari, and Firefox). +- Do not require old-browser compatibility unless the PR explicitly expands support scope. +- Do not request legacy polyfills or fallback-only workarounds by default. + +## Focus areas + +- CDN-first runtime integrity: imports and fallback behavior should remain compatible with src/modules/cdn.js patterns. +- UI state correctness: drawers, toggles, dialogs, and compact/mobile states should remain synchronized and predictable. +- BYOT safety: token handling must stay browser-local, avoid leakage, and preserve clear user-facing privacy/removal cues. +- Accessibility and semantics: form controls, labels, button types, ARIA relationships, and keyboard interactions should be valid. +- Build and workflow stability: scripts and output behavior should remain consistent unless change is explicitly documented. +- Tests and docs alignment: behavior changes should update tests and relevant docs. + +## What to verify + +- No generated artifacts are edited (dist/, coverage/, test-results/). +- CDN import/fallback behavior is not bypassed with ad hoc URLs in feature modules. +- Sensitive values (PAT/token) are not logged or exposed in UI/status output. +- New UI behavior is covered in Playwright where appropriate. +- Lint/build expectations still pass for changed areas. + +## Validation expectations + +- Run npm run lint for code and HTML/a11y checks. +- Run npm run build when touching scripts/, bootstrap/runtime wiring, or import map behavior. +- For interactive UI changes, confirm behavior in compact/mobile layout and at least one non-default mode. + +## Review output format + +- Present findings first, ordered by severity. +- Label each finding as blocking, important, or nit. +- Include file reference and the minimal fix direction. +- Keep summary brief and secondary to findings. + +## Ask for changes when + +- Behavior changes ship without corresponding test updates. +- New dependencies are added without clear approval. +- Build/CI/import-map contracts change without docs updates. +- Accessibility regressions or semantic HTML issues are introduced. +- Feedback requests are based on unsupported legacy-browser constraints. diff --git a/AGENTS.md b/AGENTS.md index be532e7..65ec35d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,13 @@ Repository structure: - Maintain graceful fallback behavior when CDN modules fail to load. - Keep the app usable in local dev without requiring a local bundle step. +## Browser support policy + +- Target evergreen browsers only (current stable Chrome, Edge, Safari, and Firefox). +- Prefer platform features available in evergreen browsers without adding legacy polyfills. +- Do not add legacy-browser workarounds unless a task explicitly requires expanded support. +- In code review, treat requests for old-browser compatibility as out-of-scope unless documented. + ## Testing and validation expectations - Run npm run lint after JavaScript edits. 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 6a3205c..d6a4b58 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -201,6 +201,81 @@ 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.', + ) + const removeButton = dialog.getByRole('button', { name: 'Remove' }) + await expect(removeButton).toBeVisible() + await expect(removeButton).not.toHaveAttribute('aria-label') + + 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 removeButton.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) diff --git a/src/app.js b/src/app.js index e65d091..d1b4d70 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') @@ -74,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 @@ -110,6 +112,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 +142,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 +165,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 +177,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 +187,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 +533,16 @@ 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.', + confirmButtonText: 'Remove', + fallbackConfirmText: + 'Remove saved GitHub token? This action removes the token from browser storage.', + onConfirm, + }) + }, onTokenChange: token => { githubAiContextState.token = token syncAiChatTokenVisibility(token) @@ -943,17 +978,19 @@ const clearStylesSource = () => { maybeRender() } -const confirmClearSource = ({ label, onConfirm }) => { +const confirmAction = ({ + title, + copy, + confirmButtonText = 'Clear', + 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 +1001,31 @@ 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 + } + + if (clearConfirmButton instanceof HTMLButtonElement) { + clearConfirmButton.textContent = confirmButtonText + clearConfirmButton.removeAttribute('aria-label') } 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 +1236,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 +1266,15 @@ document.addEventListener('click', event => { setCompactAiControlsOpen(false) } } + + if (githubTokenInfoOpen) { + if ( + !githubTokenInfo?.contains(clickTarget) && + !githubTokenInfoPanel?.contains(clickTarget) + ) { + setGitHubTokenInfoOpen(false) + } + } }) document.addEventListener('keydown', event => { @@ -1218,6 +1284,7 @@ document.addEventListener('keydown', event => { setStackedRailViewControlsOpen(false) setCompactAiControlsOpen(false) + setGitHubTokenInfoOpen(false) }) for (const button of editorToolsButtons) { @@ -1277,6 +1344,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..9020995 100644 --- a/src/index.html +++ b/src/index.html @@ -42,15 +42,33 @@