From 0ee0bd245417d8e2d5bab9bacbc50f9da1a3d075 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 03:52:38 +0000 Subject: [PATCH 1/5] feat(web): add sprite + initial 13-icon set --- web/src/icons/Icon.tsx | 22 +++++++++++++ web/src/icons/sprite.tsx | 51 +++++++++++++++++++++++++++++++ web/tests/component/Icon.test.tsx | 18 +++++++++++ 3 files changed, 91 insertions(+) create mode 100644 web/src/icons/Icon.tsx create mode 100644 web/src/icons/sprite.tsx create mode 100644 web/tests/component/Icon.test.tsx diff --git a/web/src/icons/Icon.tsx b/web/src/icons/Icon.tsx new file mode 100644 index 0000000..efbca7a --- /dev/null +++ b/web/src/icons/Icon.tsx @@ -0,0 +1,22 @@ +type IconName = + | 'check' | 'x' | 'plus' | 'stop' | 'retry' | 'search' | 'alert' | 'info' + | 'arrow-right' | 'back' | 'list' | 'network' | 'message'; + +interface IconProps { + name: IconName; + size?: 12 | 14 | 16; +} + +export function Icon({ name, size = 14 }: IconProps) { + return ( + + + + ); +} diff --git a/web/src/icons/sprite.tsx b/web/src/icons/sprite.tsx new file mode 100644 index 0000000..1f4e40a --- /dev/null +++ b/web/src/icons/sprite.tsx @@ -0,0 +1,51 @@ +export function IconSprite() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/web/tests/component/Icon.test.tsx b/web/tests/component/Icon.test.tsx new file mode 100644 index 0000000..145b1fe --- /dev/null +++ b/web/tests/component/Icon.test.tsx @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '../_helpers/render'; +import { Icon } from '@/icons/Icon'; +import { IconSprite } from '@/icons/sprite'; + +describe('', () => { + it('renders the named SVG via ', () => { + render(<>); + const svg = screen.getByRole('img', { hidden: true }); + expect(svg.querySelector('use')?.getAttribute('href')).toBe('#i-check'); + }); + + it('inherits currentColor', () => { + render(<>); + const svg = screen.getByRole('img', { hidden: true }); + expect(svg.getAttribute('width')).toBe('14'); + }); +}); From d4abc315e4d6a6f551d53e576f0ff70375624877 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 03:52:35 +0000 Subject: [PATCH 2/5] feat(web): add + ); +} diff --git a/web/tests/component/Button.test.tsx b/web/tests/component/Button.test.tsx new file mode 100644 index 0000000..9106b4c --- /dev/null +++ b/web/tests/component/Button.test.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '../_helpers/render'; +import { Button } from '@/components/Button'; + +describe('); + expect(screen.getByRole('button')).toHaveTextContent('Click me'); + }); + + it('default variant uses ink-on-bg style (data-variant="primary")', () => { + render(); + expect(screen.getByRole('button')).toHaveAttribute('data-variant', 'primary'); + }); + + it('respects variant="secondary"', () => { + render(); + expect(screen.getByRole('button')).toHaveAttribute('data-variant', 'secondary'); + }); + + it('respects variant="ghost"', () => { + render(); + expect(screen.getByRole('button')).toHaveAttribute('data-variant', 'ghost'); + }); + + it('disabled state is non-interactive', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); From 717f9a7d582ec4d1fe36fd14370b6a1e095e5cb2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 03:52:35 +0000 Subject: [PATCH 3/5] feat(web): add + primitives with semantic variants --- web/src/components/Pill.tsx | 39 +++++++++++++++++++++++++++++++ web/src/components/Tag.tsx | 33 ++++++++++++++++++++++++++ web/tests/component/Pill.test.tsx | 25 ++++++++++++++++++++ web/tests/component/Tag.test.tsx | 20 ++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 web/src/components/Pill.tsx create mode 100644 web/src/components/Tag.tsx create mode 100644 web/tests/component/Pill.test.tsx create mode 100644 web/tests/component/Tag.test.tsx diff --git a/web/src/components/Pill.tsx b/web/src/components/Pill.tsx new file mode 100644 index 0000000..2b2afed --- /dev/null +++ b/web/src/components/Pill.tsx @@ -0,0 +1,39 @@ +import type { CSSProperties, ReactNode } from 'react'; + +type Kind = 'running' | 'paused' | 'error' | 'resolved' | 'neutral'; + +interface PillProps { + kind: Kind; + children: ReactNode; +} + +const baseStyle: CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + gap: 4, + padding: '2px 8px', + fontFamily: 'var(--ff-sans)', + fontSize: 'var(--t-micro)', + fontWeight: 500, + letterSpacing: '0.08em', + textTransform: 'uppercase', + border: '1px solid var(--hair)', + borderRadius: 0, + whiteSpace: 'nowrap', +}; + +const kindStyles: Record = { + running: { color: 'var(--acc)', borderColor: 'var(--acc-mid)', background: 'var(--acc-soft)' }, + paused: { color: 'var(--warn)', borderColor: 'var(--warn)', background: 'var(--warn-bg)' }, + error: { color: 'var(--danger)', borderColor: 'var(--danger)', background: 'var(--danger-bg)' }, + resolved: { color: 'var(--good)', borderColor: 'var(--good)', background: 'var(--good-bg)' }, + neutral: { color: 'var(--ink-3)', borderColor: 'var(--hair-strong)', background: 'var(--bg-subtle)' }, +}; + +export function Pill({ kind, children }: PillProps) { + return ( + + {children} + + ); +} diff --git a/web/src/components/Tag.tsx b/web/src/components/Tag.tsx new file mode 100644 index 0000000..c2b7b98 --- /dev/null +++ b/web/src/components/Tag.tsx @@ -0,0 +1,33 @@ +import type { CSSProperties, ReactNode } from 'react'; + +type Variant = 'default' | 'mono'; + +interface TagProps { + variant?: Variant; + children: ReactNode; +} + +const baseStyle: CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + padding: '1px 6px', + fontSize: 'var(--t-meta)', + color: 'var(--ink-2)', + border: '1px solid var(--hair)', + background: 'var(--bg-subtle)', + borderRadius: 0, + whiteSpace: 'nowrap', +}; + +const variantStyles: Record = { + default: { fontFamily: 'var(--ff-sans)' }, + mono: { fontFamily: 'var(--ff-mono)' }, +}; + +export function Tag({ variant = 'default', children }: TagProps) { + return ( + + {children} + + ); +} diff --git a/web/tests/component/Pill.test.tsx b/web/tests/component/Pill.test.tsx new file mode 100644 index 0000000..651ae6f --- /dev/null +++ b/web/tests/component/Pill.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '../_helpers/render'; +import { Pill } from '@/components/Pill'; + +describe('', () => { + it('renders SHORT_CAPS label text', () => { + render(Running); + // Implementation transforms via CSS text-transform: uppercase; + // the underlying text node should still contain the literal children. + expect(screen.getByText('Running')).toBeInTheDocument(); + }); + + it('exposes the kind via data-kind', () => { + render(Error); + expect(screen.getByText('Error').closest('[data-kind]')).toHaveAttribute('data-kind', 'error'); + }); + + it.each(['running', 'paused', 'error', 'resolved', 'neutral'] as const)( + 'accepts kind="%s"', + (kind) => { + render(label); + expect(screen.getByText('label').closest('[data-kind]')).toHaveAttribute('data-kind', kind); + }, + ); +}); diff --git a/web/tests/component/Tag.test.tsx b/web/tests/component/Tag.test.tsx new file mode 100644 index 0000000..3847025 --- /dev/null +++ b/web/tests/component/Tag.test.tsx @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '../_helpers/render'; +import { Tag } from '@/components/Tag'; + +describe('', () => { + it('renders children text', () => { + render(hello); + expect(screen.getByText('hello')).toBeInTheDocument(); + }); + + it('defaults to variant="default"', () => { + render(x); + expect(screen.getByText('x').closest('[data-variant]')).toHaveAttribute('data-variant', 'default'); + }); + + it('accepts variant="mono"', () => { + render(slack.post_message); + expect(screen.getByText('slack.post_message').closest('[data-variant]')).toHaveAttribute('data-variant', 'mono'); + }); +}); From eaab9afa0848deb675901ec094715f00f2423bc2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 03:54:06 +0000 Subject: [PATCH 4/5] feat(web): add /