From c9c9206a4f936bc47c63db28540a7a8e2cc4f051 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:09:41 -0400 Subject: [PATCH 1/6] feat(site): make the homepage demo-first and mobile accessible --- .github/workflows/test_website.yml | 49 ++ site/tests/playwright.config.ts | 32 + site/tests/server.mjs | 23 + site/tests/website.spec.ts | 123 ++++ site/www/index.html | 995 +++++++---------------------- 5 files changed, 470 insertions(+), 752 deletions(-) create mode 100644 .github/workflows/test_website.yml create mode 100644 site/tests/playwright.config.ts create mode 100644 site/tests/server.mjs create mode 100644 site/tests/website.spec.ts diff --git a/.github/workflows/test_website.yml b/.github/workflows/test_website.yml new file mode 100644 index 00000000..956c9b24 --- /dev/null +++ b/.github/workflows/test_website.yml @@ -0,0 +1,49 @@ +name: test / website + +on: + push: + branches: [main] + paths: + - 'site/www/**' + - 'site/tests/**' + - '.github/workflows/test_website.yml' + - 'package.json' + - 'package-lock.json' + pull_request: + branches: [main] + paths: + - 'site/www/**' + - 'site/tests/**' + - '.github/workflows/test_website.yml' + - 'package.json' + - 'package-lock.json' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + website: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium firefox webkit + - run: npx playwright test --config site/tests/playwright.config.ts + - uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: website-browser-report + path: | + playwright-report/website + test-results + retention-days: 14 diff --git a/site/tests/playwright.config.ts b/site/tests/playwright.config.ts new file mode 100644 index 00000000..98308dd3 --- /dev/null +++ b/site/tests/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; +import { resolve } from 'node:path'; + +export default defineConfig({ + testDir: '.', + testMatch: 'website.spec.ts', + fullyParallel: true, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 2 : undefined, + outputDir: resolve(__dirname, '../../test-results/website'), + reporter: [['list'], ['html', { + outputFolder: resolve(__dirname, '../../playwright-report/website'), + open: 'never', + }]], + use: { + baseURL: process.env.WEBSITE_URL || 'http://127.0.0.1:4186', + reducedMotion: 'reduce', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: process.env.WEBSITE_URL ? undefined : { + command: 'node site/tests/server.mjs', + cwd: resolve(__dirname, '../..'), + url: 'http://127.0.0.1:4186', + reuseExistingServer: false, + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + ], +}); diff --git a/site/tests/server.mjs b/site/tests/server.mjs new file mode 100644 index 00000000..a6cdf50d --- /dev/null +++ b/site/tests/server.mjs @@ -0,0 +1,23 @@ +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; + +// Serve only the two assets used by the website tests, never the checkout. +const assets = new Map([ + ['/', ['index.html', 'text/html; charset=utf-8']], + ['/askable-ui-code.mp4', ['askable-ui-code.mp4', 'video/mp4']], +]); + +createServer(async (request, response) => { + const asset = assets.get(new URL(request.url, 'http://localhost').pathname); + if (!asset || !['GET', 'HEAD'].includes(request.method)) { + response.writeHead(404).end(); + return; + } + try { + const body = await readFile(new URL(`../www/${asset[0]}`, import.meta.url)); + response.writeHead(200, { 'content-type': asset[1], 'content-length': body.length }); + response.end(request.method === 'HEAD' ? undefined : body); + } catch { + response.writeHead(500).end(); + } +}).listen(4186, '127.0.0.1'); diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts new file mode 100644 index 00000000..43f9ab1f --- /dev/null +++ b/site/tests/website.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.route('https://fonts.googleapis.com/**', route => route.abort()); + await page.goto('/'); +}); + +for (const width of [320, 390, 768, 1280, 1440]) { + test(`layout fits ${width}px and keeps the first action visible`, async ({ page }) => { + await page.setViewportSize({ width, height: width < 768 ? 844 : 720 }); + await expect(page.locator('#kpi-grid .kpi-card')).toHaveCount(4); + await expect(page.locator('#install-copy')).toBeInViewport(); + await expect(page.getByRole('heading', { name: 'Analytics workspace' })).toBeInViewport(); + await expect(page.locator('.developer-panel')).not.toHaveAttribute('open'); + await expect(page.locator('video')).not.toHaveAttribute('autoplay'); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); + await page.getByText('Developer view', { exact: true }).click(); + await expect(page.getByRole('button', { name: 'JSON', exact: true })).toBeVisible(); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); + await page.screenshot({ path: test.info().outputPath(`website-${width}.png`), fullPage: true }); + }); +} + +test('mobile menu supports keyboard, Escape, anchor navigation and outside clicks', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + const menu = page.locator('#nav-menu'); + const summary = menu.locator('summary'); + await summary.focus(); + await page.keyboard.press('Enter'); + await expect(menu).toHaveAttribute('open'); + await expect(menu.getByRole('link', { name: 'Docs', exact: true })).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(menu).not.toHaveAttribute('open'); + await expect(summary).toBeFocused(); + await summary.click(); + await menu.getByRole('link', { name: 'MCP', exact: true }).click(); + await expect(page).toHaveURL(/#mcp$/); + await expect(menu).not.toHaveAttribute('open'); + await summary.click(); + await page.getByRole('heading', { name: 'Connect the assistant you already use.' }).click(); + await expect(menu).not.toHaveAttribute('open'); + await summary.click(); + await page.setViewportSize({ width: 1280, height: 720 }); + await expect(menu).not.toHaveAttribute('open'); +}); + +test('selected context can be reviewed, formatted, sent and removed', async ({ page }) => { + await page.locator('.kpi-card').first().click(); + await page.getByText('Review context', { exact: true }).click(); + await expect(page.locator('#context-chip')).toContainText('$128,400'); + await expect(page.locator('#context-chip')).not.toContainText('Ask AI'); + await page.getByText('Developer view', { exact: true }).click(); + await page.getByRole('button', { name: 'JSON', exact: true }).click(); + await expect(page.getByRole('button', { name: 'JSON', exact: true })).toHaveAttribute('aria-pressed', 'true'); + await expect(page.getByRole('button', { name: 'Natural', exact: true })).toHaveAttribute('aria-pressed', 'false'); + await expect(page.locator('#code-ctx')).toContainText('"widget": "revenue"'); + await page.getByRole('button', { name: 'Meta only', exact: true }).click(); + await expect(page.getByRole('button', { name: 'Meta only', exact: true })).toHaveAttribute('aria-pressed', 'true'); + await expect(page.getByRole('button', { name: 'Text + meta', exact: true })).toHaveAttribute('aria-pressed', 'false'); + expect(JSON.parse(await page.locator('#code-ctx').innerText())).not.toHaveProperty('text'); + await page.getByLabel('Question about selected context').fill('Why is revenue growing?'); + await page.getByRole('button', { name: 'Send question', exact: true }).click(); + await expect(page.getByRole('log', { name: 'Demo conversation' })).toContainText('MRR grew 12.4%'); + await page.getByRole('button', { name: 'Remove selected context', exact: true }).click(); + await expect(page.locator('#chat-context-bar')).toBeHidden(); + await expect(page.locator('#chat-input')).toBeEmpty(); +}); + +for (const shape of ['region', 'square', 'circle', 'lasso']) { + test(`${shape} captures content and keeps the mode selected`, async ({ page }) => { + const tool = page.locator(`[data-tool="${shape}"]`); + await tool.click(); + await expect(tool).toHaveAttribute('aria-pressed', 'true'); + await page.locator('.kpi-card').first().scrollIntoViewIfNeeded(); + const box = await page.locator('.kpi-card').first().boundingBox(); + if (!box) throw new Error('Missing KPI bounds'); + const x = box.x + 8, y = box.y + 8; + await page.mouse.move(x, y); + await page.mouse.down(); + if (shape === 'lasso') { + await page.mouse.move(x + box.width - 16, y, { steps: 6 }); + await page.mouse.move(x + box.width - 16, y + box.height - 16, { steps: 6 }); + await page.mouse.move(x, y + box.height - 16, { steps: 6 }); + await page.mouse.move(x, y, { steps: 6 }); + } else { + await page.mouse.move(x + box.width - 16, y + box.height - 16, { steps: 10 }); + } + await page.mouse.up(); + await expect(page.locator('#selected-region-preview')).toContainText('Monthly Revenue'); + await expect(page.locator('#context-chip')).not.toContainText('Ask AI'); + await expect(tool).toHaveAttribute('aria-pressed', 'true'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + await expect(page.locator('[data-mode="click"]')).toHaveAttribute('aria-pressed', 'true'); + }); +} + +test('text mode ignores plain clicks and retains captured text', async ({ page }) => { + await page.locator('#send-selection').click(); + await page.locator('.kpi-card').first().click(); + await expect(page.locator('#chat-context-bar')).toBeHidden(); + await page.locator('.kpi-label').first().evaluate(element => { + const range = document.createRange(); + range.selectNodeContents(element); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + }); + await expect(page.locator('#selected-text-preview')).toHaveText('Monthly Revenue'); + await page.getByLabel('Question about selected context').focus(); + await expect(page.locator('.text-capture-mark').first()).toBeVisible(); +}); + +test('current capabilities and release boundaries are explicit', async ({ page }) => { + await expect(page.getByRole('link', { name: 'Explore Bridge' })).toHaveAttribute('href', /guide\/bridge$/); + await expect(page.locator('.release-note')).toContainText('not yet released to npm'); + await expect(page.locator('main')).not.toContainText('v0.15.0'); + const duplicateIds = await page.locator('[id]').evaluateAll(elements => { + const ids = elements.map(element => element.id); + return ids.filter((id, index) => ids.indexOf(id) !== index); + }); + expect(duplicateIds).toEqual([]); +}); diff --git a/site/www/index.html b/site/www/index.html index f7e8ebc1..f2b72a3c 100644 --- a/site/www/index.html +++ b/site/www/index.html @@ -3,16 +3,16 @@ - askable-ui — Your LLM doesn't know what the user is looking at. Fix that. - + askable-ui — UI context for AI assistants + - - + + - - + + @@ -25,7 +25,7 @@ --bg: #ffffff; --ink: #111317; --ink-2: #45474d; - --ink-3: #8a8f98; + --ink-3: #626872; --line: rgba(0,0,0,0.08); --line-strong: rgba(0,0,0,0.14); --accent: #4f46e5; @@ -33,30 +33,25 @@ --red: #dc2626; --yellow: #d97706; --radius-pill: 9999px; - --radius-card: 24px; - --radius-lg: 32px; + --radius-card: 8px; + --radius-lg: 8px; --font: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace; } - html { scroll-behavior: smooth; } + html { scroll-behavior: smooth; scroll-padding-top: 6rem; } + :focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; } + [hidden] { display: none !important; } + .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } body { font-family: var(--font); background: var(--bg); color: var(--ink); line-height: 1.6; min-height: 100vh; - overflow-x: hidden; -webkit-font-smoothing: antialiased; } - #field-canvas { - position: fixed; - inset: 0; - pointer-events: none; - z-index: -1; - } - /* NAV */ nav { position: fixed; @@ -91,7 +86,7 @@ font-weight: 800; color: var(--ink); text-decoration: none; - letter-spacing: -.035em; + letter-spacing: 0; white-space: nowrap; } .nav-mark { @@ -129,6 +124,12 @@ transition: color .16s, background .16s, transform .16s; } .nav-link:hover { color: var(--ink); background: rgba(0,0,0,0.045); transform: translateY(-1px); } + .nav-menu { display: none; position: relative; } + .nav-menu summary { cursor: pointer; list-style: none; padding: .5rem .75rem; font-size: .85rem; font-weight: 600; } + .nav-menu summary::-webkit-details-marker { display: none; } + .nav-menu-panel { position: absolute; right: 0; top: calc(100% + .75rem); width: 13rem; padding: .5rem; background: #fff; border: 1px solid var(--line-strong); border-radius: 8px; box-shadow: 0 8px 24px rgba(17,19,23,.1); } + .nav-menu-panel a { display: block; padding: .65rem .75rem; color: var(--ink); text-decoration: none; border-radius: 4px; } + .nav-menu-panel a:hover { background: #f4f4f5; } .nav-gh { display: inline-flex; align-items: center; @@ -166,7 +167,7 @@ cursor: pointer; text-decoration: none; transition: transform .15s, opacity .15s, box-shadow .15s; - letter-spacing: -.01em; + letter-spacing: 0; } .btn:hover { transform: translateY(-1px); } .btn-primary { background: var(--ink); color: #fff; box-shadow: 0 4px 16px rgba(0,0,0,0.14); } @@ -185,49 +186,11 @@ /* LAYOUT */ .page-shell { max-width: 1280px; margin: 0 auto; padding: 0 2rem 8rem; } - /* HERO */ - .hero { - display: grid; - grid-template-columns: minmax(0, 1.12fr) minmax(320px, .88fr); - gap: 3rem; - align-items: center; - padding: 7rem 0 5rem; - } - .eyebrow { - display: inline-flex; - align-items: center; - gap: .5rem; - padding: .35rem .85rem; - border-radius: var(--radius-pill); - border: 1px solid rgba(0,0,0,0.08); - background: rgba(0,0,0,0.03); - color: var(--ink-3); - font-size: .72rem; - font-weight: 700; - letter-spacing: .05em; - text-transform: uppercase; - margin-bottom: 1.35rem; - width: fit-content; - } - .eyebrow .spark { color: var(--accent); font-style: normal; } - .hero-copy h1 { - font-size: clamp(3rem, 7.5vw, 5.75rem); - line-height: 1.06; - letter-spacing: -.025em; - font-weight: 750; - margin-bottom: 1.35rem; - max-width: 11.4ch; - text-wrap: balance; - } - .hero-copy h1 em { font-style: normal; color: var(--accent); } - .hero-copy > p { - max-width: 36rem; - font-size: 1.05rem; - line-height: 1.68; - color: var(--ink-2); - margin-bottom: 2rem; - } - .hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; margin-bottom: 1.5rem; } + /* INTRO */ + .hero { padding: 7rem 0 2.5rem; } + .hero-copy h1 { font-size: 3.5rem; line-height: 1.08; font-weight: 750; margin-bottom: 1rem; } + .hero-copy > p { max-width: 40rem; font-size: 1.15rem; line-height: 1.6; color: var(--ink-2); margin-bottom: 1.5rem; text-wrap: pretty; } + .hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; } .install-cmd { display: inline-flex; align-items: center; @@ -246,134 +209,32 @@ } .install-cmd:hover { border-color: rgba(0,0,0,0.18); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,0.07); } .copy-icon { color: var(--ink-3); font-size: .75rem; } - .hero-meta { display: flex; flex-wrap: wrap; gap: 1.25rem; font-size: .82rem; color: var(--ink-3); } - .hero-patterns { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.25rem; } - .hero-pattern-pill { - display: inline-flex; align-items: center; gap: .35rem; - border: 1px solid rgba(79,70,229,0.15); background: rgba(79,70,229,0.06); - color: #3730a3; border-radius: var(--radius-pill); padding: .36rem .7rem; - font-size: .75rem; font-weight: 700; - } - - /* HERO CARD */ - .hero-stage { position: relative; perspective: 1200px; } - .hero-card { - position: sticky; - top: 4.25rem; - border-radius: var(--radius-lg); - overflow: hidden; - background: #fff; - border: 1px solid rgba(0,0,0,0.09); - box-shadow: 0 32px 80px rgba(0,0,0,0.09), 0 2px 8px rgba(0,0,0,0.04); - transform-style: preserve-3d; - transition: transform .15s ease; - } - .hero-card-top { - display: flex; - align-items: center; - justify-content: space-between; - padding: .9rem 1rem .78rem; - border-bottom: 1px solid rgba(0,0,0,0.06); - } - .hero-window-dots { display: flex; gap: .38rem; } - .hero-window-dots span { width: .62rem; height: .62rem; border-radius: 50%; background: #e4e7ec; } - .hero-chip { - border: 1px solid rgba(0,0,0,0.07); - background: rgba(0,0,0,0.03); - color: var(--ink-3); - border-radius: var(--radius-pill); - padding: .28rem .65rem; - font-size: .68rem; - font-weight: 700; - letter-spacing: .02em; - max-width: 13rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - } - .hero-card-body { padding: 1rem; } - .lens-grid { display: grid; grid-template-columns: 1.1fr .9fr; gap: .85rem; align-items: stretch; } - - .mini-board, .mini-chat, .mini-code { - border-radius: 18px; - border: 1px solid rgba(0,0,0,0.07); - background: #f9fafb; - } - .mini-board { padding: .85rem; display: grid; gap: .72rem; min-width: 0; } - .mini-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; min-width: 0; } - .mini-stat { - border-radius: 12px; padding: .82rem; background: #fff; - border: 1px solid rgba(0,0,0,0.07); - transition: transform .18s, border-color .18s, box-shadow .18s; - min-width: 0; overflow: hidden; cursor: pointer; - } - .mini-stat.highlight { transform: translateY(-2px); border-color: rgba(79,70,229,0.22); box-shadow: 0 8px 22px rgba(79,70,229,0.09); } - .mini-label { color: var(--ink-3); font-size: .66rem; text-transform: uppercase; letter-spacing: .07em; overflow-wrap: anywhere; } - .mini-value { font-size: clamp(.92rem, 1.3vw, 1.2rem); font-weight: 800; letter-spacing: -.03em; margin: .18rem 0; line-height: 1.1; overflow-wrap: anywhere; word-break: break-word; } - .mini-sub { font-size: .7rem; color: var(--ink-2); overflow-wrap: anywhere; word-break: break-word; } - .mini-table { border-radius: 12px; border: 1px solid rgba(0,0,0,0.06); overflow: hidden; } - .mini-table-row { - display: grid; grid-template-columns: minmax(0,1.2fr) minmax(0,.8fr) minmax(0,.8fr); - gap: .4rem; padding: .58rem .78rem; font-size: .72rem; - border-bottom: 1px solid rgba(0,0,0,0.05); align-items: center; - } - .mini-table-row > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .mini-table-row:last-child { border-bottom: none; } - .mini-table-row.is-active { background: rgba(79,70,229,0.05); box-shadow: inset 3px 0 0 var(--accent); } - .mini-chat { padding: .85rem; display: flex; flex-direction: column; justify-content: space-between; gap: .75rem; min-width: 0; } - .mini-chat-bubble { - border-radius: 12px; padding: .68rem .82rem; font-size: .74rem; line-height: 1.52; - border: 1px solid rgba(0,0,0,0.07); background: #fff; color: var(--ink); - overflow-wrap: anywhere; word-break: break-word; - } - .mini-chat-bubble.user { background: var(--ink); border-color: transparent; color: #fff; align-self: flex-end; max-width: 90%; } - .mini-code { - margin-top: .85rem; padding: .85rem; font-family: var(--mono); font-size: .67rem; - line-height: 1.55; color: var(--ink-2); min-height: 80px; - white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; - } - /* SECTIONS */ - .section { padding-top: 6.5rem; } - .section-label { text-align: center; color: var(--ink-3); text-transform: uppercase; letter-spacing: .14em; font-size: .68rem; font-weight: 700; margin-bottom: .8rem; } - .section-title { text-align: center; font-size: clamp(2rem, 4vw, 2.8rem); line-height: 1.05; letter-spacing: -.055em; font-weight: 800; margin-bottom: .65rem; } + .section { padding-top: 5rem; } + #demo { padding-top: 1rem; } + #demo .section-title { font-size: 1.5rem; text-align: left; margin-bottom: 1rem; } + .section > *, .steps > *, .pkg-grid > * { min-width: 0; } + pre { max-width: 100%; } + h2, h3, h4, .pkg-name { overflow-wrap: anywhere; } + .section-label { text-align: center; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0; font-size: .68rem; font-weight: 700; margin-bottom: .8rem; } + .section-title { text-align: center; font-size: 2.25rem; line-height: 1.05; letter-spacing: 0; font-weight: 800; margin-bottom: .65rem; } .section-sub { text-align: center; max-width: 42rem; margin: 0 auto 2.5rem; color: var(--ink-2); font-size: .98rem; line-height: 1.62; } - /* INSIGHTS */ - .insight-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 1.5rem; } - .insight-card { - padding: 1.5rem; border-radius: var(--radius-card); border: 1px solid var(--line); background: #fff; - transition: transform .18s, border-color .18s, box-shadow .18s; - } - .insight-card:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: 0 12px 36px rgba(0,0,0,0.06); } - .insight-eyebrow { font-size: .68rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); margin-bottom: .6rem; } - .insight-card h3 { font-size: 1.04rem; line-height: 1.22; letter-spacing: -.03em; font-weight: 700; margin-bottom: .45rem; } - .insight-card p { color: var(--ink-2); font-size: .87rem; line-height: 1.62; } - - /* PATTERNS */ - .pattern-shell { - display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); - gap: 1rem; align-items: stretch; margin-top: 2.2rem; - } - .pattern-copy { - border-radius: var(--radius-lg); border: 1px solid var(--line); background: #fff; - padding: 1.7rem; display: flex; flex-direction: column; justify-content: space-between; gap: 1.5rem; - } - .pattern-copy h3 { font-size: 1.65rem; line-height: 1.08; letter-spacing: -.045em; } - .pattern-copy p { color: var(--ink-2); font-size: .92rem; line-height: 1.65; } - .pattern-stack { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem; } - .pattern-card { - border-radius: 18px; border: 1px solid var(--line); background: #fff; - padding: 1rem; transition: transform .16s, border-color .16s, box-shadow .16s; - } - .pattern-card:hover { transform: translateY(-2px); border-color: rgba(79,70,229,0.18); box-shadow: 0 12px 30px rgba(0,0,0,0.055); } - .pattern-icon { - width: 2rem; height: 2rem; display: inline-flex; align-items: center; justify-content: center; - border-radius: 12px; background: rgba(79,70,229,0.08); color: var(--accent); - font-weight: 850; margin-bottom: .7rem; - } - .pattern-card h4 { font-size: .96rem; letter-spacing: -.025em; margin-bottom: .35rem; } - .pattern-card p { color: var(--ink-2); font-size: .8rem; line-height: 1.55; } + /* PRODUCT CAPABILITIES */ + .capability-list, .integration-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2rem; } + .capability-list article, .integration-list article { min-width: 0; } + .capability-list article { padding-top: 1.25rem; border-top: 1px solid var(--line-strong); } + .capability-list h3, .integration-list h3 { font-size: 1.1rem; line-height: 1.35; margin-bottom: .65rem; } + .capability-list p, .integration-list p { color: var(--ink-2); font-size: .9rem; margin-bottom: 1rem; } + .text-link { color: var(--accent); font-size: .88rem; font-weight: 600; text-underline-offset: .2em; } + .capability-list .release-note { font-size: .8rem; color: var(--ink-3); } + .recording { margin-top: 2rem; border-top: 1px solid var(--line); padding-top: 1rem; } + .recording summary { cursor: pointer; font-weight: 600; } + .recording video { display: block; width: 100%; aspect-ratio: 16 / 9; margin-top: 1rem; border-radius: 8px; background: #f5f5f5; } + .integration-note { margin-top: 2rem; max-width: 65ch; color: var(--ink-2); font-size: .9rem; } /* DEMO DECK */ - .deck { padding: 1.5rem; border-radius: var(--radius-lg); border: 1px solid var(--line); background: #f9fafb; } + .deck { padding: 0; border-top: 1px solid var(--line-strong); } .demo-controls { display: grid; gap: .8rem; margin-bottom: 1.35rem; } .demo-controls-row { display: flex; justify-content: center; align-items: center; gap: .6rem; flex-wrap: wrap; } .toggle-label { color: var(--ink-3); font-size: .82rem; } @@ -382,27 +243,26 @@ .toggle-btn.active { color: var(--ink); background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.09); } .pattern-dock { - border: 1px solid var(--line); background: #fff; border-radius: 20px; padding: .72rem; - box-shadow: 0 8px 24px rgba(0,0,0,0.045); + background: #fff; padding: 1rem 0; border-bottom: 1px solid var(--line); } .pattern-dock-header { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: .08rem .18rem .62rem; } - .pattern-dock-header span { color: var(--ink-3); font-size: .72rem; font-weight: 750; text-transform: uppercase; letter-spacing: .06em; } - .pattern-dock-header strong { font-size: .88rem; letter-spacing: -.02em; } + .pattern-dock-header span { color: var(--ink-3); font-size: .72rem; font-weight: 750; text-transform: uppercase; letter-spacing: 0; } + .pattern-dock-header strong { font-size: .88rem; letter-spacing: 0; } .pattern-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: .36rem; } .pattern-option { display: grid; grid-template-columns: 1.45rem minmax(0, 1fr); align-items: center; gap: .48rem; border: 1px solid transparent; background: #f8fafc; color: var(--ink-2); - border-radius: 14px; padding: .55rem .62rem; cursor: pointer; text-align: left; + border-radius: 6px; padding: .65rem .5rem; cursor: pointer; text-align: left; transition: transform .14s, box-shadow .14s, border-color .14s, background .14s; min-width: 0; } .pattern-option:hover { transform: translateY(-1px); border-color: rgba(79,70,229,0.16); background: #fff; } .pattern-option.active { background: #fff; color: var(--ink); border-color: rgba(79,70,229,0.26); - box-shadow: 0 7px 18px rgba(79,70,229,0.1); + box-shadow: inset 0 -2px 0 var(--accent); } .pattern-glyph { width: 1.45rem; height: 1.45rem; display: inline-flex; align-items: center; justify-content: center; @@ -410,12 +270,16 @@ font-size: .82rem; font-weight: 850; } .pattern-copy-mini { min-width: 0; display: grid; gap: .08rem; } - .pattern-copy-mini b { display: block; font-size: .78rem; line-height: 1.08; letter-spacing: -.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .pattern-copy-mini b { display: block; font-size: .78rem; line-height: 1.08; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pattern-copy-mini small { display: block; color: var(--ink-3); font-size: .62rem; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .pattern-status { - display: flex; align-items: center; gap: .55rem; margin-top: .58rem; padding: .5rem .58rem; - border: 1px solid rgba(0,0,0,0.06); background: #fbfcfe; border-radius: 14px; - } + .pattern-status { display: flex; align-items: center; gap: .55rem; margin-top: .75rem; } + .pattern-payload, .tool-hint { display: none !important; } + .developer-panel { margin-top: 1.5rem; border-top: 1px solid var(--line); padding-top: 1rem; } + .developer-panel > summary { width: fit-content; cursor: pointer; font-size: .9rem; font-weight: 600; } + .developer-panel .output-controls { margin-top: 1rem; justify-content: flex-start; } + .context-review { flex: 1; min-width: 0; } + .context-review summary { cursor: pointer; font-size: .8rem; font-weight: 600; } + .context-review pre { white-space: pre-wrap; overflow-wrap: anywhere; font: .72rem/1.6 var(--mono); max-height: 8rem; overflow-y: auto; padding-top: .5rem; } .pattern-payload { flex: 0 0 auto; max-width: 45%; border-radius: var(--radius-pill); padding: .22rem .5rem; background: rgba(79,70,229,0.08); color: #3730a3; font-size: .68rem; font-weight: 800; @@ -493,7 +357,7 @@ padding: .28rem .52rem; border-radius: var(--radius-pill); border: 1px solid rgba(124,58,237,0.18); background: rgba(255,255,255,0.94); color: #5b21b6; font-size: .66rem; font-weight: 850; - letter-spacing: .05em; text-transform: uppercase; + letter-spacing: 0; text-transform: uppercase; box-shadow: 0 10px 24px rgba(15,23,42,0.1); white-space: nowrap; } .selected-region-label::before { @@ -562,11 +426,11 @@ .kpi-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .85rem; } .panel { background: #fff; border: 1px solid var(--line); border-radius: var(--radius-card); overflow: hidden; min-width: 0; } - .panel-header { display: flex; justify-content: space-between; align-items: center; padding: .88rem 1.1rem; border-bottom: 1px solid rgba(0,0,0,0.05); font-size: .87rem; font-weight: 700; letter-spacing: -.01em; } + .panel-header { display: flex; justify-content: space-between; align-items: center; padding: .88rem 1.1rem; border-bottom: 1px solid rgba(0,0,0,0.05); font-size: .87rem; font-weight: 700; letter-spacing: 0; } .panel-header span { color: var(--ink-3); font-size: .72rem; font-weight: 500; } table { width: 100%; border-collapse: collapse; font-size: .87rem; } - th { text-align: left; padding: .62rem 1.1rem; font-size: .68rem; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3); border-bottom: 1px solid rgba(0,0,0,0.05); } + th { text-align: left; padding: .62rem 1.1rem; font-size: .68rem; letter-spacing: 0; text-transform: uppercase; color: var(--ink-3); border-bottom: 1px solid rgba(0,0,0,0.05); } td { padding: .84rem 1.1rem; border-bottom: 1px solid rgba(0,0,0,0.04); } tbody tr[data-askable] { outline: none; cursor: pointer; transition: background .15s; } tbody tr[data-askable]:hover, tbody tr[data-askable]:focus { background: rgba(0,0,0,0.02); } @@ -583,8 +447,8 @@ overflow: hidden; transition: transform .2s, border-color .2s, box-shadow .2s; } .kpi-card:hover, .kpi-card:focus, .kpi-card.active { transform: translateY(-3px); border-color: rgba(79,70,229,0.2); box-shadow: 0 14px 36px rgba(0,0,0,0.07); } - .kpi-label { font-size: .68rem; text-transform: uppercase; letter-spacing: .09em; color: var(--ink-3); margin-bottom: .5rem; } - .kpi-value { font-size: 1.9rem; font-weight: 800; letter-spacing: -.05em; margin-bottom: .18rem; } + .kpi-label { font-size: .68rem; text-transform: uppercase; letter-spacing: 0; color: var(--ink-3); margin-bottom: .5rem; } + .kpi-value { font-size: 1.9rem; font-weight: 800; letter-spacing: 0; margin-bottom: .18rem; } .kpi-change { font-size: .81rem; font-weight: 600; } .up { color: var(--green); } .down { color: var(--red); } @@ -606,10 +470,10 @@ min-width: 0; overflow: hidden; background: #fff; border: 1px solid var(--line); border-radius: var(--radius-card); } .chat-header { display: flex; align-items: center; gap: .55rem; padding: .88rem 1rem; border-bottom: 1px solid rgba(0,0,0,0.05); } - .chat-dot { width: .62rem; height: .62rem; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 5px rgba(22,163,74,0.1); animation: pulse 1.8s ease-in-out infinite; } + .chat-dot { display: none; } @keyframes pulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(.85);opacity:.7} } - .chat-title { font-size: .87rem; font-weight: 700; letter-spacing: -.02em; } - .chat-sim-badge { font-size: .6rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); background: rgba(0,0,0,0.05); padding: .18rem .5rem; border-radius: var(--radius-pill); } + .chat-title { font-size: .87rem; font-weight: 700; letter-spacing: 0; } + .chat-sim-badge { font-size: .6rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0; color: var(--ink-3); background: rgba(0,0,0,0.05); padding: .18rem .5rem; border-radius: var(--radius-pill); } .chat-sub { margin-left: auto; font-size: .68rem; color: var(--ink-3); } .chat-messages { flex: 1 1 0; min-height: 0; padding: .88rem; overflow-y: auto; display: flex; flex-direction: column; gap: .6rem; overscroll-behavior: contain; } .chat-messages::-webkit-scrollbar { width: 4px; } @@ -625,7 +489,7 @@ background: linear-gradient(180deg, rgba(124,58,237,0.08), rgba(79,70,229,0.04)); color: var(--ink); padding: .64rem .72rem; border-radius: 14px; } - .msg-context-kicker { display: block; margin-bottom: .28rem; color: #6d28d9; font-size: .62rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; } + .msg-context-kicker { display: block; margin-bottom: .28rem; color: #6d28d9; font-size: .62rem; font-weight: 800; letter-spacing: 0; text-transform: uppercase; } .msg-context-quote { display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; color: var(--ink-2); font-size: .76rem; line-height: 1.48; } .msg-label { font-size: .65rem; color: var(--ink-3); padding: 0 .2rem; } .msg.user .msg-label { align-self: flex-end; } @@ -635,10 +499,10 @@ @keyframes bounce { 0%,80%,100%{transform:translateY(0)} 40%{transform:translateY(-4px)} } .chat-context-bar { display: flex; gap: .45rem; align-items: center; padding: .62rem .9rem; background: rgba(79,70,229,0.05); border-top: 1px solid rgba(79,70,229,0.12); } .chat-context-bar.text-context { background: rgba(124,58,237,0.07); border-top-color: rgba(124,58,237,0.18); } - .context-bar-label { font-size: .68rem; font-weight: 600; color: var(--accent); letter-spacing: .04em; text-transform: uppercase; flex-shrink: 0; } + .context-bar-label { font-size: .68rem; font-weight: 600; color: var(--accent); letter-spacing: 0; text-transform: uppercase; flex-shrink: 0; } .chat-context-bar.text-context .context-bar-label { color: #6d28d9; } .context-chip { flex: 1; font-family: var(--mono); font-size: .69rem; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .context-dismiss { border: none; background: none; color: var(--ink-3); cursor: pointer; font-size: .95rem; } + .context-dismiss { flex: 0 0 32px; width: 32px; height: 32px; border: none; background: none; color: var(--ink-3); cursor: pointer; font-size: .95rem; } .selected-text-card { margin: .72rem .78rem 0; padding: .72rem .78rem; border-radius: 14px; border: 1px solid rgba(124,58,237,0.18); @@ -650,7 +514,7 @@ background: linear-gradient(180deg, rgba(79,70,229,0.075), rgba(255,255,255,0.9)); } .selected-text-card-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; margin-bottom: .44rem; } - .selected-text-card-title { display: flex; align-items: center; gap: .42rem; color: #5b21b6; font-size: .7rem; font-weight: 850; letter-spacing: .05em; text-transform: uppercase; } + .selected-text-card-title { display: flex; align-items: center; gap: .42rem; color: #5b21b6; font-size: .7rem; font-weight: 850; letter-spacing: 0; text-transform: uppercase; } .selected-text-card-title::before { content: '“'; display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 999px; background: #6d28d9; color: #fff; font-size: .9rem; line-height: 1; } .selected-text-card-state { color: var(--ink-3); font-size: .64rem; font-weight: 700; } .selected-text-quote { margin: 0; color: var(--ink); font-size: .78rem; line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; } @@ -677,9 +541,9 @@ .question-chip:hover { background: rgba(124,58,237,0.1); border-color: rgba(124,58,237,0.24); transform: translateY(-1px); } .chat-input-row { display: flex; gap: .5rem; padding: .78rem; border-top: 1px solid rgba(0,0,0,0.06); } .composer-suggestions + .chat-input-row { border-top: none; } - .chat-input { flex: 1; border-radius: var(--radius-pill); padding: .72rem 1rem; border: 1px solid rgba(0,0,0,0.1); background: #fff; font-size: .81rem; color: var(--ink); outline: none; font-family: var(--font); transition: border-color .15s, box-shadow .15s; } + .chat-input { min-width: 0; flex: 1; border-radius: var(--radius-pill); padding: .72rem 1rem; border: 1px solid rgba(0,0,0,0.1); background: #fff; font-size: .81rem; color: var(--ink); outline: none; font-family: var(--font); transition: border-color .15s, box-shadow .15s; } .chat-input:focus { border-color: rgba(79,70,229,0.28); box-shadow: 0 0 0 4px rgba(79,70,229,0.07); } - .chat-send { width: 38px; height: 38px; border-radius: 50%; border: none; background: var(--ink); color: #fff; cursor: pointer; font-size: .9rem; transition: opacity .15s; } + .chat-send { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; border: none; background: var(--ink); color: #fff; cursor: pointer; font-size: .9rem; transition: opacity .15s; } .chat-send:hover { opacity: .82; } /* CODE PANEL */ @@ -692,10 +556,9 @@ /* STEPS */ .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 2.5rem; } - .step { padding: 1.5rem; border-radius: var(--radius-card); border: 1px solid var(--line); background: #fff; transition: transform .18s, border-color .18s, box-shadow .18s; } - .step:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: 0 12px 36px rgba(0,0,0,0.06); } + .step { padding: 1rem 0; min-width: 0; } .step-num { width: 1.9rem; height: 1.9rem; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-pill); background: var(--ink); color: #fff; font-weight: 800; font-size: .78rem; margin-bottom: 1rem; } - .step h3 { margin-bottom: .44rem; font-size: 1rem; font-weight: 700; letter-spacing: -.025em; } + .step h3 { margin-bottom: .44rem; font-size: 1rem; font-weight: 700; letter-spacing: 0; } .step p { color: var(--ink-2); font-size: .87rem; line-height: 1.62; } .step-code { margin-top: .85rem; padding: .75rem .9rem; border-radius: 12px; background: #f6f7f9; border: 1px solid rgba(0,0,0,0.06); font-family: var(--mono); font-size: .72rem; line-height: 1.6; color: var(--ink-2); white-space: pre-wrap; word-break: break-word; } @@ -703,45 +566,18 @@ .pkg-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 2.5rem; } .pkg-card { display: block; text-decoration: none; color: inherit; padding: 1.4rem; border-radius: var(--radius-card); border: 1px solid var(--line); background: #fff; transition: transform .18s, border-color .18s, box-shadow .18s; } .pkg-card:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: 0 12px 36px rgba(0,0,0,0.07); } - .pkg-badge { font-size: .64rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-3); margin-bottom: .6rem; } - .pkg-name { font-family: var(--mono); color: var(--ink); font-size: .85rem; font-weight: 700; margin-bottom: .38rem; letter-spacing: -.01em; } + .pkg-badge { font-size: .64rem; font-weight: 700; letter-spacing: 0; text-transform: uppercase; color: var(--ink-3); margin-bottom: .6rem; } + .pkg-name { font-family: var(--mono); color: var(--ink); font-size: .85rem; font-weight: 700; margin-bottom: .38rem; letter-spacing: 0; } .pkg-desc { color: var(--ink-2); font-size: .83rem; line-height: 1.56; } .pkg-tag { display: inline-flex; align-items: center; gap: .3rem; margin-top: .78rem; padding: .22rem .52rem; border-radius: var(--radius-pill); border: 1px solid rgba(0,0,0,0.08); color: var(--ink-3); font-size: .66rem; font-weight: 600; } - .mcp-panel { margin-top: 2.5rem; border-radius: var(--radius-lg); border: 1px solid var(--line); background: #fff; overflow: hidden; } - .mcp-panel-head { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); gap: 1.5rem; padding: 1.6rem; border-bottom: 1px solid rgba(0,0,0,0.06); } - .mcp-panel h3 { font-size: clamp(1.35rem, 2.4vw, 2rem); line-height: 1.05; letter-spacing: -.045em; margin-bottom: .6rem; } - .mcp-panel p { color: var(--ink-2); font-size: .9rem; line-height: 1.62; } - .mcp-endpoint { display: flex; align-items: center; gap: .5rem; align-self: start; padding: .8rem .95rem; border: 1px solid rgba(79,70,229,0.16); border-radius: 16px; background: rgba(79,70,229,0.05); font-family: var(--mono); color: var(--accent); font-size: .78rem; word-break: break-all; } - .mcp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); } - .mcp-card { padding: 1.25rem; border-right: 1px solid rgba(0,0,0,0.06); } - .mcp-card:last-child { border-right: 0; } - .mcp-card h4 { font-size: .92rem; letter-spacing: -.02em; margin-bottom: .42rem; } - .mcp-card pre { margin-top: .82rem; padding: .8rem; border-radius: 12px; background: #f6f7f9; border: 1px solid rgba(0,0,0,0.06); font-family: var(--mono); font-size: .68rem; line-height: 1.58; color: var(--ink-2); white-space: pre-wrap; word-break: break-word; } - - /* COMPARE */ - .compare-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 2rem; } - .compare-card { background: #fff; border-radius: var(--radius-lg); border: 1px solid var(--line); padding: 1.5rem; } - .compare-bad { border-color: rgba(239,68,68,0.18); background: #fff9f9; } - .compare-good { border-color: rgba(79,70,229,0.28); background: var(--accent-faint); } - .compare-icon { font-size: 1.5rem; margin-bottom: .75rem; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; border-radius: 10px; background: #fee2e2; } - .compare-card h3 { font-size: 1rem; font-weight: 700; letter-spacing: -.02em; margin-bottom: .75rem; } - .compare-card ul { list-style: none; display: flex; flex-direction: column; gap: .45rem; } - .compare-bad ul li::before { content: '✗ '; color: #ef4444; font-weight: 700; } - .compare-good ul li::before { content: '✓ '; color: var(--accent); font-weight: 700; } - .compare-card li { font-size: .85rem; color: var(--ink-2); line-height: 1.45; } - - /* DARK CTA */ - .cta-section { margin-top: 7rem; } - .cta-card { border-radius: var(--radius-lg); background: var(--ink); padding: 5rem 4rem; text-align: center; position: relative; overflow: hidden; } - .cta-card::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse at 25% 60%,rgba(79,70,229,0.35),transparent 55%), radial-gradient(ellipse at 80% 30%,rgba(124,58,237,0.25),transparent 50%); pointer-events: none; } - .cta-card h2 { font-size: clamp(2rem, 4vw, 3.25rem); letter-spacing: -.055em; font-weight: 850; color: #fff; margin-bottom: .85rem; position: relative; } - .cta-card p { color: rgba(255,255,255,0.55); font-size: .98rem; line-height: 1.65; margin-bottom: 2.25rem; max-width: 36rem; margin-left: auto; margin-right: auto; position: relative; } - .cta-actions { display: flex; gap: .75rem; justify-content: center; flex-wrap: wrap; position: relative; } - .btn-inverse { background: #fff; color: var(--ink); } - .btn-inverse:hover { opacity: .9; } - .btn-ghost-white { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.13); } - .btn-ghost-white:hover { background: rgba(255,255,255,0.16); } + .cta-section { margin-top: 5rem; padding-top: 3rem; border-top: 1px solid var(--line-strong); } + .cta-card { text-align: center; } + .cta-card h2 { font-size: 2rem; line-height: 1.2; margin-bottom: 1rem; } + .cta-card p { color: var(--ink-2); max-width: 36rem; margin: 0 auto 1.5rem; } + .cta-actions { display: flex; gap: .75rem; justify-content: center; flex-wrap: wrap; } + .btn-inverse { background: var(--ink); color: #fff; } + .btn-ghost-white { background: #f4f4f5; color: var(--ink); } code { font-family: var(--mono); font-size: .9em; padding: .1em .38em; border-radius: .42em; color: var(--accent); background: rgba(79,70,229,0.08); } @@ -751,19 +587,15 @@ /* REDUCED MOTION */ @media (prefers-reduced-motion: reduce) { - #field-canvas { display: none; } + html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } /* RESPONSIVE */ @media (max-width: 1040px) { - .hero { grid-template-columns: 1fr; } - .hero-stage { min-height: 0; } - .hero-card { position: relative; top: auto; } .demo-layout { grid-template-columns: 1fr; } .chat-sidebar { position: relative; top: auto; height: 480px; max-height: none; } - .insight-grid, .steps, .pattern-stack { grid-template-columns: 1fr 1fr; } - .pattern-shell { grid-template-columns: 1fr; } + .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } .pkg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } } @media (max-width: 720px) { @@ -779,22 +611,27 @@ .nav-links { min-height: 2.55rem; padding: .22rem; } .nav-gh { width: 2.08rem; height: 2.08rem; margin-left: 0; } .nav-link { display: none; } + .nav-menu { display: block; } .page-shell { padding: 0 1rem 4rem; } - .hero { padding-top: 3.5rem; } - .lens-grid, .code-panel, .kpi-grid { grid-template-columns: 1fr; } - .insight-grid, .steps, .pattern-stack { grid-template-columns: 1fr; } + .hero { padding: 6rem 0 1.5rem; } + .hero-copy h1 { font-size: 2.5rem; } + .hero-copy > p { font-size: 1rem; } + .section-title { font-size: 1.75rem; } + .hero-actions { gap: .65rem; } + .install-cmd { max-width: 100%; font-size: .72rem; gap: .4rem; padding-inline: .75rem; } + .capability-list, .integration-list { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; } + .code-panel, .kpi-grid, .steps { grid-template-columns: minmax(0, 1fr); } .pkg-grid { grid-template-columns: 1fr !important; } - .deck { padding: 1rem; border-radius: 24px; } + .deck { padding: 0; } .demo-controls-row { align-items: stretch; flex-wrap: wrap; } - .pattern-dock { padding: .62rem; border-radius: 18px; } - .pattern-dock-header { align-items: flex-start; flex-direction: column; gap: .2rem; } - .pattern-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .pattern-option { grid-template-columns: 1.35rem minmax(0, 1fr); padding: .52rem; } + .pattern-dock { padding: .75rem 0; } + .pattern-dock-header { gap: .5rem; } + .pattern-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } + .pattern-copy-mini small, .pattern-glyph { display: none; } + .pattern-copy-mini b { font-size: .72rem; white-space: normal; overflow: visible; } + .pattern-option { display: flex; justify-content: center; min-height: 44px; padding: .5rem .2rem; } .pattern-status { align-items: flex-start; flex-direction: column; } .pattern-payload { max-width: 100%; } - .mcp-panel-head, .mcp-grid, .compare-grid { grid-template-columns: 1fr; } - .mcp-card { border-right: 0; border-bottom: 1px solid rgba(0,0,0,0.06); } - .mcp-card:last-child { border-bottom: 0; } .tool-hint { min-width: 0; } .toggle-group { justify-content: stretch; } .toggle-btn { flex: 1 1 0; padding-inline: .65rem; } @@ -827,7 +664,7 @@ content: attr(data-label); color: var(--ink-3); font-size: .66rem; - letter-spacing: .08em; + letter-spacing: 0; text-transform: uppercase; font-weight: 700; } @@ -838,18 +675,28 @@ } td:last-child::before { display: none; } .ask-btn-row { font-size: .68rem; padding: .38rem .72rem; } - .cta-card { padding: 3rem 1.5rem; } + .cta-card { padding: 0; } + .chat-sub { display: none; } + .code-block-header { flex-wrap: wrap; } .section { padding-top: 4rem; } footer { flex-direction: column; align-items: flex-start; } } - -
-
+
-
✦ Claude Desktop · Cursor · CopilotKit · any LLM
-

Your model knows what the user means.

-

- Your AI copilot can't tell which metric the user is looking at, which row triggered the question, - or what they highlighted. askable-ui fixes that with one attribute — no screenshots, no stale system - prompts, no guessing. The AI gets the exact data the user sees, updated on every interaction. -

-
- Click - Ask AI button - Region - Square - Circle - Lasso - Selected text -
+

askable-ui

+

Give your AI the context behind the question. Selected text, a chart, an account, or your app's own data.

- - See it live - Documentation - - - -
-
- Element + visual context - Zero framework lock-in - MCP-ready packets - React · Vue · Svelte · Vanilla JS -
-
- -
-
-
-
-
live focus lens
-
-
-
-
-
-
-
MRR
-
$128,400
-
+12.4% this month
-
-
-
Churn
-
3.2%
-
improving
-
-
-
NPS
-
67
-
industry avg 44
-
-
-
Upsell
-
Umbrella
-
starter → business
-
-
-
-
Acme$8.4khealthy
-
Globex$5.2kat risk
-
Umbrella$2.1kexpanding
-
-
-
-
The assistant can read the element, area, or text the user explicitly selected.
-
What should I do about this circled account?
-
Globex is at risk. No login in 14 days, $5.2k MRR exposed.
-
-
-
<tr data-askable='{"company":"Globex Inc","status":"at_risk","last_login":"14 days ago"}'> - ... -</tr> - -User is focused on: company: Globex Inc, status: at_risk, last_login: 14 days ago
-
-
- -
- -
- -

Not every question starts with a click.

-

Askable gives users more ways to say “this is what I mean” without sending the entire page to the model.

-
-
-
-

Context becomes an intentional user action.

-

Use DOM focus for the common path, then add explicit tools for visual ambiguity: draw over a chart, square off a fixed area, circle an outlier, lasso an irregular area, or send the text the user highlighted.

-
-
region.start({ shape: 'lasso' });
-text.captureNow();
-ctx.push(meta, text);
-
-
-
-
↖
-

Element focus

-

Click, hover, keyboard focus, and Ask AI buttons bind context to existing widgets.

-
-
-
□
-

Region capture

-

Drag a rectangular page area when the answer depends on a visible section.

-
-
-
▣
-

Square capture

-

Constrain the selected area when the target should keep an equal width and height.

-
-
-
○
-

Circle capture

-

Circle an anomaly, data point, product defect, chart spike, or object on screen.

-
-
-
⌁
-

Lasso capture

-

Freehand select irregular content that does not fit cleanly into a box.

-
-
-
T
-

Selected text

-

Send highlighted copy without asking the model to infer which words matter.

-
-
-
◇
-

Context packets

-

Move structured context through MCP bridges, browser surfaces, and agent runtimes.

-
-
-
-
- -
-
-
-
No page scraping
-

Context follows intent, not the whole DOM

-

Other approaches serialize the entire page or guess what matters. askable captures exactly what the user marked: an element, area, shape, or text selection.

-
-
-
App-owned sources
-

Use real state, not just visible DOM

-

Register redacted, timeout-safe resolvers for paginated tables, documents, maps, charts, calendars, or custom state so selected context resolves the right backing data.

-
-
-
Any stack
-

Dashboards, forms, tables, support tools

-

Works anywhere a user points, clicks, draws, circles, lassos, highlights, or hovers. React, Vue, Svelte, or plain HTML.

-
-
-
-
- -

Select the UI. Watch the prompt tighten.

-

Click a KPI, draw a region, circle a row, lasso a panel, or highlight text. The context panel updates in real time.

+

Analytics workspace

@@ -1051,7 +736,7 @@

Select the UI. Watch the prompt tighten.

Interaction pattern Click
-
+
-
- Format: -
- - -
- Prompt text: -
- - -
-
+
@@ -1133,16 +807,19 @@

Select the UI. Watch the prompt tighten.

simulated powered by askable
-
+
-
Click a KPI, draw a region, circle or lasso an area, or send highlighted text — I'll load that context into the input.
+
No context attached yet.
Assistant
+
+ Developer view +
+ Format: +
+ + +
+ Prompt text: +
+ + +
+
@@ -1182,6 +874,30 @@

Select the UI. Watch the prompt tighten.

waiting for focus…
+
+
+
+ +
+

Keep the context with the question.

+

From a single selection to the data behind your app.

+
+
+

Beyond the visible rows

+

Connect app-owned sources for paginated tables, documents, carts, or charts. Include the backing data you choose, not just the rendered page.

+ Connect your data +
+
+

Bring your own chat

+

Bridge selected context to your chat panel, browser extension, iframe, or server. Choose the destination and handle delivery explicitly.

+ Explore Bridge +
+
+

Review before sending

+

Let users inspect a prepared request, then send that snapshot without replacing it with their latest selection.

+

The reviewed-request React API is on main, not yet released to npm.

+ Review the React guide +
@@ -1221,6 +937,13 @@

Inject

+
+ Watch the integration walkthrough + +
+

Pick your stack. Keep the idea.

@@ -1235,31 +958,31 @@

Pick your stack. Keep the idea.

JS · React
@askable-ui/react
-
45+ hooks — useAskableCartSource(), useAskableMultistepSource(), useAskableFormSource(), useAskableTableSource(), useAskableTimeSource(), useAskableThemeSource() and more. <Askable> for React 17+.
+
Components and hooks for selection, app data, and chat. React 17+.
npm
JS · Vue
@askable-ui/vue
-
45+ composables — useAskableCartSource(), useAskableMultistepSource(), useAskableFormSource(), useAskableThemeSource(), useAskableLocaleSource() and more. <Askable> for Vue 3.
+
Reactive context and app-owned sources for Vue 3.
npm
JS · Svelte
@askable-ui/svelte
-
Svelte 4 stores and Svelte 5 runes — 45+ sources including useAskableCartSource(), useAskableMultistepSource(), useAskableScrollSource(), useAskableWindowSource() and more.
+
Svelte 4 stores and Svelte 5 runes for reactive UI context.
npm
JS · SolidJS
@askable-ui/solid
-
45+ reactive primitives for SolidJS — useAskableCartSource(), useAskableMultistepSource(), useAskableNetworkSource(), useAskableFocusSource() and more. <Askable> for SolidJS.
+
Fine-grained reactive context and source primitives for SolidJS.
npm
TS · Angular
@askable-ui/angular
-
Injectable services for Angular 16+ — AskableCartSourceService, AskableMultistepSourceService, AskableFormSourceService, AskableTableSourceService, AskableTimeSourceService and more.
+
Injectable services and directives for Angular 16+.
npm
@@ -1274,239 +997,42 @@

Pick your stack. Keep the idea.

<askable-context> custom element — zero build step, works in HTMX, Ember, or plain HTML via CDN.
npm
-
-
More coming
-
Your framework?
-
The core is framework-agnostic. Writing an adapter is ~30 lines. PRs welcome.
- coming soon -
- -
- -
- -

Plugs into your AI layer.

-

toPromptContext() returns a plain string. Pass it to any LLM framework or API — no adapter needed.

-
-
-
MCP client
-
Claude Desktop
-
Claude sees exactly what the user sees. Ask about the focused element without screenshots.
-
-
-
MCP client
-
Cursor
-
Your IDE agent reads live UI context via the askable MCP server. Accurate, no manual description.
-
-
-
Framework
-
CopilotKit
-
Pass context via useCopilotReadable(). The LLM sees what the user selected.
-
-
-
Framework
-
Vercel AI SDK
-
Inject into the system message or tool context for streaming chat UIs.
-
-
-
Any LLM API
-
OpenAI · Anthropic
-
Append the string to your system or user message. Works with any provider.
-
-
- -
-
-
- -

Put Askable context on the page, then let MCP clients read it.

-

WebMCP is the browser-local path: a page widget connects to a local MCP server and exposes approved page tools, prompts, and resources. Hosted MCP is the separate HTTPS endpoint path for remote MCP clients.

-
-
page widget ↔ local MCP client
-
-
-
-

WebMCP page

-

Add a page-side bridge that exposes the current Askable packet, prompt text, and approved app sources as page-owned context.

-
createAskableMcpPageBridge({
-  provider,
-  allowedOrigins: [
-    window.location.origin
-  ]
-});
-
-
-

Local client

-

The user runs a local WebMCP MCP server in their client, creates a token, then connects the page widget with that token.

-
{
-  "mcpServers": {
-    "webmcp": {
-      "command": "npx",
-      "args": [
-        "-y",
-        "@jason.today/webmcp@latest",
-        "--mcp"
-      ]
-    }
-  }
-}
-
-
-

Page resources

-

Askable can provide the selected element, lassoed region, highlighted text, viewport, or app-owned source as an askable://current page resource.

-
window.postMessage({
-  type: "read_current_resource",
-  options: {
-    resource: { uri: "askable://current" }
-  }
-});
-
-
-

Hosted MCP

-

For server-side use, expose the same provider through a secure HTTPS MCP route with auth, CORS, request limits, and tenancy checks.

-
createAskableMcpWebHandler({
-  authorize,
-  provider,
-  maxRequestBodyBytes: 256 * 1024
-});
-
-
+ +
Transport
+
@askable-ui/bridge
+
Deliver context to a chat UI, extension, iframe, or HTTP endpoint.
+ npm +
- -

Give Claude Desktop eyes into your app.

-

Expose your app's live UI context as an MCP server. Claude, Cursor, and any MCP client can call get_current_context to see exactly what the user is looking at — no screenshots, no description needed.

-
-
-
-

Three lines to an MCP server.

-

Any page annotated with data-askable instantly becomes queryable by AI agents. The MCP bridge translates live DOM state into structured Context packets that agents can read and reason about.

-
-
import { createAskableMcpServer } from '@askable-ui/mcp';
-
-const server = createAskableMcpServer({ provider });
-server.connect(transport); // stdio, SSE, or WebSocket
-
-
-
-
↗
-

get_current_context

-

Returns the focused element's structured metadata, surrounding context, and interaction history as a typed JSON packet.

-
-
-
T
-

format_context_for_prompt

-

Returns a prompt-ready text rendering of the current context — drop it directly into any system message.

-
-
-
🔒
-

Privacy gate built in

-

Set requireRedacted: true to block unredacted packets from leaving the page. PII stays in your app.

-
-
-
◇
-

Schema resource

-

The MCP server exposes the full Context packet JSON Schema so agents can self-document and validate responses.

-
-
-
-
-

claude_desktop_config.json

-
{
-  "mcpServers": {
-    "my-app": {
-      "command": "node",
-      "args": ["./mcp-server.js"]
-    }
-  }
-}
-

→ Full MCP integration guide

-
-
- -
- -

Screenshots don't scale. Descriptions go stale.

-

Every other approach to giving AI context about your UI breaks down under real conditions.

-
-
-
📸
-

Vision / screenshots

-
    -
  • Expensive per token, slow to generate
  • -
  • Fails on dense data tables and charts
  • -
  • Leaks PII to the model if not scrubbed
  • -
  • Can't read dynamic or lazy-loaded data
  • -
+

Connect the assistant you already use.

+

Choose how selected context reaches your assistant. Each connection needs its own setup and permissions.

+
+
+

In-app chat

+

Pass prompt context to your model or use Bridge to deliver a packet to your existing chat UI.

+ AI SDK examples
-
-
📝
-

Hardcoded system prompts

-
    -
  • Describe the page, not the user's current state
  • -
  • Go stale the moment the data changes
  • -
  • Burn context window on irrelevant sections
  • -
  • No awareness of what the user selected or clicked
  • -
+
+

Browser companion

+

Capture page context through the extension example, or connect a page-side bridge to a local MCP companion.

+ Browser extension example
-
-
✦
-

askable-ui

-
    -
  • Structured JSON — no vision model needed
  • -
  • Updates on every click, focus, or scroll event
  • -
  • Privacy-gated before leaving the page
  • -
  • Works with any LLM, framework, or MCP client
  • -
+
+

MCP clients

+

Expose approved context through an MCP server for clients such as Claude Desktop or Cursor. Configure the provider, transport, and authorization.

+ MCP integration guide
-
- -
- -

Every layer of your app. Zero prompt engineering.

-

45+ built-in sources cover UI state, user session, environment, and device — automatically composed into a single context string the AI can reason about.

-
-
-
New in v0.15.0
-
Cart source
-
useAskableCartSource() — items, quantities, subtotal, tax, shipping, coupon. Mutate with addItem, setTotals, clearCart. Works in React, Vue, Svelte, SolidJS, and Angular.
-
-
-
New in v0.15.0
-
Multistep source
-
useAskableMultistepSource() — step name, index, progress %, isComplete. Navigate with next, prev, goTo. Works in all frameworks.
-
-
-
UI & content
-
Forms, tables, search
-
useAskableFormSource(), useAskableTableSource(), useAskableSearchSource(), useAskableTabSource(), useAskableMediaSource().
-
-
-
Environment
-
Device & browser state
-
useAskableThemeSource(), useAskableWindowSource(), useAskableLocaleSource(), useAskableNetworkSource(), useAskablePermissionSource(), useAskableConnectionSource().
-
-
-
Sensors & time
-
Time, location, battery
-
useAskableTimeSource() with business hours config, useAskableGeolocationSource(), useAskableBatterySource(), useAskablePerformanceSource() with Core Web Vitals.
-
-
-
User behaviour
-
Session intelligence
-
useAskableAnalyticsSource(), useAskableAbTestSource(), useAskableFeatureFlagSource(), useAskableIdleSource(), useAskableFocusSource(), useAskableScrollSource().
-
-
-

All sources are composable — read the full API reference →

+

You control what leaves your app. Sanitize sensitive data before sharing it. The redaction gate checks the packet's redaction flag; it does not detect personal information for you.

Your AI. Your UI. Finally connected.

-

Drop data-askable on your most important element. Working context in under five minutes — or scaffold a full copilot app in one command.

+

Drop data-askable on your most important element. Working context in under five minutes — or scaffold a full copilot app in one command.

Read the docs Live demo ↗ @@ -1739,8 +1265,10 @@

Your AI. Your UI. Finally connected.

document.querySelectorAll('[data-format]').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); - document.querySelectorAll('[data-format]').forEach(function(b) { b.classList.remove('active'); }); - btn.classList.add('active'); + document.querySelectorAll('[data-format]').forEach(function(b) { + b.classList.toggle('active', b === btn); + b.setAttribute('aria-pressed', String(b === btn)); + }); promptOpts.format = btn.getAttribute('data-format'); refreshCtxDisplay(); }); @@ -1748,8 +1276,10 @@

