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'); }); +});