From cb2df3ced11a22e8b06816688ffb17c6962b11da Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 15 May 2026 03:41:15 -0700 Subject: [PATCH 1/2] feat: add sidebar and breadcrumb CSS exports Closes #3 --- dist/breadcrumb.css | 45 ++++++++++++++++++ dist/sidebar.css | 100 +++++++++++++++++++++++++++++++++++++++ package.json | 6 ++- tests/breadcrumb.test.ts | 13 +++++ tests/sidebar.test.ts | 17 +++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 dist/breadcrumb.css create mode 100644 dist/sidebar.css create mode 100644 tests/breadcrumb.test.ts create mode 100644 tests/sidebar.test.ts diff --git a/dist/breadcrumb.css b/dist/breadcrumb.css new file mode 100644 index 0000000..43e7d1f --- /dev/null +++ b/dist/breadcrumb.css @@ -0,0 +1,45 @@ +/* @bchen/ui — breadcrumb navigation + * Uses only tokens defined in dist/tokens.css. + * Companion to dist/tokens.css and dist/base.css. */ + +.breadcrumb { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.25rem; + padding: 0.75rem 2rem 0; + font-size: 13px; + color: var(--fg-muted); +} + +.breadcrumb a { + color: var(--fg-muted); + text-decoration: none; +} + +.breadcrumb a:hover { + color: var(--fg); + text-decoration: underline; + text-underline-offset: 2px; +} + +.breadcrumb a:focus-visible { + color: var(--fg); + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 2px; +} + +.breadcrumb [aria-current="page"] { + color: var(--fg); + font-weight: 500; + text-decoration: none; + cursor: default; +} + +.breadcrumb-sep { + color: var(--fg-muted); + user-select: none; + font-size: 11px; + opacity: 0.6; +} diff --git a/dist/sidebar.css b/dist/sidebar.css new file mode 100644 index 0000000..fcba076 --- /dev/null +++ b/dist/sidebar.css @@ -0,0 +1,100 @@ +/* @bchen/ui — sidebar layout + * Uses only tokens defined in dist/tokens.css. + * Companion to dist/tokens.css and dist/base.css. */ + +/* ── Desktop: two-column grid ─────────────────────────────────────────── */ +@media (min-width: 768px) { + body { + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: 1fr; + } + + .mobile-header { display: none; } + + .sidebar { + display: flex; + flex-direction: column; + height: 100vh; + position: sticky; + top: 0; + border-right: var(--border); + background: var(--bg); + padding: 1rem 0; + overflow-y: auto; + } + + .app-body { + min-width: 0; + display: flex; + flex-direction: column; + min-height: 100vh; + } +} + +/* ── Mobile: sidebar hidden, top bar shown ─────────────────────────────── */ +@media (max-width: 767px) { + .sidebar { display: none; } + .mobile-header { display: flex; } + .app-body { + display: flex; + flex-direction: column; + flex: 1; + } +} + +/* ── Sidebar components ────────────────────────────────────────────────── */ +.sidebar-brand { + padding: 0 1rem 1rem; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; +} + +.sidebar-brand a { + text-decoration: none; + color: var(--fg); +} + +.sidebar-nav { + display: flex; + flex-direction: column; + flex: 1; + gap: 0.125rem; + padding: 0 0.5rem; +} + +.nav-link { + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + color: var(--fg); + text-decoration: none; + font-size: 14px; + transition: background 0.1s ease; +} + +.nav-link:hover, +.nav-link:focus-visible { + background: var(--bg-elevated); + outline: none; +} + +.nav-link.nav-active { + font-weight: 600; + background: var(--bg-elevated); + color: var(--fg); +} + +.sidebar-footer { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + border-top: 1px solid var(--border-color); + margin-top: auto; +} + +.sidebar-user { + font-size: 11px; + color: var(--fg-muted); +} diff --git a/package.json b/package.json index 28566fb..0bb0494 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "dist" ], "exports": { - "./tokens.css": "./dist/tokens.css", - "./base.css": "./dist/base.css" + "./tokens.css": "./dist/tokens.css", + "./base.css": "./dist/base.css", + "./sidebar.css": "./dist/sidebar.css", + "./breadcrumb.css": "./dist/breadcrumb.css" }, "scripts": { "test": "vitest run" diff --git a/tests/breadcrumb.test.ts b/tests/breadcrumb.test.ts new file mode 100644 index 0000000..adf92fe --- /dev/null +++ b/tests/breadcrumb.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; + +const css = readFileSync(new URL('../dist/breadcrumb.css', import.meta.url), 'utf-8'); + +describe('breadcrumb.css', () => { + it('is non-empty', () => { expect(css.length).toBeGreaterThan(0); }); + it('references --fg', () => { expect(css).toContain('var(--fg)'); }); + it('references --fg-muted', () => { expect(css).toContain('var(--fg-muted)'); }); + it('styles aria-current="page"', () => { expect(css).toContain('[aria-current="page"]'); }); + it('styles .breadcrumb-sep', () => { expect(css).toContain('.breadcrumb-sep'); }); + it('styles .breadcrumb links', () => { expect(css).toContain('.breadcrumb a'); }); +}); diff --git a/tests/sidebar.test.ts b/tests/sidebar.test.ts new file mode 100644 index 0000000..77e60d7 --- /dev/null +++ b/tests/sidebar.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; + +const css = readFileSync(new URL('../dist/sidebar.css', import.meta.url), 'utf-8'); + +describe('sidebar.css', () => { + it('is non-empty', () => { expect(css.length).toBeGreaterThan(0); }); + it('references --fg', () => { expect(css).toContain('var(--fg)'); }); + it('references --bg', () => { expect(css).toContain('var(--bg)'); }); + it('references --border', () => { expect(css).toMatch(/var\(--border/); }); + it('references --radius', () => { expect(css).toContain('var(--radius)'); }); + it('references --fg-muted', () => { expect(css).toContain('var(--fg-muted)'); }); + it('includes responsive breakpoint at 768px', () => { expect(css).toContain('768px'); }); + it('hides sidebar on mobile', () => { expect(css).toMatch(/\.sidebar\s*\{\s*display\s*:\s*none/); }); + it('styles .nav-link', () => { expect(css).toContain('.nav-link'); }); + it('styles .nav-active', () => { expect(css).toContain('nav-active'); }); +}); From 75930e5c1fab8e842fecc87df2b0eeb1c9b80868 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 15 May 2026 03:46:32 -0700 Subject: [PATCH 2/2] fix: separate hover/focus-visible on .nav-link; restore outline ring for keyboard focus (WCAG 2.4.7) --- dist/sidebar.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dist/sidebar.css b/dist/sidebar.css index fcba076..678d4ef 100644 --- a/dist/sidebar.css +++ b/dist/sidebar.css @@ -73,10 +73,15 @@ transition: background 0.1s ease; } -.nav-link:hover, +.nav-link:hover { + background: var(--bg-elevated); +} + .nav-link:focus-visible { background: var(--bg-elevated); - outline: none; + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: var(--radius); } .nav-link.nav-active { @@ -90,7 +95,7 @@ display: flex; flex-direction: column; gap: 0.25rem; - border-top: 1px solid var(--border-color); + border-top: 1px solid var(--border-color); /* subtle divider, lighter than --border */ margin-top: auto; }