Your AI. Your UI. Finally connected.

document.querySelectorAll('[data-text]').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); - document.querySelectorAll('[data-text]').forEach(function(b) { b.classList.remove('active'); }); - btn.classList.add('active'); + document.querySelectorAll('[data-text]').forEach(function(b) { + b.classList.toggle('active', b === btn); + b.setAttribute('aria-pressed', String(b === btn)); + }); promptOpts.includeText = btn.getAttribute('data-text') === 'on'; refreshCtxDisplay(); }); @@ -1760,10 +1290,14 @@

Your AI. Your UI. Finally connected.

} function setPatternState(btn, hint) { if (!btn) return; + document.querySelectorAll('.pattern-option').forEach(function(option) { + option.setAttribute('aria-pressed', String(option === btn)); + }); if (activePatternName) activePatternName.textContent = btn.getAttribute('data-pattern-title') || btn.textContent.trim(); if (patternPayload) patternPayload.textContent = btn.getAttribute('data-pattern-payload') || 'Context packet'; if (hint) setToolHint(hint); } + setPatternState(document.querySelector('.pattern-option.active')); function resizeAiTrail() { if (!aiCursorCanvas || !selectionLayer || !aiCursorCtx) return; var rect = selectionLayer.getBoundingClientRect(); @@ -2531,9 +2065,9 @@

Your AI. Your UI. Finally connected.

if (installCopy) { installCopy.addEventListener('click', function() { var el = document.getElementById('copy-icon'); - function flash(label) { if (!el) return; el.textContent = label; setTimeout(function() { el.textContent = '⎘ copy'; }, 1800); } + function flash(label) { if (!el) return; el.textContent = label; setTimeout(function() { el.textContent = 'Copy'; }, 1800); } if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText('npm install @askable-ui/react').then(function() { flash('✓ copied'); }).catch(function() { flash('copy manually'); }); + navigator.clipboard.writeText('npm install @askable-ui/react').then(function() { flash('Copied'); }).catch(function() { flash('copy manually'); }); } else { flash('copy manually'); } }); } @@ -2542,70 +2076,27 @@

