From 25fd0ea9fe000b81a5eed19073bc8331843803fb Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 26 May 2026 23:01:09 +0000 Subject: [PATCH] feat: add Callout component with 5 types, semantic HTML, and design tokens --- src/components/Callout.astro | 62 +++++++++++++++ tests/callout.test.mjs | 142 +++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/components/Callout.astro create mode 100644 tests/callout.test.mjs diff --git a/src/components/Callout.astro b/src/components/Callout.astro new file mode 100644 index 0000000..4ff05ce --- /dev/null +++ b/src/components/Callout.astro @@ -0,0 +1,62 @@ +--- +interface Props { + type?: 'note' | 'pattern' | 'antipattern' | 'warning' | 'tip'; +} + +const { type = 'note' } = Astro.props; + +const config: Record = { + note: { borderColor: 'var(--border-default)', labelColor: 'var(--text-secondary)', label: 'Note' }, + pattern: { borderColor: 'var(--accent-primary)', labelColor: 'var(--accent-primary)', label: 'Pattern' }, + antipattern: { borderColor: 'var(--error-border)', labelColor: 'var(--error-text)', label: 'Anti-pattern' }, + warning: { borderColor: 'var(--warning-border)', labelColor: 'var(--warning-text)', label: 'Warning' }, + tip: { borderColor: 'var(--accent-secondary)', labelColor: 'var(--accent-secondary)', label: 'Tip' }, +}; + +const { borderColor, labelColor, label } = config[type]; +--- + + + + \ No newline at end of file diff --git a/tests/callout.test.mjs b/tests/callout.test.mjs new file mode 100644 index 0000000..7f0ba06 --- /dev/null +++ b/tests/callout.test.mjs @@ -0,0 +1,142 @@ +import { describe, it } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const componentPath = resolve('src/components/Callout.astro'); + +function readComponent() { + return readFileSync(componentPath, 'utf-8'); +} + +describe('Callout component', () => { + it('should exist at src/components/Callout.astro', () => { + // Will throw if file doesn't exist + const content = readComponent(); + assert.ok(content.length > 0, 'Component file should not be empty'); + }); + + describe('Props interface', () => { + it('should define type prop with 5 allowed values', () => { + const content = readComponent(); + assert.match(content, /type\?/, 'type should be an optional prop'); + assert.match(content, /note/, 'should include note type'); + assert.match(content, /pattern/, 'should include pattern type'); + assert.match(content, /antipattern/, 'should include antipattern type'); + assert.match(content, /warning/, 'should include warning type'); + assert.match(content, /tip/, 'should include tip type'); + }); + + it('should default type to note', () => { + const content = readComponent(); + assert.match(content, /=\s*['"]note['"]/, 'type should default to note'); + }); + }); + + describe('Config record mapping', () => { + it('should map note to border-default, text-secondary, Note', () => { + const content = readComponent(); + assert.match(content, /border-default/, 'note should use border-default'); + assert.match(content, /text-secondary/, 'note should use text-secondary'); + }); + + it('should map pattern to accent-primary, accent-primary, Pattern', () => { + const content = readComponent(); + assert.match(content, /accent-primary/, 'pattern should use accent-primary'); + }); + + it('should map antipattern to error-border, error-text, Anti-pattern', () => { + const content = readComponent(); + assert.match(content, /error-border/, 'antipattern should use error-border'); + assert.match(content, /error-text/, 'antipattern should use error-text'); + assert.match(content, /Anti-pattern/, 'antipattern should have Anti-pattern label'); + }); + + it('should map warning to warning-border, warning-text, Warning', () => { + const content = readComponent(); + assert.match(content, /warning-border/, 'warning should use warning-border'); + assert.match(content, /warning-text/, 'warning should use warning-text'); + }); + + it('should map tip to accent-secondary, accent-secondary, Tip', () => { + const content = readComponent(); + assert.match(content, /accent-secondary/, 'tip should use accent-secondary'); + }); + }); + + describe('Semantic HTML', () => { + it('should use aside element with role=note', () => { + const content = readComponent(); + assert.match(content, /