Your AI. Your UI. Finally connected.

- - From cd268707c18ff9fae6908d772cce2f8e07058eb5 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:15:00 -0400 Subject: [PATCH 2/6] test(site): verify semantic capture labels and retain preview screenshots --- site/tests/website.spec.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts index 43f9ab1f..368eeada 100644 --- a/site/tests/website.spec.ts +++ b/site/tests/website.spec.ts @@ -14,9 +14,11 @@ for (const width of [320, 390, 768, 1280, 1440]) { await expect(page.locator('.developer-panel')).not.toHaveAttribute('open'); await expect(page.locator('video')).not.toHaveAttribute('autoplay'); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); + await page.screenshot({ path: test.info().outputPath(`homepage-${width}.png`) }); await page.getByText('Developer view', { exact: true }).click(); await expect(page.getByRole('button', { name: 'JSON', exact: true })).toBeVisible(); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); + await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: test.info().outputPath(`website-${width}.png`), fullPage: true }); }); } @@ -86,7 +88,8 @@ for (const shape of ['region', 'square', 'circle', 'lasso']) { await page.mouse.move(x + box.width - 16, y + box.height - 16, { steps: 10 }); } await page.mouse.up(); - await expect(page.locator('#selected-region-preview')).toContainText('Monthly Revenue'); + await expect(page.locator('#selected-region-preview')).toContainText('Monthly Recurring Revenue'); + await expect(page.locator('#context-chip')).toContainText('$128,400'); await expect(page.locator('#context-chip')).not.toContainText('Ask AI'); await expect(tool).toHaveAttribute('aria-pressed', 'true'); await page.getByRole('button', { name: 'Cancel', exact: true }).click(); @@ -106,11 +109,28 @@ test('text mode ignores plain clicks and retains captured text', async ({ page } selection?.addRange(range); element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); }); - await expect(page.locator('#selected-text-preview')).toHaveText('Monthly Revenue'); + await expect(page.locator('#selected-text-preview')).toHaveText(/^Monthly Revenue$/i); await page.getByLabel('Question about selected context').focus(); await expect(page.locator('.text-capture-mark').first()).toBeVisible(); }); +test('homepage and selected context screenshots use the website font', async ({ page }) => { + await page.unroute('https://fonts.googleapis.com/**'); + await page.setViewportSize({ width: 1440, height: 1100 }); + await page.reload(); + await page.evaluate(() => document.fonts.ready); + await expect.poll(() => page.evaluate(() => document.fonts.check('400 16px Inter'))).toBe(true); + await page.screenshot({ path: test.info().outputPath('homepage-desktop.png') }); + await page.locator('.kpi-card').first().click(); + await page.getByText('Review context', { exact: true }).click(); + await page.evaluate(() => window.scrollTo(0, 0)); + await page.screenshot({ path: test.info().outputPath('homepage-context.png') }); + await page.setViewportSize({ width: 390, height: 844 }); + await page.reload(); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: test.info().outputPath('homepage-mobile.png') }); +}); + test('current capabilities and release boundaries are explicit', async ({ page }) => { await expect(page.getByRole('link', { name: 'Explore Bridge' })).toHaveAttribute('href', /guide\/bridge$/); await expect(page.locator('.release-note')).toContainText('not yet released to npm'); From 19fcffa0f6ba4fcd5ebe6f274aa16fb094a1a5c7 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:44:23 -0400 Subject: [PATCH 3/6] feat(site): redesign the homepage as a focused product workspace --- site/tests/server.mjs | 5 +- site/tests/website.spec.ts | 24 +- site/www/assets/INTER-LICENSE.txt | 93 +++ site/www/assets/LUCIDE-LICENSE.txt | 43 + site/www/assets/README.md | 6 + site/www/assets/icons.svg | 1 + site/www/assets/inter-latin-variable.woff2 | Bin 0 -> 48256 bytes site/www/index.html | 922 ++++----------------- site/www/site.css | 432 ++++++++++ 9 files changed, 754 insertions(+), 772 deletions(-) create mode 100644 site/www/assets/INTER-LICENSE.txt create mode 100644 site/www/assets/LUCIDE-LICENSE.txt create mode 100644 site/www/assets/README.md create mode 100644 site/www/assets/icons.svg create mode 100644 site/www/assets/inter-latin-variable.woff2 create mode 100644 site/www/site.css diff --git a/site/tests/server.mjs b/site/tests/server.mjs index a6cdf50d..2ad27ebb 100644 --- a/site/tests/server.mjs +++ b/site/tests/server.mjs @@ -1,9 +1,12 @@ import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; -// Serve only the two assets used by the website tests, never the checkout. +// Serve only website assets used by the tests, never the checkout. const assets = new Map([ ['/', ['index.html', 'text/html; charset=utf-8']], + ['/site.css', ['site.css', 'text/css; charset=utf-8']], + ['/assets/icons.svg', ['assets/icons.svg', 'image/svg+xml']], + ['/assets/inter-latin-variable.woff2', ['assets/inter-latin-variable.woff2', 'font/woff2']], ['/askable-ui-code.mp4', ['askable-ui-code.mp4', 'video/mp4']], ]); diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts index 368eeada..28df773f 100644 --- a/site/tests/website.spec.ts +++ b/site/tests/website.spec.ts @@ -1,8 +1,8 @@ import { test, expect } from '@playwright/test'; test.beforeEach(async ({ page }) => { - await page.route('https://fonts.googleapis.com/**', route => route.abort()); await page.goto('/'); + await page.evaluate(() => document.fonts.ready); }); for (const width of [320, 390, 768, 1280, 1440]) { @@ -115,7 +115,6 @@ test('text mode ignores plain clicks and retains captured text', async ({ page } }); test('homepage and selected context screenshots use the website font', async ({ page }) => { - await page.unroute('https://fonts.googleapis.com/**'); await page.setViewportSize({ width: 1440, height: 1100 }); await page.reload(); await page.evaluate(() => document.fonts.ready); @@ -131,6 +130,27 @@ test('homepage and selected context screenshots use the website font', async ({ await page.screenshot({ path: test.info().outputPath('homepage-mobile.png') }); }); +test('integration tabs support mouse and keyboard navigation', async ({ page }) => { + const tabs = page.getByRole('tablist', { name: 'Integration steps' }); + await tabs.getByRole('tab', { name: 'Observe', exact: true }).click(); + await expect(page.getByRole('tabpanel')).toContainText('createAskableContext'); + await page.getByRole('tab', { name: 'Observe', exact: true }).press('ArrowRight'); + await expect(page.getByRole('tab', { name: 'Send', exact: true })).toBeFocused(); + await expect(page.getByRole('tabpanel')).toContainText('toAgentRequest'); + await page.getByRole('tab', { name: 'Send', exact: true }).press('Home'); + await expect(page.getByRole('tab', { name: 'Annotate', exact: true })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByRole('tabpanel')).toContainText('data-askable'); +}); + +test('font and toolbar icons load from local assets', async ({ page }) => { + await expect.poll(() => page.evaluate(() => document.fonts.check('400 16px Inter'))).toBe(true); + await expect(page.locator('.pattern-option .icon')).toHaveCount(8); + const asset = await page.request.get('/assets/icons.svg'); + expect(asset.ok()).toBe(true); + expect(await asset.text()).toContain('id="lasso"'); + await expect(page.locator('link[href*="fonts.googleapis.com"]')).toHaveCount(0); +}); + test('current capabilities and release boundaries are explicit', async ({ page }) => { await expect(page.getByRole('link', { name: 'Explore Bridge' })).toHaveAttribute('href', /guide\/bridge$/); await expect(page.locator('.release-note')).toContainText('not yet released to npm'); diff --git a/site/www/assets/INTER-LICENSE.txt b/site/www/assets/INTER-LICENSE.txt new file mode 100644 index 00000000..40589daa --- /dev/null +++ b/site/www/assets/INTER-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/www/assets/LUCIDE-LICENSE.txt b/site/www/assets/LUCIDE-LICENSE.txt new file mode 100644 index 00000000..718bb3f0 --- /dev/null +++ b/site/www/assets/LUCIDE-LICENSE.txt @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/www/assets/README.md b/site/www/assets/README.md new file mode 100644 index 00000000..120daff9 --- /dev/null +++ b/site/www/assets/README.md @@ -0,0 +1,6 @@ +# Website Assets + +- `inter-latin-variable.woff2`: Inter Latin variable font, distributed by `@fontsource-variable/inter` 5.3.0. Unmodified, licensed under SIL Open Font License 1.1. See `INTER-LICENSE.txt`. +- `icons.svg`: a subset of Lucide 1.48.0, generated from `lucide-static` icon nodes. Licensed under ISC with upstream MIT notices. See `LUCIDE-LICENSE.txt`. + +These are local website dependencies, not promotional assets. Keeping them local avoids third-party font and icon requests on page load. diff --git a/site/www/assets/icons.svg b/site/www/assets/icons.svg new file mode 100644 index 00000000..27010ea3 --- /dev/null +++ b/site/www/assets/icons.svg @@ -0,0 +1 @@ + diff --git a/site/www/assets/inter-latin-variable.woff2 b/site/www/assets/inter-latin-variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d15208de03cd1ad7c5199f0a0ce915fe841e4722 GIT binary patch literal 48256 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∾Na#*TBTU(!jF?9zmR-!C*+RdvGrd0&{`| z!nipr`K70{m0;mZfs++H&xu<;V;(1SE|+r!TRFhJOmm?gm8-v%s6s7Dh*U=6WlK&) zC>hc(wUUwchV19Gxwd(0Le0hY-uAsswcmCJy3hWX{bWBq)Wa7MBv?Wsi2sRLiZDai zARG`L2q+>PfkV&{v54&U=V}XL9C01-f7ubpIAjTO8u=7ug~Fk9sH^B8v-K?tH+E$tA{R|y7rfMm>)3O>5hn|XxGC5boX2=_Pot%TcGfy9LC3I_y~ z6ETjn6|x|L=TPnCWcZZJ9w9&w8g3s%v=2fS00<@u6cWy^Py{RrY?t5w!Jjm;{u0CO z6R%@5R>eQqFP3f8xPWJG`dtlPnnvc`7$0s2{&Vf>blg)Vp4wlw{Tt9EuaE_v@R0~k zANvIoOQL%88Y}la@W2bbGVuK-)=GTz%F5$oL1$nN+)6vc;bHG;{pA?4!)>uR^owM} z|98H-NSX}X&`959c2sEz=NJ7s0461Kr5c%rzYt6JF?6aopPF+&a{PxbPZV{?Y=^SG z9dJBntAABsR@l$4-454=YibT_2oQ1hErC2EO_IE*0CDf_wEk6rdR7gW_otm;Ns7!R zyK1XeWN@8VR*LKteIGU{r>@Yrw6De=4*kcQxBz|>XA3!u{c?Dhy+kv;I~GzPvgOCh z1#oCbKrm)zxMtuw7u}XC5Fy)i$*RDRv^mYBSVc&tAIu8c$0HU zdnPxrA&RngA6s*U-?`_k*u~9^*52^PMFyB*EvU72jV{f;)el$~rUXsyKh|ECWnU4< z^LC%vQ?gpf`!prC`kM|l0g34KLZsVFLp}7@K-a)o{QvN#U+lXdhqgYZ33Pu?RGw++ z6z&IpG*(IN3xN~OPlE+kKDH?SiN_S*!l>eJc!6R$p1=BKR<(#HPb_$Ifwc+_xOQ4o zOax7V1S!`c0(DkZ4(%Q|J@88lYtzl2?c9ass3^Ywhzo8u%gm{E4vXOy)7ewRwXv6E zIWoyZ*6(NHnxSFahjLh!j>NXoM(4&MkA<}XzS|%~82|kc-54u_ls!xC(QpP_J!7q& z-&}6@*U?P|eD;bHEP(G)nv1GciUVe8g{iZ~M9YCOYTUa%kAmlT-fkS;|qOb}^wqOIFnEBb~&*M3fbZE~b?e8n!TSNsx_il^~8#Xs=4B8w{( zS72S^Rjbm(EnQ7NZvzKJ427^$9JEy$X^X5x|7RD#CGqSr%IBljMX)lHO6Y8{jqSXw zBRY_nvuR;s!JHr>Hf6gJyHhxaa^&Z8jT9=A{?Ry@f?P_xKm;nCA;)aq5XBuR6T>v9 zt$V`nGVa^+!Rves5om~1ac=p)L$5U|AqsU^-D}e+sN!#rau)||N3r@}3gQr^imm=) zlh97Q&OZMB8!3U{w?q;xgfFJ*%E*cV^htt1l8O2!7rG+y8|}A=td4AqgP#?ChLMKo zD0Eop0O@AfYC(q}ngj@5j)OqP0X}ayJ@o!Wxdl+ws?xBv-KICSj51r;4X_I1UYI-+kute1`I!B2{wVdPt+Egjky9)Uxur5=7Jq#pSuxTQJ|riiE=$D0RmQyRzX>KUEdA zM18QdlJ{u0WwHp*_GnBAh-_29lfUM_dGPx)Prq`}%_$uOcoM_twG>3yppV}iL}R2D)F zFv(AGTZx9x&DaQajukV)cOSC)+#VZauI!3H-2bUlKAkM zrten5b+Fhp_U1&|5LhU3kcNObaa-{$;z-_W{YXANYZsUhXQKv;{&G0u1rP}#d6v3N z_^*L^bIxDx#$kj|eKsZuTMbLG6)i`mJ8W0K!8gPVjXB_MYsry4t>`IaOxj3}o_KI%r(_Vp=D7ik{ zf+Q7P_%PVE-;-dM0g2shO+W8~Tj4<)+lf5wa6P9oB`%a`YZC$Em>k_;BU#4G!&<{S z2X@PSSqlr%KJc1uF{XJNJGue1x(mi6f*{t5Vyas^NWR?@D^5tQN@S#%%Glm}or9Q^ zF$k{PHM6v%iHQ~AG~0_`Sa0tK`Pmllx}EnU2}G#hCE}W?Jr$;8Wij_T@69n^X_N>t z_Sm5F5q%O?Wow<)y>bvr!69IBM>o>OlAKrwcK%M_3 zpo-s~xB!ln6$J6!roCT=5UQYn`a(@c1+@{*ezs_vhJIq7Isw`x>e zyKBK?eby?N3i<)u$p6IyKs3=48|$}HKn*WDtJXY%=r*7)LeSFEZOqi#S^?D%>x1_| z75<{vVpRKCm+xUHl7&`d6bsekyp4uKXYuJ!55dJB@!98RbBeXqo3KWgwyuu)5F z9$2x}Tg!_!ixu&E9G1SZcHws8u|!R*Ri{}?&L-))vUkq&cl^G^x^RpF>LLWTFbTP* zTx}*-;9j3~{hig(*p&1g+Zq~oNLk+l`u2|(|4=EzpEPo2(4NU0f3B3K^V0EWt9W*M zYBW~5i=@Z1@pZ>-3^G6yC~3{@5`nQ5B<&J{@`z*$_@i{bEIyw?V|buP4^q2hv`zfi zk7=`6pfi~Jv4pDfjeYlbnHAV2xSP*N_Tc%OvOyU+Sdwxm5lvc6^C_u{9Zm4?`0XlM;J&ifbu zm&s3iXc`z$d=dp@YyK_8Qb{1bm_|`?kJH}ps9J4fpk-!B${LJM42OUmmr{$k?GWm3 zpkNa`WX5_bI2HeHv!`vK+Oo}fG}$>WelDrQ=?0i!;{}B!Mb@7R1f2HX==3O~a?xzg@35~^pTQg429Qm%jk>G|W0q|9!S^ot@EN?J!Zjy$A;)O}Iy^TLEs+!q6E24_PtEzQc1Wql%&C&b zQ(afeI-WnK>elgw>&>m;_HvjF{~s!2&%T4XfN)*K${w-jdg*0j?CEPsWI6iVCm})M z69DZcH%!BWth2Bi@mO4Lwy9i{V@FB&u$|6FmznzZN6ZrxDoOCR-lDv`=@%*eV5G%j zO!QJDUE6|dT2jGv;o|VB2V*5#VjUX@Xim$?*ppN+7zY1$4a&?{N`D(!lB#Ih z2ID54hzDFqq5Pnap$_hH}9goO(b8V-saSg27||S54H7^J+M<4Cm;Ha`Is0d?IKTp3yHlOy}ULZedp~ zNcGl+bOCbbx(gRY!0Pd4INr$6i;GkqAkNAMGb_$%@ z353$29tNB5BsRw0%tu`Phwx=_MM?kA(R31)@HYrRJ-VI(_8u9fjXeb|mxeCP*w%XU z?4sq!as|^)TuHRsLtg7!(UJ5Rd|PW#ZrfT>?JPw^=gb5plYxqC2rrMQt9InH=AGJU z8;1@Gbd%xC8HH=Qk~Km*|J7g{(`8)5z#?8av)mRYRwe}hw#PhDk@mFflFq>@F!UX*LjLobnk-nGU4hUN`ns;E zlU)-zY@~a|w_QFs3)&|&l@6E1a{R$3OfI`pzisOGgA8x3`<64XNwW{Y+LoeNE@mVd zWK^v%X$&P5TWor{Q+Llhm%zWa`WfUhMqQbm}b3g~d7Nuw1V+EexvRlqb~| z>z6F@?+OBTL1|9S(VNi}C5fNJ`$TYEe2vIfWt|8wJpkpYTH3_hOPd9G+k7k`eMPch zZfs%Ldz9Oe#tV6|@Ap=)pm^C7Tt%6Oi^H%R` zgd0RdF_c9nzKrGf{5bT5X%~e`jLV0VqADr`3U&<u{bOcD;@0cD5#KT|BLTf%w(! z%Auh_iVeC_O>G-&Jf*ftt;*@KT2D&kspZ_}fYDgdKO; zYlkwI>FNnDMa#6R6()?Kq|B~9(11;I-mz=YtKI1JO~YtjJ*SN{pBDV~L`FbWYBoP; z1B75kiarz2HLgsiQD+YNyzS61wm^fQ1dk-X!N5lXWRD}ojnvk+#I46@FQ0t`rm41v zkp#mlD3s{)KxJBdvvwfiNhSDFFqBvWssCPJ4Tw!C=vTR~Yd;MM4q4*C0pZNwDtKnN zpPtAN*x=*9j$54sABDa-Nm#D+pc~TA(d_H_sAx`H_Y)Zhx1{>{icw4{?#75>79UkS zj*AsHV|fq)VosdxoZcBYSd(6Ee=-a{8jnx13s&!F_${ztpElwD(Bt-YCSf=z?)8Ix z;;hfQ#0%^x)7~Hny8858lkRFIyMr^jX$kG~xcgyn zcJH0IrZ7QVZ9t*12M?_Ag-4;h)!J;nRI-zys1Oa3g-3^!i&(Fw&FzfF(R0 zzUpyXL})FjXhLSUYQ;LmOwnU){f_n%I)HibK-_)j-~$)K1nT0+emH=5+%CkW3wlJ} zLSokpx>cr!{j)R!d@C_)FrCVrd^E*~#aI$}d7lUExWf#$Pu;uwZau4Rsjm&{FntMP zc9pbDXl@LvIu3pbjU=?vPIIA`pRXauN|w=3@PR&0Ib>_7h6ct3%tRtBV&*OzUC5Xp ze=Rsxij#~y2Ux#T#GBF;HAQex{>|0;?90mZct3nv&<&DtrEa*mzZ-Rfv;tu*_{gK{ zEs4Qu{`|Nq##AVTRR&h?uvN?4U~>Y8t&n2*iZJ%@2P=>uKiKehsDwrBMq4-I*Q|zU zK@u5##kv~?N4)I#6c`d;Dl%DGMs3m(6YKEU|IRVFXGx7o0!#c$7t6`nOm2K@)|sM) zphFz{%&9j8+H8dDE*+T1z&O%XUh&8i${>*sNjY0e&4EJOq5lO zZy0nwkyBt#D#H)r8ZqeWfnXeT9~3TEz%IYNyckibxCl{-p`Ba)r$NgG7=j>uV&s<@pk6eHsRi%OQuxiu3 zhBMliOG{;~Vf!>kBdy8HlyjLtfRYfS=H^^Ry~)U(55x9zuvLkpU{1u5CH+=+fTvN( zwL18GFnOK6VlD%P_dwB%hfiPc6oN<}nj-bW$Wd$FZ3|mEKxl_)W+5>XUNiA~k*gbxBYJjKCx{omVe0kNzZVtcQa9TF&uZm>Nh?O8 zI;jY*$k#;<RxxvR!#M`BUr{MzUo~Z zT{lUrRkI*z1PZ(Iwg|^8;ImFBVMx~7$*y7?vTE0RP^sQJXN2H~h|lzc7~9fH^rxLnrjoxi=e)7g`G*K&qeGSBw57V;zx|E!nf)#@#x+ zQx>Q*K<{+so#aOI_grRmQAPjm3XY!1sc)O{Zs-;#Xp6QNHBb;|C!R=5g|T|FSto}V zlWCqz(&EE^A1>U4XkO>7^M79G)?(^}_I-g!x>2PJULc;VbmF7}1?%z8i@DU1sT_tP z4bqt4QWHL(9)x?|c=(%gzcyeRox1WrM6A2MP1Ow_I~152L2G(~K4r?8DVf(@>jdDgb0IG7ai|`$11P!RH@g^pn7xFq9y)BQ` zl(E^){%U?$Bby}!1F~~<972@5?bA|dV<1ezXUOl=fOF7cHkmrz*{F4gW@#8wQ#dj+ zTjw!XC=E7_gh)ENJ2&*!1hgmV=zlmE^a%P=w%Dpz)fe%iZs*Tv8L7Y zrWyafukI?c=z1|M`AuUm>6lu?Y4LKLwuZ}(ol+^Nq10zMtW<` zcggh*UzOe!O%qTgI4prgI@c$&*+LIm{Ykhx`8e*0!CSuteeNG(Qs|!dJsx-AK`u4+ z_1Z*lzZh$PE)3dnbBgy$^8eZ4=@lamMgLXunynLry}rJ70(TmWa6eqfWe%%h-s-cS zIfJl>2>ju!5Z$9OHoBBy4@>XjCkG?KI z1HXZQLNw<~0bnvAFI^E62Ys&$IbnC3&{TTgG&$zPgs;TaUX*183gysCVVI#G!zJV708N?(n?R0+fjLDZz6 zP3fSKvqEZoi)$_i1QRECxS>Lho37n0c*Pi={uwXqPA6y+gFifZw>5y5-XszUvrwRg zNC+aKu=j7$O0~{A?o3Dk1Gl{q`6p3DZlmBHN=6btcXPjk$Y>SyZ#Mn2Ex3Y|G55(Y z>NZ_@$TZ=KQGJ@`7qU2fg2$t}Rs_n5oV)Wv+vx8Tj4kvjk-l8^-CU7Z(Q^zE_jBlg z@zY{MvOFFz-@2U~(|yfgmzG-}>s)NWX34pc!ooaG95JMUKZ?eDyW(4`ku@5I8(~u^ zDcMuEA?|Wo>oWR_ie_X9U|(*`sN{_!k+3>b$wXo3>belxtFcV`nuE>;Xs5RMueFOFze2c$6Bd(=>`l~A8+cS-*hX<&=`et@73g~( zck154O4cJiH~Pc&!sM>Yp@2?F33Xj1b?Ca@@x{8@^sa>jC`n-hJo%b&Nh-hAMijo< z_i?9*BT2_aFyLw0l0VtSG zBA0J=oFJg_vcqKB?EbEHj-os}phFD4d^NZ%{(>dp8L=-SMsk4{fNP-|9zjwC!8W@yvJGKLJzLz-nGDS6= zwV68&9b^u($G*w6Nmy-$W|zKhuzqaG{#0ej}Xa z&Q$iP)6{+ID~-SXwXQ(}zhRoFyBhRdDvA`2`z|$Pp$=PIf#L5Cm`-`oFy67s=Sk;X zvaZ@;Ycgo%4`8&nZ?xH%8T<+6-!kN^I>&8G+B^;%5X?_;)!@P#u)M7phdzDq5DwMN z8(2hUU_-@~Y%6;#FIB_=d1>HdYbiibSMYWvaY{TyHa}JsVQ(0Tb?Eb=1;lU>lCvEI z9kK-)WdAvHC#!%6;K9G#7s2YLhBNR?fs=<~vOob5f{YMKs6R=+&6sWPvDyAjq235} z>z+;iVqAVF1Dg@Ne^yNLU@W1`2np!Eaedzth){^n-MCTlpaD%LnL7ukGoKp=LKg6O z5zuCVfDx|^O*Q(VYhaW!3@nyF+3iv?-}egBU=gjeSY*CxY|7+r+;v8Q4*G6054g=Mn zW`b<>cvqaQEUGnT7|2))ntG#gk2<;0Xjea7uVYeJbQDgIURdJs-h?V=FhodAe6`Y2 z6LbZxwEs2fKP>fd}gmst3ExDm-;EV(Rsf&j=Qr~2T~xZhlORnPNwA_3-3C# zk*Qm4{J`x~DhzbUp{ammj{~1JCj^nXI3Nn)8p&zPoh*gNl*--E_?^aue zyI65#tx*A`vg=)zBd*^|&8%aZzDNL%y;CSYj@lPjt7$bP0=!%tz0lOMwj3XdbL9E8 z#z2cgX+a;?Np+KkfgYkn2ofx`Co}gm4q?0f)OwReEwsMjUU-(mI}Bm4$g_}fFE&&w zP-PO!&$5#nRXUYQh7cTAnUzKQR2hAU0-b0&=N!zN7hF!|hX3$f;(vf&WKX9C^E=+Y>i9^nSORRG{Xp}hY z3%aGau80{*_*3%tYq^4D2Yb?h72v4Sx0^O(L+fToMuxx~-}MAasT@?m%uZyUWf*0D zPL_dDG1it=Yk&lQa66wA5BYvlJ2aT!_7ou7(5)iK zocHTDhyYFdYvV-20AI0J6U*>CJX}tM#`uQk7qKwO`^U=W);lNz2gPT=MRu6TW(1Qo zFm$%^%mzV4IdrA$B!Y+j_KpfQQw$r^q!1XDwCb2%F0`;tHQy#jOzBj!;#?2su^f-* z#qQXl_|pHxsKQ2;#>OTK5!+-4tvD}|Q71MtoZ25>$-pGdX!^WbX1sF={uLpEPzGW50 z099fr!vxd~cfdmqncblV`{HF;buXP86S9gYsjRBCzu>w?KOqyeLc3efWrqAwxk6Gf zcD;UfXOB$gXn&%2LNNL7_m4N3+OVmew~|g|L7SRi0BC?o67i}Tu>Br-W8|;swyZyL zWW{cpRqQx@&JHQSFur%EjJu-KnLKXIN-WdE2yTmQmcT&Y@=7aj>n&T%fNRaZ@gDEY z?&R#@`xXlh$<0<*#LDRe4S!Km_GQHdd?Ipn!lPLt4?|3eDcBb&Wb@}lbaRuu>z%Pd z|J0sIbR{CT8dN}&X`I{{j2GdjCOOjz(ZSS6UpcW#7IDj5Pj?*rIn=JGA`iDpa8IHm zb9NSX$}OIOl3qM6$QFhPOeNM-2?T~D^kMmN^CURA zB@h}Js*7ON%>-y|0SnxzNwnIpB5L(2lVzq}4#vcw$7ST1wD=6t^>y}55a6OBJQ(1{3nX*yzmnGJJNV@hzwiF*$*%+(6Lg(bdX)9DU;Odr_U z{hC=VXaCG!CWf&V6$fMo4w%N*v$*<1u5W7qbStx{=ZrMJ=gYCvJhU&5O`$6AlRUCx zoe^(YnSag^IrwA1@1LW{F)rw{{59CuqksDNv2T)cUZ#Am>!;8lK z_1+wc)05#qVQQ%Di%!!d*ClabHanNgWF(N@jn@4%n#tV6e49Uywde;%1#8xPyMrrE zRm|~B9k*O4a zHMHj*wkL^??clvTjQ?{nb%kPUAQ)eI$w7nn2_8RwtUXx3w_7+MZp^8y=?4BS+)QhL zH8>4PrN8ajdSm9mQ$hWJwxQfka5JqZ0$bBnc!O)3CFMz_VP2(loLB!=IFVfGll!{_ z$<&H_x{PW#1A@41aMilSMXoT+BUU9`8(e#`KJPv>qS4y+MkRECJ30OjTUud}jXzom zJ?I~J0zNdAXB+DKAtyo%-DB3veO}G`Lb^x~fe*e=FLeKRr^OQL2;H!22&+Lfd|5sh zff1>husg0#?3qi!G%SJp3ZIwF4P8t1>4r-ktf9Jsqrkq!>akHNG)m%cR?DqgT*ano zFgkr^YSs!Ch9)bRN~NhRqs9+1*cMOQf?TR^q;;dVxePq5+~+J^)nr!7WdiDVj?aw< z5s0$J7prym4+Pyt8X`BS>S2p!PJ>azJ5V{jk9PQs50ELa+PrL)2(?5cob{<;XtJzG zGI7vNYi*0&gDU489npu=JyLreR)m}m?Lw?>a<_9@hlL(aMW<3ptgjQ0%z5wn>{K=F z?L~QEUvBs=B=ztEC2t01CjRl>1<)_+p!_LBL4Qqu)XExu%^u$Lr~ zh62AhV<;Ea3l`Mta^79phzbZ_NuhK)@QD@uvubnpDrdGI#%>?`l@SJ-?v~VBu?#J9 zNqK1rJkkV*3b9{9&Diku1=hI@&1zVccV3L?iC)d4Vcy9Xw)D}4!+@<|?i+Ic ze`YGK4i|bwbU>cYC0Wv5BskNOXJ(j!lvj9Wa#o}^rG-TkOS*3V%!LHOSPM7t-toXf z1|@tD4%oeyy2_S0)G{enyXu<2PV4t;HB4>#NTXGZsY9Bh;~9}dc5EXu!)j>L1EO-FMKs^xne zl`AnMKrzD0{;``0`Ru735J%%$3#uli^$cV? zxql&UoH(8odyVJW_X-n_42>#)+nz+}IO^<&#rJh^TJe{S8s4|~=w+Drx9_(XWLkfY zVY-(XTK8F&SEE3IKeguayz!DGH0VSvD6Mq4MQxnVx1b{^dRw86(LM6Al$=+M?JX}x z8(pDx?n3G{lt}2Z_h1QZDe%5G*seE-ztU{ZTBkg^-MMJWzuuc3{qIF5U08j(`d&Ki z{9E^+#Wx5^)phwrm)tTE3k1RkSF@m*!KgZY*RX#s2zM@5qKev_uib6+$fi9dEUpwR_6LJ67{m@1!B3{tGZ8-Uly`^d4q<4Z=9Cfc`dYJGK>YsdZCg^9emp1^F@o-oWWm1^REAt1)>1-V@u9@ttiAxX8iOP3n zC%%b9PhAh2x#WuFqLR!pDaUXdlR}Xl(OMar)+7lf`wO)jXNjPWHH zUnm@=kpN2e;mtmq%K(SNpYZ#lS!Z#NMo})W^iioLEVWmI;8yOVe?JAf$HA9$*t{+nxCOWu$Cy_CnM#IVgEP z$yXKD4VP0mY~hDctM;2-_Ug;4re#H5>MOn!SP*Xo0yD)}!e_L7&C_ThuM6Bv>q>j? ze##@z6h_k(7Xk=*R9i9f(r^{51HWfr1Y;FhB!8E!d$PduBfEV(#{iTf;k@1UTPC~w zoz~mZvyS+G%stuMaP+T*u}2$oK7!b$^yA&bpT>W#)}K=-tbyeed&q~8YywEk=kYjA zTHWM)ubnZ*ii?edo&~8uxj42%THA91`&zbT*JZJL$!22U4l!OZl@b>Wz2*Z(R+_Q{Wi6rzP&UxlC+m~G z>08M;gf%Jx5#UY)Z`ukd@J)%(lqu^cL*Tu&%v_86d8BkY1NYtUc{J=FfO=mk^>4pZ z%=36ztHFK%`KjVuVqNkQQxplvFg_NjmD49gv%GEr)t%6*5AKthTWsq{ZQoJk!aRpg z&3F!E?B7=8sX3RqHBRlf)<0n$i{Rns538Q$AT=(6VLO+DNEJC8yVpVvxmE8L55TSM zzavDq;>~#=2u4>i+)<^_jmLY4_jW7C&+3+5nUgp`IQH@n9BEYw0SLw$A3l=}051Cc z1d-p!ED&47c2R(YAm3v{sCT&^-~ia$W$@A|TA(d@SBQuN_@35Gb-5rTHFgMr!2MV* z_eL1p&KY+5veNr9=B@)A_-x;F5B$p?7yFPRS}b{@@H&1QR5S!V>8aSV%@RS!M^l3^ z>-=hH@VOq}MiuDqL6t~Zbi7$F;AU}r4Vh9F45UF5{$rPxCsQ(QrL0&mEmCG zlOc=wlEBPt=x!}=`Ln{znR%I6o})@M-H~~GP0b=&p&bR0H3x!tBRysV?FmYbi#FV# zxoAR0Yc?R#hw!+Fge^alQ3^kW&1=b%!hno^Q{k zC?FKU{(sD*T{WhnivMoJAJ6(g6)_+xIlg2YhY1oxM+3rkiDy+y4khH1p6%;w!R=y6 z-hxzQ{m1{2B`ul!lS;e)vh64u1|`r7yGI6Els|U*xqAIWki;_u4)+|~O z=<&YP`5w4kkmNsU$()71{w+n8;OUmL;F+7l4u^+t`{(d;11kUhzW)x9tJPn;umcnp zM-lcgnw{y&BS#khlz+PR$QQug@z?3Ckp$#@;J(*@o??5OY|}@pKmMci*S*{BxUCNS z?uoCH=R9=p5B>&7k@$YW?xvDIUcc+_RGjMi5rRI`msEcmPv=X12zuqY90!oAyPzx6 zEW!70rMGO+_niB|6QJ_Gn7()YiyimAsYeK-iZn?Gk2EHHI;&H(zh8hJE4=iE_DXxG zv)$eq|MR~ql50J+eG4rIK!1JwPV^{zAySjEWOY_<V{|DNav){&#NPf&tN<_PF%GY#cc*Q|CE(jMoxOt3e=%1jNF zKO3HN%(4z8tX8{noLs486z=rW?kJYnC8?7|+P{H#zvAJ{O98j9xYp#$!Ym?j zaV8}PNG=w)pkR&-H*+8BEGN0FFfM1N*`7bl)Zq3L{uq#Am1 z&#ZoG2|bh3C(#1Xw}vR#ZdgUm4p+QaC|7U;fkvSKW%IqM^u!i z5qws)y`u!`nqM*Mse3-Q3;)UhwVImrjL%SAP1Y^5703C#&C$@+PHgV_D9`K1`G?(5(+%~N>>#Mp*#yN zUUx84Q+GwL#Rx*nOR6*}6dP}6_5QW6=464eFZ`qSmU{U2^fxW)ynw>LVQh*?= zy2@rn8ObEuz5@#e%UPJArVv7zt&t!|(x2gyLR>Re7Ik)ldAd#>kUGYsa1{k;j;{6; zF0jS_whpQmsnya9-zLZ^ho@u_d6aueIWGt;&4(s5bE{6vBalQE1z}Qfo;rk`2i>7> z(sRUjRH|5>eNN_Cg02!E0hR!JJxf>N_vhMe@6^d5ic-;Za!LucY)LOrf5eEu+ab*% zS+P39COQ`Sd)e-vjpUG!E{ZYrb$q! z?(^MR96;st04}HTu>>f59GnxJ(pK3Pfa;PPj8#$C~ug<<@!eoiBLJTJPK4w=q)P0mrxAdjCNLFN)lR4?)Tq>MAl#{ z9j-h>w!dvSi!zyPFoild=BkcfG8zLncHte?!8$WME+&w*p`qhFa?e@Pxh=qbAsbQt3gT{fPbTOh zY~psMz0uhgu%&k4mWs60TQZ_Sk@m6)f{7ET)PCZLSfNpFnrU}}SitEmE+L9iQRzxh z8&shx3K)CA99lvFxtLi?m@%&UD z_+ZjQR!FihhKjssd; zURv|Djn>6eIlS_?o0xC>TI1oK8JrzXX~DThc6)D-MKI z`y5w%5vD9XuSWA&q99c)q0*`>y*Gq|`JU-`>yN0~Z=jN95?2guVB;Asza zp`jG9Cl+(H+47`K%wanG=irMCe(aRlU=D`Qy$D+WtN>r3eQZFE_i3fr`yAJEbW<2f z=FNWSK3le=g$R@#6z0oW;}QRAWSKcfYhHz=@Qx@%DaG6G#2}sON|mq6M+EuH7q{Bd zqMt7}Xxu9L!?4x6cQxXSc;R@NUAT10S$Fnoyf1#fjikmpIwK{bu}XigJg&H+H+`b! zp(6di<$1$Wx&JDbMI;JhRx826BdB+vVlt&`B+HG$EbgBzBMYgi|1G57K6yZTYDh2P z%3Sc+2qY4VEXY{(SGUZNwwjYuJ;ji2drW2UhDk3+>k`r22kBfJ-_pHdb@@{*s|ChN zuT{%ZhiNX1O>e9$b#rLlce@T55q?;Ls?$(HS*(K{0~**Ba}UTZod*k?Txj$2}Bp{L*zmmxh(bCN|#D9n|xAg*kac)_Q6c?xsqy=g`prVh{rjyAo-LU zX^f{)!>Q_iLCfZA4Y>yVTA*LH(-}PstFo$9ZvI)nkTMrRYmxu14^nE-!?E%>o+Ovs@QW!O(&_SkaCo(#&oTU8}oasrn@ zGq8$Tm7;dpf_Z&}!3A{hIi<7^5bqU~GGr`>kSjc1jU|;33_Nz-9<}yh87yJ?jzuaW z{A2pmcu6gJ32I0Gl~c0^yz`7Z7mGsh=5zqf>%muLw}E7BnG%s9UK~Q|WhxZ8O|K!7 zLMM?81`)5vd5Gr)A)4U0cy21~FkZQBbDz*7Qm{e#bG>5jQi#oG2iuyxR)|TBw_V6G z7&|Ve6sS}9Mq$_QFDgX}*%cYIPLfZ(#1ah8m*woiba)Dc65Cv<5y=5ou%2_7q_XP_ z+3``8*~}3je%uQDm1o)k!w-~hv)PUT(HR(iH0wM=U+WPbjZ`TI9F#vqgJr3_%ZFKZ zzx!rf2GdPrR3yxVnM}(eBF__i)L1hhb;rS6(6Eun=6~52qHW=5)a#%r3blj!S`}O| zqUe9)N`r|m>rje6gyE}Qd!4uII0;2#sSHW4anqxma9>ft#foKZDRletGu(OAunBoa zjgMGu^oO7{wF<_XEg19!kxW|IDC^4Vl|$^Mh+56=MwK^*TwrqsS~f9@>?S;jqxoG8;4mUN0>s%UqJmDsrzmfs#|k@e>~%sq~kW z5%h?0{4Uo)_`0^NOEw$F?bYOgDOH?)K{&*AYY5guvsrYdLa3ThYZyjP5n{ok#&x2; zrY4b2$!!SV@JNDr=b$ve%N)|w6P{6L{h-hNQ(~4~<|9#_<+%ufcv*{5e_}@>d?Gth zetEi|%_?LJv+z`lmdznn7)Z!!Rk!MtQ_nyYa(NB|M5%aSDVjz|W3gzNcGH1H$LrPg zV2zSgC&?v4=P!2T1(6iKS7;gBu*`~4r<58NhK8c#Sj9ATZKFg3tJRuCwJoQ4M~A7Jg8tN<>W>sAvQn&|R#O$ppN7M(Nfk3O9*Q@Mq3?-LHx9E0 zcnjY=F2!P&Pav9dN~KEfkYITnfjnN_%DIA01KMxGi3axqLo6uWjUxqb$S3YEw2SCc zAb_RzY4m79l8>b^dwwA^&ny@C933u7xO_4eV6`M$tr+cg-z*86Q5N^B9a0j_sA-K> zJ%OZ-=QoF}N`-WQSE$PMp|m&Y2Wh9H?*J+Pecu1K0)F>3LnmqeropRw65=mgPc+eg z5%$5ouzO`iLHgDo57cG(XE%$R)X(3l>YYzb9y$d!WMcpsVJk`QeOe}S;xKD|0|fZ0 z@x>i`6MygSqm41l(efVB(Ot3D6O2)LfcyvW3zHaIjU$NkaR{sppCS9 z04oKLr2TNf9?rbQvHQ!1ckI*d@SUJIY()=@J;}#u#Z~wZ#XOu;oR9sA%W%D7A@(Y+ z#XX8Qao<(CUmX5QaRI)dxbTWzu7V?1=C5>@vGB~p7Y#dl|1q*P)qg?wshMPr@N}r6 zaOnp=`h(bhfYU9aPvq^`vr}N}*PXt#JAr@~m|WYZfk4{}8qo>1B(4IF;e6(Cl!T*H zRG3?RN00)(gLm`VuX+ed{KD30t&nUf(M96l4_Js)y`jM#{OimFtRzTf<5-G|N4OBO zX1NAk3aXHhWgFK<4V;%j{cbgifNDsKD+~R8D{F!|ii84d18emkMe`9>TB5Ln=!QppeM2f*b9m(_?HVBoT7`4!)4(D-PRV=2>Th;9uuLm=2zw*c zV^)0g1%1)h@tPYJ3pj#?BOEUCB-B0By~idK#C2~bi>PY1C!CRZUtpW}fPJ@kya0Z_ z9&k87&d(k@vM={z!|x49?un6IkAe`M;{UTga=oVVA>cs%P~cIEf+2z2 zmkA!*LyxKUxeI^IQd3ZPE_^mj(98_IwEKy=o0%2-mG15OQGZ12Pi^ zFe<>*uY7L1Ll<#EHD9B#1dAi*F$^;A2}35+Ls2Eorej211`9_MKW1bsF<$o_Lop%agnwS9}aQ#o<-w5!;WOu1>{kyB#6F#dh z=-w|XVY?A)hL?~K(+g}!#Cb3hUz1S7sFXJDsZ;R#F+l3M0MO6YSl>}XOGd{hC1EmM zPqslK{>aBKqY`zyBITl=&tnG+mtkI+R{qs+6nx5T){m@i&=q{ji%qT;1zIptybQ=3 zAycB%iF~;65y(vaUVBgM8xjeT0tgdKi|QQzF@(^NVACNiH6jA64IZX|pRX`lh6TfS zx!|ANd=k|D`h)()*az0QCLi18oe$foI!VwsIRPaC**7l9P&lUd#oUuXV6BVpD=Bs7humwD@aR2 z*Q$2GyQ$YqTk)4&(A(#t2p*f(^zIQszFoejXkqz)ztWOpHr5V;59FJ6 z9`xPIVFw@B6^CZzbz%MX5hJ8Dn*Vuui4YOFC&_rcT^FTz3TPFSc~A-k9#oU5JD}rU6g{$ za7MLyO-!G&9AxtOSP;@z6T9M>N(lvs3X@zls5a%od>CY)Myh|FFA$9lTsN}Pm_fSt z?1OiMP?1dxq^gTuI*^EuKJdwV3{u{y87?p{nLjWLu_597*S~y>_AI(QR&j0|)^6Dv z<(tyRdP?fYaKR9Ot?Drp6JB{DagF{R2v-09>8|$^YB#h9AuWOYD+s$}$&@mKb~e#8 zt^ZWhE2w(a&QMF0Vi&axHJf>T?}BRiPvyC4r|gp=t53h7Q09XHJ76IGfGF#uAC<_G zM&qy=)t*0jUSi1o5r|Lj@4G&aZV!rq1_?pf$-8j)-(&hLFA82>5EigQ@iLv@q!{Q) zFggZNv_9j21A`21R2X@iLbHz@99drT@G@`OLrcrb(0VtbLNPTSQx_&6?WEhc{QT~! z|5CD(BadhgpT|C4Ba3KsBT7zpyuE+fG&T#6LZsfJfsYWLo3!$s4fqr;f23ZA1{N#&kV+V&oTcZNAID}y`=C6H*g`BAjZP) zdA6v+D4~~*3O^&zAV9 zoUpdqK*HW{l1oI}Bbl{us}RvJ4e{A zE0rQXbF3K8_(gjz;xFQ!5O6^Wr8rAD{22rPZwy6eV@9Qs@bs#%ng5sQG%!{@M}r?e zUCr?BdpsJq)XhL0ir~)Lbzo^qt)5Uyq)*1Rku-9#F+vm}TgOwS{z2QOUhw22>HRRq zD?~qy&f9lmV4rNNBm?#y$A>mGqM^-R70-JRzD@>XUAf>!fejKI8<8l8HmikPX1V8$ z#6&kr!h*uPMcYaQqIlsBHHRn8c?b~KJz)2LW%xbT0>HqE9BwfdWgF1A+uF2uurp5! zzP&^ADTL?~x3;<#HeMw5^Spf)K;xf7%L+fKe^y2Un*yV}IBdgbt*7jaDgD@6Z`4Mc zU+)k@jN6WVCw^B?muq&sP>vGyZy$zaA?SXmh$}VHFBZ1`>vA`)Je!RIQFmPNF1hQ} zbNGvVwIgEf#86RI|vh8c~)PC~U}~{0j7zLYBzuhgE2~ zz!^gH2$952KyvMos<5hYu=>HpZA~wk%Z7EOG-seC%@U{Y<$nQO zl#FM>hi_jtIk$>5PkTDcqCn7s>=`7CnlYRq_tBh55RZY#bmv?XEJ55V4xxmBki#{3mVD@B*aZA2jN-KI-89 zPca5ud=KJmmehPz1>{bgua|5aa>rFMHA|k7=xd3@b8co)To3ulEiSyC}NuJS@jg} zHErc#oU?GpRWr9f>9??FF$#&#b|`sn$-IoaMmxnzvRUhDU(Mr`^|PEWTMQOnq}Z<} ztdknGyO&AuwWf4fM9g{&I}~di-bGG2kP%Q4XJ*E+H-yPJnVnpe(#}xypT9u27cd+jnt{=twju8qPvF|D@Udu@;#Z-)A z79M{Qj?Q*C%jR=L<`VX-a7gr-yb^CH99;@?JPA>cYV=dZE>=Yk)kAkWl6k`$xD2_l z1*#zrYPEcb=HqA$wnVr!3`C^@lxU`1GHp&rt3@~UPzMj8rt$h3b0V*D0U$**Gzoyq zBfBlNR^7QeExg{>1ux{_C`=+aM6O?SAd!wJM-<=Y)c-_T*qT zfwvRX!|}uHhqYHg6F>;~-txamDsjLAVbPTq5Qg#w9j28Y`wDi(}FR_%QRnz%BeZ**M-%2`Q#)|QA*DrPK zB2VgDayp7$m6$10Ded~zkY7ksrQdRxVIj+S~L72yn*_zZ@8v4>-N=Jf|t=pEmeb36f}iO zV)%XQA3nam_w;Jrey!Pse#s=Ni_d-3odyco=^IH&)XTCBiFYH*HX&I$cH%KrNfz6L z#=W#fVR$nZX32hX<(Syt@s(nMH{HLbP+h#Rt+!A+sj&u-F;=K_N~2r;XhlyNH1t8i z*ievI<(F}Zz}8NyY3b@jy+-@=fE`z}ebTw<2zl&jM|(B9-OlFv)X2-1hBkjAe$oZx zrh;y0$_!V1+t8^vK%$TlLdX zk^72^{wk$ZqUj}b_n(UiprB!3`@L%TW?N^2h$34b0kKFMmGl5|UQz1cZIdBBQSQnKWd`josKlvFhVC|w|JNI^nDuccrYd}-oO zx)3_$zWRhCRj!>;nV=}CsQVp}izH-K{nrLY0 zs&y70OO5}#=|*~n3>f)@W&dR5Z*}S&bKFQq9Ktu9&e><>DdqZ8ppBh_ldCeGX43C| z=`yILIH0&baJ!{7z{}UAh9Lq_K^R;}7||3VcJjRbJ|}s6fw52|ChFCvAJ?B|$@J7S z&%N}jl2j&FL^Y%&K>C{@E2^d&re!;>=ks%U`V`F3AP+$$`VwO$l%CpC|RzkC6M?s1e;;VpC5-Z zc9KwTCQ8Uc*_HjDgMjCp?y4F-|NE*gB_Tq1zGIypfmb~%IX*(fa<|AEv*A!^xkrL; z2VO%6n{JXefDoDQSs^q(Np6tHZ--qzG|gYYbHN91e+)jBd?W@F@Og-xpO1j+2{~|L zQ3^tY@Oa9_LfLtU5Xg+j za)YzLwdD;ucXk!yVAnUE9?CA@I-osM$AAv*MN)KIO0cNDJXPZ{tBq)Ma*M*NLvr|ggzT(k8&Diziw%}5!U%gf=bl;q6_Zs zSX-$O)h;HSDdB8Sqbp@anTQG(B@Z6!Ydum5O*5tvkz%ObZfpK$x;N#{v_fsKp4IAu z<+Qdi1ECN$uQgy>ghT!on{(I9ifOh+7Enu$Nn?_FA!Sa`=%d81yWRLK>}hLlWEH#@ z=h~bGADN%zNSgek=0iy@70JAx9r0SEx}&8SW|6k*F4_BT-+au1pb)VmkFzx&qYvuvf>RH45Rn z{T#+_5L&EKVz?A9Zuu}+k_Afu6vCx=kz>Q;5eBexV3$c4FidQqd!_+2mKSdT@tb^` zpal00K(GDaumnIMOs}y45U^9g003+NfbIck0B->CD{vLm$UQd6j|%Z-jLrS|$+2AV zcoKRRD#n#xY4Ac2Gegc0CecSM=6zow9U$}+WS-^`Wq21j)({W0zIbZCPvGa@T?O4MgT03=&6St#>snIr7UzT-&}k z>v>pU6xG@og7{IenU-t)AK{BTuc|ejZA%x*LUR&K(0^EM7~AFrThcz7;mW#-?oTFLTj(bCRieu&P`s;A7 ov+5Z$hJGB_FISZ`lH5O4PBqq<6^F{d-($z`vHZ{4l`jAQ04qY - askable-ui — UI context for AI assistants + askable-ui: UI context for AI assistants - + - + - - - + + - +
-

askable-ui

-

Give your AI the context behind the question. Selected text, a chart, an account, or your app's own data.

+

UI context for AI.

+

Let people select what matters. Give their assistant the context to understand it.

- Try the demo - Read the docs + Get started
-

Analytics workspace

-
+
+

Analytics workspace

+ Sample data +
@@ -737,36 +75,36 @@

Analytics workspace

Click
- - - - - - - -
@@ -792,7 +130,7 @@

Analytics workspace

-
Top Accounts same data drives UI + AI context
+
Accounts Monthly recurring revenue
@@ -802,16 +140,13 @@

Analytics workspace

@@ -879,22 +214,25 @@

Analytics workspace

-

Keep the context with the question.

-

From a single selection to the data behind your app.

+

More than what meets the eye.

+

A selection is the start. Connect the data behind it and choose where it goes.

+

Beyond the visible rows

-

Connect app-owned sources for paginated tables, documents, carts, or charts. Include the backing data you choose, not just the rendered page.

+

Include full datasets, documents, and app state. You choose the backing data, even when it is not on screen.

Connect your data
+

Bring your own chat

-

Bridge selected context to your chat panel, browser extension, iframe, or server. Choose the destination and handle delivery explicitly.

+

Deliver selected context to your chat, browser extension, iframe, or server through Bridge.

Explore Bridge
+

Review before sending

-

Let users inspect a prepared request, then send that snapshot without replacing it with their latest selection.

+

Send the exact snapshot someone reviewed, even if their selection changes before delivery.

The reviewed-request React API is on main, not yet released to npm.

Review the React guide
@@ -902,38 +240,53 @@

Review before sending

- -

Three simple steps.

-

No giant framework. No invasive serialization. Let the model borrow the user's explicit context.

-
-
-
1
-

Annotate or capture

-

Add data-askable wherever your UI already has meaning, then add tools for regions, circles, lasso selections, and highlighted text.

-
<div data-askable='{"metric":"MRR","value":"$128K"}'>
-  ...
+      
+

A small addition.
A shared understanding.

+

Keep your components, your data, and your model. Add context where your UI already has meaning.

+ Explore the integration +
+
+
+ + + +
+
+

Structured data, attached to the element it describes.

+
<div
+  data-askable='{
+    "metric": "Monthly revenue",
+    "value": 128400,
+    "change": "+12.4%"
+  }'
+>
+  $128,400
 </div>
-
-
-
2
-

Observe intent

-

Call askable.observe(document) once. askable tracks click, hover, focus, explicit selection, and semantic app events.

-
askable.observe(document, {
+        
+ +
@@ -945,63 +298,62 @@

Inject

- -

Pick your stack. Keep the idea.

-

One core library. Thin adapters for every framework. Same mental model everywhere.

+

At home in your stack.

+

One core, with adapters for the way you build.

@@ -1031,8 +383,8 @@

MCP clients

-

Your AI. Your UI. Finally connected.

-

Drop data-askable on your most important element. Working context in under five minutes — or scaffold a full copilot app in one command.

+

Start with one element.

+

Add meaningful context to the interface you already have.

Read the docs Live demo ↗ @@ -1046,7 +398,7 @@

Your AI. Your UI. Finally connected.

- askable-ui — context that feels native to the UI + askable-ui · Open source. MIT licensed.
Docs MCP guide @@ -1165,7 +517,7 @@

Your AI. Your UI. Finally connected.

var p = ['User is focused on:']; if (ms) p.push(ms); if (inclText && f.text) p.push('value "' + f.text + '"'); - return p.join(' — '); + return p.join(': '); }; var kpiData = [ { widget: 'revenue', metric: 'Monthly Recurring Revenue', value: '$128,400', change: '+12.4%', period: 'last 30 days', label: 'Monthly Revenue', changeDir: 'up', changeText: '+12.4% vs last month' }, @@ -1189,7 +541,7 @@

Your AI. Your UI. Finally connected.

var card = document.createElement('div'); card.className = 'kpi-card'; card.tabIndex = 0; card.setAttribute('data-askable', JSON.stringify(meta)); - card.innerHTML = '' + + card.innerHTML = '' + '
' + d.label + '
' + '
' + d.value + '
' + '
' + d.changeText + '
'; @@ -1205,7 +557,7 @@

Your AI. Your UI. Finally connected.

tr.setAttribute('data-askable', JSON.stringify(meta)); tr.innerHTML = '
' + '' + - ''; + ''; tbody.appendChild(tr); }); var askable = new Ctx(); @@ -1789,6 +1141,8 @@

Your AI. Your UI. Finally connected.

}; function scrollBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; } function addMsg(type, text) { + var empty = document.getElementById('chat-empty'); + if (empty) empty.remove(); var wrap = document.createElement('div'); wrap.className = 'msg ' + type; var bubble = document.createElement('div'); bubble.className = 'msg-bubble'; bubble.textContent = text; wrap.appendChild(bubble); @@ -1960,7 +1314,7 @@

Your AI. Your UI. Finally connected.

} codeAttr.classList.remove('empty'); codeCtx.textContent = prompt; codeCtx.classList.remove('empty'); - // Show context bar — let user see what's loaded before sending + // Show context bar: let user see what's loaded before sending contextBar.style.display = 'flex'; contextChip.textContent = prompt; if (meta && meta.capture === 'text-selection') { @@ -2025,8 +1379,8 @@

Your AI. Your UI. Finally connected.

reply = 'I will answer from the selected text you attached. In a real chat request this text is sent as explicit context beside your question, so the model does not need to infer which copy on the page matters.'; } else if (w === 'revenue') { if (lower.indexOf('trend') !== -1 || lower.indexOf('why') !== -1 || lower.indexOf('driv') !== -1) - reply = 'MRR grew 12.4% — likely a mix of new expansion + retention improving. Check if seat upgrades or plan changes account for the delta.'; - else reply = 'MRR is $128,400 — up 12.4%. Momentum looks healthy with expansion and retention both moving in the right direction.'; + reply = 'MRR grew 12.4%: likely a mix of new expansion + retention improving. Check if seat upgrades or plan changes account for the delta.'; + else reply = 'MRR is $128,400: up 12.4%. Momentum looks healthy with expansion and retention both moving in the right direction.'; } else if (w === 'churn') { if (lower.indexOf('driv') !== -1 || lower.indexOf('why') !== -1) reply = 'Churn dropped 0.8 points to 3.2%. Most likely driven by improved onboarding or a cohort maturing past the risky first 90 days.'; @@ -2034,27 +1388,27 @@

Your AI. Your UI. Finally connected.

} else if (w === 'arpu') { if (lower.indexOf('improv') !== -1 || lower.indexOf('how') !== -1) reply = 'To lift ARPU: audit discount depth, push expansion offers to Starter/Business users, and check if new signups are landing on lower tiers than expected.'; - else reply = 'ARPU slipped 2.1% to $42.10. Watch plan mix — topline growth can mask dilution from discounting.'; + else reply = 'ARPU slipped 2.1% to $42.10. Watch plan mix: topline growth can mask dilution from discounting.'; } else if (w === 'nps') { if (lower.indexOf('do') !== -1 || lower.indexOf('action') !== -1 || lower.indexOf('next') !== -1) reply = 'With NPS at 67, focus on converting promoters into case studies or referrals, and identify passives who might become promoters with one targeted win.'; - else reply = 'NPS is 67, well above the 44 benchmark. Customer sentiment is strong — good leverage for expansion.'; + else reply = 'NPS is 67, well above the 44 benchmark. Customer sentiment is strong: good leverage for expansion.'; } else if (w === 'account_row') { var r = responses.account_row(meta); if (lower.indexOf('action') !== -1 || lower.indexOf('next') !== -1 || lower.indexOf('do') !== -1) reply = meta.status === 'at_risk' ? 'Book an EBR this week for ' + meta.company + '. Lead with ROI data and offer a success manager touchpoint.' : meta.status === 'churned' ? 'Run a win-back sequence for ' + meta.company + '. 60-day window is still live. Offer a credit or a new POC.' : - meta.company + ' is growing — queue an upsell conversation for Business plan. Expansion signal is already showing.'; + meta.company + ' is growing: queue an upsell conversation for Business plan. Expansion signal is already showing.'; else reply = r; } else reply = 'I can see this context. Ask about trends, risk, or recommended actions.'; } else if (lower.indexOf('risk') !== -1) { - reply = 'Globex Inc is the top risk — no login for 14 days, $5.2k MRR at stake. Select their row for the full picture.'; + reply = 'Globex Inc is the top risk: no login for 14 days, $5.2k MRR at stake. Select their row for the full picture.'; } else if (lower.indexOf('summar') !== -1 || lower.indexOf('overview') !== -1) { reply = 'MRR is growing, churn is improving, NPS is strong. Globex is the obvious retention priority. Select any card for detail.'; } else if (lower.indexOf('upsell') !== -1) { - reply = 'Umbrella Ltd is the cleanest upsell candidate — healthy status, expansion signal, Starter plan. Select their row for specifics.'; + reply = 'Umbrella Ltd is the cleanest upsell candidate: healthy status, expansion signal, Starter plan. Select their row for specifics.'; } else { - reply = 'Select a KPI, account row, region, lasso, circle, or highlighted text first — I\'ll load its context and give you a grounded answer.'; + reply = 'Select a KPI, account row, region, lasso, circle, or highlighted text first: I\'ll load its context and give you a grounded answer.'; } addMsg('ai', reply); }, 900); @@ -2065,7 +1419,14 @@

Your AI. Your UI. Finally connected.

if (installCopy) { installCopy.addEventListener('click', function() { var el = document.getElementById('copy-icon'); - function flash(label) { if (!el) return; el.textContent = label; setTimeout(function() { el.textContent = 'Copy'; }, 1800); } + var status = document.getElementById('copy-status'); + var copyMarkup = el ? el.innerHTML : ''; + function flash(label) { + if (status) status.textContent = label; + if (!el) return; + el.textContent = label === 'Copied' ? 'Done' : 'Retry'; + setTimeout(function() { el.innerHTML = copyMarkup; }, 1800); + } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText('npm install @askable-ui/react').then(function() { flash('Copied'); }).catch(function() { flash('copy manually'); }); } else { flash('copy manually'); } @@ -2093,6 +1454,29 @@

Your AI. Your UI. Finally connected.

window.matchMedia('(min-width: 721px)').addEventListener('change', function(event) { if (event.matches) menu.open = false; }); + var exampleTabs = Array.from(document.querySelectorAll('[data-example]')); + function selectExample(tab) { + exampleTabs.forEach(function(item) { + var active = item === tab; + item.setAttribute('aria-selected', String(active)); + item.tabIndex = active ? 0 : -1; + document.getElementById(item.getAttribute('aria-controls')).hidden = !active; + }); + } + exampleTabs.forEach(function(tab, index) { + tab.addEventListener('click', function() { selectExample(tab); }); + tab.addEventListener('keydown', function(event) { + var next; + if (event.key === 'ArrowRight') next = (index + 1) % exampleTabs.length; + if (event.key === 'ArrowLeft') next = (index + exampleTabs.length - 1) % exampleTabs.length; + if (event.key === 'Home') next = 0; + if (event.key === 'End') next = exampleTabs.length - 1; + if (next === undefined) return; + event.preventDefault(); + selectExample(exampleTabs[next]); + exampleTabs[next].focus(); + }); + }); var recording = document.querySelector('.recording'); recording.addEventListener('toggle', function() { if (!recording.open) recording.querySelector('video').pause(); diff --git a/site/www/site.css b/site/www/site.css new file mode 100644 index 00000000..f856a0cd --- /dev/null +++ b/site/www/site.css @@ -0,0 +1,432 @@ +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('./assets/inter-latin-variable.woff2') format('woff2'); +} + +*, *::before, *::after { box-sizing: border-box; } +* { margin: 0; padding: 0; letter-spacing: 0; } +:root { + --bg: #fff; --surface: #f7f7f8; --ink: #202124; --ink-2: #53545c; --ink-3: #70717a; + --line: #e8e8ec; --line-strong: #d9d9e0; --accent: #6650cc; --accent-light: #f0ecfc; + --green: #347459; --red: #b24448; --yellow: #996316; + --radius-card: 6px; --radius-pill: 6px; + --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} +html { scroll-behavior: smooth; scroll-padding-top: 96px; } +body { color: var(--ink); background: var(--bg); font: 400 15px/1.65 var(--font); -webkit-font-smoothing: antialiased; } +button, input, summary { font: inherit; } +button, a, summary { -webkit-tap-highlight-color: transparent; } +button { cursor: pointer; } +a { color: inherit; text-decoration: none; } +button:focus-visible, a:focus-visible, summary:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; } +[hidden] { display: none !important; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; } +.github-icon { width: 20px; height: 20px; fill: currentColor; } +.page-shell { max-width: 1168px; padding: 0 32px; margin: 0 auto; } + +/* One navigation bar, aligned with the page grid. */ +.site-header { position: sticky; top: 0; z-index: 40; background: rgba(255,255,255,.97); border-bottom: 1px solid var(--line); } +nav { max-width: 1168px; height: 72px; padding: 0 32px; margin: auto; display: flex; justify-content: space-between; align-items: center; gap: 24px; } +.nav-logo { display: inline-flex; align-items: center; gap: 10px; font-size: 19px; font-weight: 680; white-space: nowrap; } +.nav-mark { width: 29px; height: 29px; display: grid; place-items: center; background: var(--ink); color: #fff; border-radius: 7px; font-size: 16px; } +.nav-links { display: flex; align-items: center; gap: 24px; } +.nav-link { font-size: 13px; color: var(--ink-2); transition: color .15s; } +.nav-link:hover { color: var(--accent); } +.nav-gh { display: grid; place-items: center; width: 36px; height: 36px; border: 1px solid var(--line); border-radius: 6px; transition: background .15s; } +.nav-gh:hover { background: var(--surface); } +.nav-menu { display: none; position: relative; } +.nav-menu summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; min-height: 44px; font-size: 13px; } +.nav-menu summary::-webkit-details-marker { display: none; } +.nav-menu-panel { position: absolute; right: -48px; top: calc(100% + 14px); width: 230px; padding: 8px; background: #fff; border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 12px 24px #2021240d; } +.nav-menu-panel a { display: block; padding: 12px; font-size: 14px; border-radius: 4px; } +.nav-menu-panel a:hover { background: var(--surface); } + +/* Compact introduction with the actual interactive workspace directly beneath. */ +.hero { text-align: center; padding: 52px 0 44px; } +.hero-copy h1 { font-size: 52px; font-weight: 600; line-height: 1.15; margin-bottom: 16px; } +.hero-copy > p { max-width: 510px; margin: auto; color: var(--ink-2); font-size: 17px; line-height: 1.6; text-wrap: pretty; } +.hero-actions { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 12px; margin-top: 24px; } +.btn { display: inline-flex; justify-content: center; align-items: center; gap: 10px; min-height: 42px; padding: 10px 18px; border: 1px solid transparent; border-radius: 6px; font-size: 13px; font-weight: 550; white-space: nowrap; transition: background .16s, border-color .16s, transform .16s; } +.btn:active { transform: translateY(1px); } +.btn-primary, .btn-inverse { color: #fff; background: var(--ink); } +.btn-primary:hover, .btn-inverse:hover { background: #383940; } +.btn-secondary, .btn-ghost-white { background: #fff; border-color: var(--line-strong); } +.btn-secondary:hover, .btn-ghost-white:hover { background: var(--surface); } +.btn-icon { width: 42px; padding: 0; } +.install-cmd { display: flex; align-items: center; gap: 16px; min-height: 42px; padding: 9px 12px 9px 14px; border: 1px solid var(--line); background: var(--surface); border-radius: 6px; font: 12px/1.5 var(--mono); color: var(--ink-2); } +.install-cmd:hover { border-color: var(--line-strong); } +.install-cmd .icon { width: 15px; height: 15px; } +.copy-icon { display: grid; place-items: center; min-width: 15px; font: 11px var(--font); } +.section { padding-top: 96px; min-width: 0; } +.section-title { font-size: 32px; line-height: 1.25; font-weight: 560; text-wrap: balance; margin-bottom: 12px; } +.section-sub { color: var(--ink-2); max-width: 560px; font-size: 15px; margin-bottom: 36px; text-wrap: pretty; } +.section-label { color: var(--ink-3); font-size: 12px; margin-bottom: 12px; } +.text-link { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-size: 13px; font-weight: 550; } +.text-link:hover { text-decoration: underline; text-underline-offset: 4px; } +h2, h3, p, .pkg-name { overflow-wrap: anywhere; } +code, pre { font-family: var(--mono); } +code { color: var(--accent); font-size: .9em; } +pre { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; } + +/* The demo is a single framed tool; its internal sections are not cards. */ +#demo { padding-top: 0; } +.deck { border: 1px solid var(--line-strong); border-radius: 8px; background: #fff; box-shadow: 0 8px 32px #20212405; } +.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px 20px; border-bottom: 1px solid var(--line); } +.workspace-title { display: flex; align-items: center; gap: 10px; } +.workspace-title .icon { color: var(--ink-3); } +#demo .section-title { margin: 0; font-size: 14px; font-weight: 600; } +.workspace-meta { color: var(--ink-3); font-size: 12px; } +.demo-controls { border-bottom: 1px solid var(--line); } +.pattern-dock { position: relative; padding: 8px 12px; } +.pattern-dock-header, .pattern-copy-mini small, .pattern-payload, .tool-hint { display: none; } +.pattern-grid { display: flex; flex-wrap: wrap; gap: 4px; } +.pattern-option { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px 11px; min-height: 40px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--ink-2); transition: background .15s, color .15s; } +.pattern-option:hover { background: var(--surface); } +.pattern-option.active { color: var(--accent); background: var(--accent-light); border-color: #e1d9f8; } +.pattern-glyph { display: inline-flex; } +.pattern-copy-mini b { font-size: 12px; font-weight: 500; white-space: nowrap; } +.pattern-status { position: absolute; right: 12px; top: 8px; } +.tool-btn { min-height: 40px; padding: 8px 10px; border: 0; color: var(--ink-2); background: #fff; border-radius: 5px; font-size: 12px; } +.tool-btn:hover { background: var(--surface); } +.demo-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; } +.dashboard { position: relative; min-width: 0; padding: 24px; background: #fafafb; border-bottom-left-radius: 8px; } +.kpi-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-bottom: 28px; gap: 1px; background: var(--line); } +.kpi-card { position: relative; min-width: 0; padding: 14px 16px; background: #fafafb; cursor: pointer; transition: background .16s, box-shadow .16s; } +.kpi-card:hover { background: #f3f2f7; } +.kpi-card.active { background: var(--accent-light); box-shadow: inset 3px 0 0 var(--accent); } +.kpi-label { color: var(--ink-2); font-size: 12px; margin-bottom: 7px; } +.kpi-value { font-size: 27px; font-weight: 580; font-variant-numeric: tabular-nums; line-height: 1.25; margin-bottom: 6px; } +.kpi-change { font-size: 11px; } +.up { color: var(--green); } .down { color: var(--red); } +.panel { min-width: 0; } +.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; font-size: 12px; font-weight: 600; } +.panel-header span { color: var(--ink-3); font-size: 11px; font-weight: 400; } +table { width: 100%; border-collapse: collapse; font-size: 12px; } +th { padding: 9px 8px; text-align: left; font-size: 10px; font-weight: 500; color: var(--ink-3); border-bottom: 1px solid var(--line-strong); } +td { padding: 13px 8px; border-bottom: 1px solid var(--line); } +th:first-child, td:first-child { padding-left: 0; } +tbody tr:last-child td { border-bottom: 0; } +tbody tr[data-askable] { cursor: pointer; transition: background .16s; } +tbody tr[data-askable]:hover { background: #f1f0f5; } +tbody tr[data-askable].active { background: var(--accent-light); } +.badge { font-size: 10px; white-space: nowrap; } +.badge-green { color: var(--green); } .badge-yellow { color: var(--yellow); } .badge-red { color: var(--red); } +.ask-btn, .ask-btn-row { display: none; align-items: center; gap: 4px; background: #fff; color: var(--accent); border: 1px solid var(--line-strong); border-radius: 4px; font-size: 10px; white-space: nowrap; padding: 5px 7px; } +.ask-btn .icon, .ask-btn-row .icon { width: 12px; height: 12px; } +.ask-btn { position: absolute; top: 9px; right: 9px; } +.button-mode .ask-btn, .button-mode .ask-btn-row { display: inline-flex; } +.button-mode .kpi-label { padding-top: 22px; } +.chat-sidebar { min-width: 0; display: flex; flex-direction: column; min-height: 470px; height: 100%; max-height: 600px; border-left: 1px solid var(--line); } +.chat-header { display: flex; align-items: center; gap: 8px; padding: 16px; border-bottom: 1px solid var(--line); } +.chat-header > .icon { color: var(--accent); width: 16px; height: 16px; } +.chat-title { font-size: 12px; font-weight: 600; } +.chat-sim-badge { margin-left: auto; font-size: 10px; color: var(--ink-3); } +.chat-sub, .chat-dot { display: none; } +.chat-messages { flex: 1 1 0; min-height: 44px; overflow-y: auto; overscroll-behavior: contain; padding: 16px; display: flex; flex-direction: column; gap: 12px; } +.chat-empty { margin: auto; padding: 20px 0; text-align: center; color: var(--ink-3); font-size: 12px; } +.chat-empty > .icon { display: block; width: 24px; height: 24px; margin: 0 auto 12px; color: #b3abc8; } +.msg { display: flex; flex-direction: column; gap: 4px; animation: messageIn .18s ease-out; } +.msg-bubble { width: fit-content; max-width: 100%; padding: 9px 11px; font-size: 12px; line-height: 1.6; background: var(--surface); border-radius: 6px; overflow-wrap: anywhere; } +.msg.user .msg-bubble { background: var(--ink); color: #fff; align-self: flex-end; } +.msg.ctx .msg-bubble { background: none; font: 10px/1.6 var(--mono); color: var(--ink-3); padding: 0; } +.msg-label { color: var(--ink-3); font-size: 10px; } +.msg.user .msg-label { align-self: flex-end; } +.msg-context-kicker { display: block; font: 500 10px/1.5 var(--font); color: var(--accent); margin-bottom: 4px; } +.msg-context-quote { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font: 12px/1.6 var(--font); } +.typing { display: flex; gap: 4px; padding: 8px; } +.typing span { width: 4px; height: 4px; border-radius: 50%; background: var(--ink-3); animation: typing 1s infinite; } +.typing span:nth-child(2) { animation-delay: .15s; } .typing span:nth-child(3) { animation-delay: .3s; } +.chat-context-bar { display: flex; align-items: flex-start; gap: 8px; padding: 12px 16px; background: #f7f5fe; border-top: 1px solid #e9e3fa; font-size: 11px; flex-shrink: 0; } +.context-bar-label { display: none; } +.context-review { flex: 1; min-width: 0; } +.context-review summary { cursor: pointer; color: var(--accent); font-size: 11px; font-weight: 550; } +.context-review pre { max-height: 96px; overflow-y: auto; padding-top: 10px; font: 10px/1.7 var(--mono); color: var(--ink-2); } +.context-dismiss { flex: 0 0 24px; display: grid; place-items: center; width: 24px; height: 24px; margin-top: -3px; border: none; background: none; color: var(--ink-3); } +.context-dismiss .icon { width: 14px; height: 14px; } +.selected-text-card, .selected-region-card { margin: 12px 16px 0; padding-left: 10px; border-left: 2px solid var(--accent); } +.selected-text-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } +.selected-text-card-title { color: var(--accent); font-size: 10px; font-weight: 600; } +.selected-text-card-state { color: var(--ink-3); font-size: 10px; } +.selected-text-quote { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: 11px; color: var(--ink-2); } +.selected-region-list { display: grid; gap: 2px; font-size: 11px; color: var(--ink-2); max-height: 64px; overflow: auto; } +.selected-region-list span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.dashboard ::selection { background: #ded5fa; color: var(--ink); } +.text-capture-mark { position: absolute; pointer-events: none; z-index: 6; background: #bda7f34d; border-bottom: 2px solid var(--accent); border-radius: 2px; } +.composer-suggestions { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px 16px 0; } +.question-chip { background: #fff; border: 1px solid var(--line); border-radius: 4px; color: var(--ink-2); font-size: 10px; padding: 5px 7px; text-align: left; } +.question-chip:hover { background: var(--surface); border-color: var(--line-strong); } +.chat-input-row { display: flex; gap: 8px; padding: 14px 16px; margin-top: auto; } +.chat-input { min-width: 0; flex: 1; padding: 10px 11px; min-height: 40px; border: 1px solid var(--line-strong); border-radius: 5px; background: #fff; color: var(--ink); font-size: 12px; } +.chat-input::placeholder { color: var(--ink-3); } +.chat-send { display: grid; place-items: center; width: 40px; height: 40px; flex: 0 0 40px; border: 0; border-radius: 5px; background: var(--ink); color: #fff; } +.developer-panel { padding: 12px 20px; border-top: 1px solid var(--line); } +.developer-panel > summary { font: 12px var(--mono); color: var(--ink-2); cursor: pointer; width: fit-content; } +.demo-controls-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.output-controls { padding-top: 20px; } +.toggle-label { font-size: 11px; color: var(--ink-3); } +.toggle-group { display: inline-flex; border: 1px solid var(--line); padding: 2px; border-radius: 5px; } +.toggle-btn { border: 0; background: none; border-radius: 3px; font-size: 11px; color: var(--ink-2); padding: 5px 9px; } +.toggle-btn.active { background: var(--surface); color: var(--ink); } +.code-panel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin: 16px 0 4px; } +.code-block { min-width: 0; background: var(--surface); border-radius: 5px; padding: 16px; } +.code-block-header { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; color: var(--ink-3); font-size: 10px; } +.code-block-header span:last-child { display: none; } +.code-block pre { font-size: 11px; line-height: 1.7; max-height: 260px; overflow: auto; } +.empty { color: var(--ink-3); } + +/* Distinct reading rhythms rather than repeated card grids. */ +.capability-list { display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 36px; padding-top: 8px; } +.capability-list article { min-width: 0; } +.capability-list .icon { color: var(--accent); margin-bottom: 16px; width: 22px; height: 22px; } +.capability-list h3 { font-size: 17px; font-weight: 550; margin-bottom: 10px; line-height: 1.4; } +.capability-list p { color: var(--ink-2); font-size: 13px; line-height: 1.75; margin-bottom: 16px; } +.capability-list .release-note { font-size: 11px; color: var(--ink-3); } +#how { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: 48px; align-items: start; } +.implementation-intro { padding-top: 8px; } +.implementation-intro .text-link { margin-top: 24px; } +.implementation-code { min-width: 0; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; } +.example-tabs { display: flex; padding: 0 12px; border-bottom: 1px solid var(--line); background: var(--surface); } +.example-tab { border: 0; background: none; padding: 14px 12px; color: var(--ink-3); font-size: 12px; border-bottom: 2px solid transparent; } +.example-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); } +.example-panel { padding: 22px; } +.example-panel p { margin-bottom: 18px; color: var(--ink-2); font-size: 13px; } +.example-panel pre { font-size: 12px; line-height: 1.8; min-height: 168px; color: var(--ink-2); } +.recording { margin-top: 24px; font-size: 13px; color: var(--ink-2); } +.recording summary { cursor: pointer; } +.recording video { margin-top: 20px; width: 100%; aspect-ratio: 16/9; background: var(--surface); border-radius: 6px; } +.pkg-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px 36px; } +.pkg-card { position: relative; min-width: 0; display: block; padding: 0 24px 0 0; } +.pkg-card > .icon { position: absolute; right: 0; top: 3px; width: 15px; height: 15px; color: #95959e; } +.pkg-card:hover .pkg-name, .pkg-card:hover > .icon { color: var(--accent); } +.pkg-badge { font-size: 11px; color: var(--ink-3); margin-bottom: 3px; } +.pkg-name { font: 550 13px/1.6 var(--mono); color: var(--ink); } +.pkg-desc { font-size: 12px; line-height: 1.65; color: var(--ink-2); margin-top: 6px; } +.pkg-tag { display: none; } +#mcp { margin-top: 88px; padding: 48px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.integration-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 36px; } +.integration-list h3 { font-size: 15px; font-weight: 550; margin-bottom: 8px; } +.integration-list p { color: var(--ink-2); font-size: 13px; margin-bottom: 14px; } +.integration-note { max-width: 720px; color: var(--ink-3); font-size: 11px; margin-top: 28px; } +.cta-section { padding: 64px 0; text-align: center; } +.cta-card h2 { font-size: 28px; font-weight: 550; line-height: 1.3; margin-bottom: 12px; } +.cta-card p { color: var(--ink-2); font-size: 14px; max-width: 460px; margin: auto; } +.cta-actions { display: flex; justify-content: center; gap: 10px; margin-top: 24px; } +footer { border-top: 1px solid var(--line); display: flex; justify-content: space-between; gap: 24px; padding: 24px 0 32px; color: var(--ink-3); font-size: 11px; } +footer a:hover { color: var(--ink); } + + .selection-layer { position: absolute; inset: 0; z-index: 9; pointer-events: none; border-radius: 6px; overflow: visible; } + .dashboard.selecting .selection-layer { pointer-events: auto; cursor: crosshair; } + .dashboard.selecting.lasso-active .selection-layer { cursor: none; } + .selection-layer::before { + content: ''; position: absolute; inset: 0; border-radius: inherit; + border: 1px solid rgba(79,70,229,0.18); background: rgba(79,70,229,0.03); + opacity: 0; transition: opacity .12s; + } + .dashboard.selecting .selection-layer::before { opacity: 1; } + .dashboard.selecting.lasso-active .selection-layer::before { + opacity: 0; + } + .selection-drawing { + position: absolute; border: 2px solid var(--accent); background: rgba(79,70,229,0.09); + box-shadow: 0 10px 28px rgba(79,70,229,0.12); pointer-events: none; z-index: 11; + } + .selection-drawing.circle { border-radius: 999px; } + .selection-drawing.lasso { background: transparent; border: none; box-shadow: none; inset: 0; filter: drop-shadow(0 0 10px rgba(79,70,229,0.32)); } + .selection-drawing svg { position: absolute; inset: 0; overflow: visible; } + .selection-drawing path { fill: rgba(79,70,229,0.08); stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; } + .selection-drawing.lasso path { + fill: none; + stroke: url(#lasso-gradient); + stroke-width: 3.2; + stroke-linecap: round; + stroke-linejoin: round; + } + .selection-drawing.lasso .lasso-aura { + fill: none; + stroke: rgba(124,58,237,0.16); + stroke-width: 8; + stroke-linecap: round; + stroke-linejoin: round; + filter: blur(1px); + } + .selected-region { + position: absolute; z-index: 12; pointer-events: none; + border: 2px solid rgba(124,58,237,0.78); + background: rgba(124,58,237,0.08); + box-shadow: 0 0 0 3px rgba(124,58,237,0.04); + animation: selectedRegionIn .18s ease-out; + } + .selected-region.circle { border-radius: 999px; } + .selected-region.lasso { inset: 0; border: none; background: transparent; box-shadow: none; } + .selected-region svg { position: absolute; inset: 0; overflow: visible; } + .selected-region path { + fill: none; + stroke: rgba(124,58,237,0.9); + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; + filter: drop-shadow(0 0 7px rgba(124,58,237,0.24)); + } + .selected-region-label { + position: absolute; left: 0; bottom: calc(100% + 7px); + display: inline-flex; align-items: center; gap: .35rem; + padding: .28rem .52rem; border-radius: var(--radius-pill); + border: 1px solid rgba(124,58,237,0.18); background: rgba(255,255,255,0.94); + color: #5b21b6; font-size: .66rem; font-weight: 850; + letter-spacing: 0; text-transform: uppercase; + box-shadow: 0 10px 24px rgba(15,23,42,0.1); white-space: nowrap; + } + .selected-region-label::before { + content: ''; width: .42rem; height: .42rem; border-radius: 999px; + background: #7c3aed; box-shadow: 0 0 0 4px rgba(124,58,237,0.1); + } + .selection-inline-prompt { + position: absolute; left: 0; top: calc(100% + 9px); z-index: 13; + display: flex; align-items: center; gap: .42rem; + width: min(310px, calc(100vw - 3rem)); padding: .38rem; + border: 1px solid rgba(124,58,237,0.18); border-radius: var(--radius-pill); + background: rgba(255,255,255,0.96); box-shadow: 0 16px 42px rgba(15,23,42,0.15); + pointer-events: auto; + } + .selection-inline-prompt.above { top: auto; bottom: calc(100% + 9px); } + .selection-inline-prompt input { + min-width: 0; flex: 1; border: none; outline: none; background: transparent; + color: var(--ink); font: 500 .78rem/1.2 var(--font); padding: .26rem .32rem; + } + .selection-inline-prompt button { + flex: 0 0 auto; border: none; border-radius: var(--radius-pill); + background: var(--ink); color: #fff; padding: .36rem .62rem; + font-size: .7rem; font-weight: 800; cursor: pointer; + } + .text-selection-inline-prompt { + z-index: 14; + box-shadow: 0 16px 42px rgba(91,33,182,0.16); + } + @keyframes selectedRegionIn { + from { opacity: 0; transform: scale(.985); } + to { opacity: 1; transform: scale(1); } + } + .ai-cursor-canvas { + position: absolute; inset: 0; z-index: 10; + width: 100%; height: 100%; + pointer-events: none; opacity: 0; + transition: opacity .12s ease; + } + .dashboard.lasso-active .ai-cursor-canvas { opacity: 1; } + .ai-cursor { + position: absolute; left: 0; top: 0; z-index: 12; + display: none; width: 1.95rem; height: 1.95rem; + transform: translate3d(0,0,0); pointer-events: none; + filter: + drop-shadow(0 2px 4px rgba(15,23,42,0.18)) + drop-shadow(0 0 7px rgba(124,58,237,0.34)); + } + .dashboard.lasso-active .ai-cursor { display: block; } + .ai-cursor-icon { + position: absolute; left: 50%; top: 50%; + display: block; width: 1.95rem; height: 1.95rem; + overflow: visible; transform: translate(-45%, -45%); + } + .ai-cursor-outline { + fill: none; + stroke: #a78bfa; + stroke-linejoin: round; + stroke-width: 4.6; + } + .ai-cursor-fill { + fill: #fff; + stroke: #7c3aed; + stroke-linejoin: round; + stroke-width: 1.2; + } + +@keyframes messageIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } +@keyframes typing { 50% { opacity: .3; } } +@media (max-width: 1040px) { + .demo-layout { grid-template-columns: minmax(0, 1fr) 300px; } + .dashboard { padding: 16px; } + .pattern-option { padding-inline: 8px; gap: 5px; } + .pattern-status { position: static; } + .pattern-status:has(#cancel-tool[style*="inline"]) { padding-top: 6px; } + .kpi-value { font-size: 24px; } + .panel-header span { display: none; } + .demo-layout th:nth-child(3), .demo-layout td:nth-child(3) { display: none; } +} +@media (max-width: 720px) { + .page-shell { padding: 0 20px; } + nav { height: 64px; padding: 0 20px; gap: 12px; } + .nav-logo { font-size: 17px; } + .nav-links { gap: 16px; } + .nav-link { display: none; } + .nav-menu { display: block; } + .hero { padding: 40px 0 32px; } + .hero-copy h1 { font-size: 36px; max-width: 340px; margin: 0 auto 14px; } + .hero-copy > p { font-size: 15px; max-width: 320px; } + .hero-actions { gap: 10px; margin-top: 22px; } + .btn { padding-inline: 14px; } + .install-cmd { max-width: 100%; font-size: 11px; min-height: 40px; } + .workspace-header { padding: 13px 14px; } + .workspace-title { gap: 7px; } + .workspace-title .icon { width: 16px; height: 16px; } + #demo .section-title { font-size: 12px; } + .workspace-meta { font-size: 10px; } + .pattern-dock { padding: 6px; } + .pattern-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2px; } + .pattern-option { min-height: 46px; padding: 6px 2px; gap: 4px; flex-direction: column; } + .pattern-option .icon { width: 17px; height: 17px; } + .pattern-copy-mini b { font-size: 10px; } + .tool-btn { min-height: 36px; } + .demo-layout { grid-template-columns: minmax(0, 1fr); } + .dashboard { padding: 14px; } + .kpi-grid { margin-bottom: 24px; } + .kpi-card { padding: 12px; } + .kpi-label { font-size: 10px; } + .kpi-value { font-size: 22px; } + .kpi-change { font-size: 9px; } + .panel-header { margin-bottom: 8px; } + table { font-size: 11px; } + td { padding: 11px 5px; } + th { padding-inline: 5px; font-size: 9px; } + .ask-btn { right: 5px; } + .ask-btn-row { font-size: 0; padding: 7px; } + .ask-btn-row .icon { width: 13px; height: 13px; } + .chat-sidebar { border-left: 0; border-top: 1px solid var(--line); height: 350px; min-height: 350px; max-height: none; } + .chat-header { padding: 12px 14px; } + .chat-messages { padding: 14px; } + .developer-panel { padding: 14px; } + .code-panel { grid-template-columns: minmax(0, 1fr); } + .section { padding-top: 64px; } + .section-title { font-size: 26px; } + .section-sub { font-size: 14px; margin-bottom: 28px; } + .capability-list { grid-template-columns: minmax(0, 1fr); gap: 28px; } + .capability-list article { display: grid; grid-template-columns: 26px minmax(0, 1fr); column-gap: 16px; } + .capability-list article > .icon { grid-column: 1; grid-row: 1 / span 4; margin-top: 2px; } + .capability-list article > :not(.icon) { grid-column: 2; } + .capability-list h3 { font-size: 16px; margin-bottom: 6px; } + .capability-list p { margin-bottom: 10px; } + #how { grid-template-columns: minmax(0, 1fr); gap: 24px; } + .implementation-intro .section-sub { margin-bottom: 0; } + .implementation-intro .text-link { margin-top: 16px; } + .example-panel { padding: 18px; } + .example-panel pre { font-size: 11px; min-height: 180px; } + .pkg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 20px; } + .pkg-card { padding-right: 0; } + .pkg-card > .icon { display: none; } + .pkg-name { font-size: 11px; } + .pkg-desc { font-size: 11px; } + #mcp { margin-top: 56px; padding: 36px 0; } + .integration-list { grid-template-columns: minmax(0, 1fr); gap: 28px; } + .cta-section { padding: 48px 0; } + .cta-card h2 { font-size: 24px; } + footer { flex-direction: column; gap: 14px; } +} +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } +} + From cef43bd2c04e64e278e0142dc6ef28a8f02182fd Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:53:25 -0400 Subject: [PATCH 4/6] fix(site): preserve icon contrast and stable selection controls --- site/tests/server.mjs | 1 - site/tests/website.spec.ts | 23 +++++++++++--- site/www/assets/README.md | 2 +- site/www/assets/icons.svg | 1 - site/www/index.html | 62 +++++++++++++++++++------------------- site/www/site.css | 11 +++---- 6 files changed, 56 insertions(+), 44 deletions(-) delete mode 100644 site/www/assets/icons.svg diff --git a/site/tests/server.mjs b/site/tests/server.mjs index 2ad27ebb..35be91c7 100644 --- a/site/tests/server.mjs +++ b/site/tests/server.mjs @@ -5,7 +5,6 @@ import { readFile } from 'node:fs/promises'; const assets = new Map([ ['/', ['index.html', 'text/html; charset=utf-8']], ['/site.css', ['site.css', 'text/css; charset=utf-8']], - ['/assets/icons.svg', ['assets/icons.svg', 'image/svg+xml']], ['/assets/inter-latin-variable.woff2', ['assets/inter-latin-variable.woff2', 'font/woff2']], ['/askable-ui-code.mp4', ['askable-ui-code.mp4', 'video/mp4']], ]); diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts index 28df773f..cf394101 100644 --- a/site/tests/website.spec.ts +++ b/site/tests/website.spec.ts @@ -142,15 +142,30 @@ test('integration tabs support mouse and keyboard navigation', async ({ page }) await expect(page.getByRole('tabpanel')).toContainText('data-askable'); }); -test('font and toolbar icons load from local assets', async ({ page }) => { +test('font is local and inline icons retain accessible colors', async ({ page }) => { await expect.poll(() => page.evaluate(() => document.fonts.check('400 16px Inter'))).toBe(true); await expect(page.locator('.pattern-option .icon')).toHaveCount(8); - const asset = await page.request.get('/assets/icons.svg'); - expect(asset.ok()).toBe(true); - expect(await asset.text()).toContain('id="lasso"'); + await expect(page.locator('.icon use')).toHaveCount(0); + expect(await page.locator('.pattern-option .icon path').count()).toBeGreaterThan(0); + await expect(page.locator('#chat-send .icon')).toHaveCSS('stroke', 'rgb(255, 255, 255)'); + await expect(page.locator('[data-mode="click"]')).toHaveCSS('color', 'rgb(102, 80, 204)'); await expect(page.locator('link[href*="fonts.googleapis.com"]')).toHaveCount(0); }); +test('selection mode controls do not resize the workspace', async ({ page }) => { + for (const width of [390, 1280]) { + await page.setViewportSize({ width, height: 900 }); + const before = await page.locator('.demo-controls').boundingBox(); + await page.locator('[data-tool="lasso"]').click(); + const after = await page.locator('.demo-controls').boundingBox(); + expect(after?.height).toBe(before?.height); + await expect(page.locator('#cancel-tool')).toBeVisible(); + await expect(page.locator('.workspace-meta')).toBeHidden(); + await page.locator('#cancel-tool').click(); + await expect(page.locator('.workspace-meta')).toBeVisible(); + } +}); + test('current capabilities and release boundaries are explicit', async ({ page }) => { await expect(page.getByRole('link', { name: 'Explore Bridge' })).toHaveAttribute('href', /guide\/bridge$/); await expect(page.locator('.release-note')).toContainText('not yet released to npm'); diff --git a/site/www/assets/README.md b/site/www/assets/README.md index 120daff9..3398ca49 100644 --- a/site/www/assets/README.md +++ b/site/www/assets/README.md @@ -1,6 +1,6 @@ # Website Assets - `inter-latin-variable.woff2`: Inter Latin variable font, distributed by `@fontsource-variable/inter` 5.3.0. Unmodified, licensed under SIL Open Font License 1.1. See `INTER-LICENSE.txt`. -- `icons.svg`: a subset of Lucide 1.48.0, generated from `lucide-static` icon nodes. Licensed under ISC with upstream MIT notices. See `LUCIDE-LICENSE.txt`. +- Icons in `index.html`: a subset of Lucide 1.48.0, generated from `lucide-static` icon nodes and inlined for consistent Safari color inheritance. Licensed under ISC with upstream MIT notices. See `LUCIDE-LICENSE.txt`. These are local website dependencies, not promotional assets. Keeping them local avoids third-party font and icon requests on page load. diff --git a/site/www/assets/icons.svg b/site/www/assets/icons.svg deleted file mode 100644 index 27010ea3..00000000 --- a/site/www/assets/icons.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/site/www/index.html b/site/www/index.html index d0015442..a8208265 100644 --- a/site/www/index.html +++ b/site/www/index.html @@ -24,7 +24,7 @@ ' + '' + - ''; + ''; tbody.appendChild(tr); }); var askable = new Ctx(); diff --git a/site/www/site.css b/site/www/site.css index f856a0cd..81a51c07 100644 --- a/site/www/site.css +++ b/site/www/site.css @@ -20,12 +20,12 @@ html { scroll-behavior: smooth; scroll-padding-top: 96px; } body { color: var(--ink); background: var(--bg); font: 400 15px/1.65 var(--font); -webkit-font-smoothing: antialiased; } button, input, summary { font: inherit; } button, a, summary { -webkit-tap-highlight-color: transparent; } -button { cursor: pointer; } +button { cursor: pointer; appearance: none; -webkit-appearance: none; } a { color: inherit; text-decoration: none; } button:focus-visible, a:focus-visible, summary:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; } [hidden] { display: none !important; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } -.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; } +.icon { width: 18px; height: 18px; color: inherit; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; } .github-icon { width: 20px; height: 20px; fill: currentColor; } .page-shell { max-width: 1168px; padding: 0 32px; margin: 0 auto; } @@ -90,7 +90,8 @@ pre { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; } .pattern-option.active { color: var(--accent); background: var(--accent-light); border-color: #e1d9f8; } .pattern-glyph { display: inline-flex; } .pattern-copy-mini b { font-size: 12px; font-weight: 500; white-space: nowrap; } -.pattern-status { position: absolute; right: 12px; top: 8px; } +.pattern-status { position: absolute; right: 12px; top: -46px; } +.deck:has(.pattern-option.active:not([data-mode])) .workspace-meta { visibility: hidden; } .tool-btn { min-height: 40px; padding: 8px 10px; border: 0; color: var(--ink-2); background: #fff; border-radius: 5px; font-size: 12px; } .tool-btn:hover { background: var(--surface); } .demo-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; } @@ -351,8 +352,6 @@ footer a:hover { color: var(--ink); } .demo-layout { grid-template-columns: minmax(0, 1fr) 300px; } .dashboard { padding: 16px; } .pattern-option { padding-inline: 8px; gap: 5px; } - .pattern-status { position: static; } - .pattern-status:has(#cancel-tool[style*="inline"]) { padding-top: 6px; } .kpi-value { font-size: 24px; } .panel-header span { display: none; } .demo-layout th:nth-child(3), .demo-layout td:nth-child(3) { display: none; } @@ -381,6 +380,7 @@ footer a:hover { color: var(--ink); } .pattern-option .icon { width: 17px; height: 17px; } .pattern-copy-mini b { font-size: 10px; } .tool-btn { min-height: 36px; } + .pattern-status { top: -40px; right: 8px; } .demo-layout { grid-template-columns: minmax(0, 1fr); } .dashboard { padding: 14px; } .kpi-grid { margin-bottom: 24px; } @@ -429,4 +429,3 @@ footer a:hover { color: var(--ink); } html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } } - From 81c189b94b587611dc93ae4240057a2179f9ac89 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:57:06 -0400 Subject: [PATCH 5/6] fix(site): stabilize toolbar dimensions across browsers --- site/tests/website.spec.ts | 5 ++++- site/www/site.css | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts index cf394101..6a23f849 100644 --- a/site/tests/website.spec.ts +++ b/site/tests/website.spec.ts @@ -158,9 +158,12 @@ test('selection mode controls do not resize the workspace', async ({ page }) => const before = await page.locator('.demo-controls').boundingBox(); await page.locator('[data-tool="lasso"]').click(); const after = await page.locator('.demo-controls').boundingBox(); - expect(after?.height).toBe(before?.height); + if (!before || !after) throw new Error('Missing toolbar bounds'); + // Firefox can round bounding boxes differently after the automatic scroll. + expect(after.height).toBeCloseTo(before.height, 2); await expect(page.locator('#cancel-tool')).toBeVisible(); await expect(page.locator('.workspace-meta')).toBeHidden(); + await page.screenshot({ path: test.info().outputPath(`lasso-controls-${width}.png`) }); await page.locator('#cancel-tool').click(); await expect(page.locator('.workspace-meta')).toBeVisible(); } diff --git a/site/www/site.css b/site/www/site.css index 81a51c07..7134ba14 100644 --- a/site/www/site.css +++ b/site/www/site.css @@ -85,11 +85,12 @@ pre { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; } .pattern-dock { position: relative; padding: 8px 12px; } .pattern-dock-header, .pattern-copy-mini small, .pattern-payload, .tool-hint { display: none; } .pattern-grid { display: flex; flex-wrap: wrap; gap: 4px; } -.pattern-option { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px 11px; min-height: 40px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--ink-2); transition: background .15s, color .15s; } +.pattern-option { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px 11px; height: 40px; min-height: 40px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--ink-2); transition: background .15s, color .15s; } .pattern-option:hover { background: var(--surface); } .pattern-option.active { color: var(--accent); background: var(--accent-light); border-color: #e1d9f8; } .pattern-glyph { display: inline-flex; } -.pattern-copy-mini b { font-size: 12px; font-weight: 500; white-space: nowrap; } +.pattern-copy-mini { display: flex; align-items: center; } +.pattern-copy-mini b { font-size: 12px; line-height: 16px; font-weight: 500; white-space: nowrap; } .pattern-status { position: absolute; right: 12px; top: -46px; } .deck:has(.pattern-option.active:not([data-mode])) .workspace-meta { visibility: hidden; } .tool-btn { min-height: 40px; padding: 8px 10px; border: 0; color: var(--ink-2); background: #fff; border-radius: 5px; font-size: 12px; } @@ -376,9 +377,9 @@ footer a:hover { color: var(--ink); } .workspace-meta { font-size: 10px; } .pattern-dock { padding: 6px; } .pattern-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2px; } - .pattern-option { min-height: 46px; padding: 6px 2px; gap: 4px; flex-direction: column; } + .pattern-option { height: 56px; min-height: 56px; padding: 6px 2px; gap: 4px; flex-direction: column; } .pattern-option .icon { width: 17px; height: 17px; } - .pattern-copy-mini b { font-size: 10px; } + .pattern-copy-mini b { font-size: 10px; line-height: 14px; } .tool-btn { min-height: 36px; } .pattern-status { top: -40px; right: 8px; } .demo-layout { grid-template-columns: minmax(0, 1fr); } From 678135f53d25abbd5a8261ab95394e5a76ad5ad7 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:58:13 -0400 Subject: [PATCH 6/6] test(site): capture screenshots after font and viewport paints --- site/tests/website.spec.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/site/tests/website.spec.ts b/site/tests/website.spec.ts index 6a23f849..ddbb5764 100644 --- a/site/tests/website.spec.ts +++ b/site/tests/website.spec.ts @@ -1,13 +1,22 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; + +async function settleRendering(page: Page) { + await page.evaluate(async () => { + await document.fonts.ready; + // Let font metrics and inherited colors reach the compositor after a resize or reload. + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); + }); +} test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.evaluate(() => document.fonts.ready); + await settleRendering(page); }); for (const width of [320, 390, 768, 1280, 1440]) { test(`layout fits ${width}px and keeps the first action visible`, async ({ page }) => { await page.setViewportSize({ width, height: width < 768 ? 844 : 720 }); + await settleRendering(page); await expect(page.locator('#kpi-grid .kpi-card')).toHaveCount(4); await expect(page.locator('#install-copy')).toBeInViewport(); await expect(page.getByRole('heading', { name: 'Analytics workspace' })).toBeInViewport(); @@ -117,7 +126,7 @@ test('text mode ignores plain clicks and retains captured text', async ({ page } test('homepage and selected context screenshots use the website font', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 1100 }); await page.reload(); - await page.evaluate(() => document.fonts.ready); + await settleRendering(page); await expect.poll(() => page.evaluate(() => document.fonts.check('400 16px Inter'))).toBe(true); await page.screenshot({ path: test.info().outputPath('homepage-desktop.png') }); await page.locator('.kpi-card').first().click(); @@ -126,7 +135,7 @@ test('homepage and selected context screenshots use the website font', async ({ await page.screenshot({ path: test.info().outputPath('homepage-context.png') }); await page.setViewportSize({ width: 390, height: 844 }); await page.reload(); - await page.evaluate(() => document.fonts.ready); + await settleRendering(page); await page.screenshot({ path: test.info().outputPath('homepage-mobile.png') }); }); @@ -155,6 +164,7 @@ test('font is local and inline icons retain accessible colors', async ({ page }) test('selection mode controls do not resize the workspace', async ({ page }) => { for (const width of [390, 1280]) { await page.setViewportSize({ width, height: 900 }); + await settleRendering(page); const before = await page.locator('.demo-controls').boundingBox(); await page.locator('[data-tool="lasso"]').click(); const after = await page.locator('.demo-controls').boundingBox(); @@ -163,6 +173,7 @@ test('selection mode controls do not resize the workspace', async ({ page }) => expect(after.height).toBeCloseTo(before.height, 2); await expect(page.locator('#cancel-tool')).toBeVisible(); await expect(page.locator('.workspace-meta')).toBeHidden(); + await settleRendering(page); await page.screenshot({ path: test.info().outputPath(`lasso-controls-${width}.png`) }); await page.locator('#cancel-tool').click(); await expect(page.locator('.workspace-meta')).toBeVisible();
CompanyMRRPlanStatus
' + d.company + '' + d.mrr + '' + d.plan + '' + statusLabel[d.status] + ' ' + d.company + '' + d.mrr + '' + d.plan + '' + statusLabel[d.status] + '