Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions RELEASE-QUALIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ This milestone qualifies the public kit and its private workbench consumer for r
- Hosted evidence covers the full current qualification request, authentication, and same-origin font loading. A single-widget adapter check is insufficient.
- Published versions, Git commits, artifact checksums, remote CI, and local CLI version are reported separately.

## Current mathematical baseline

The audited layout model records each component's declared text behavior: single line, wrapping, wrapping anywhere, or managed scrolling. It retains the full single-line estimate and checks every requested width. Wrapping controls use their authored minimum block size; an explicit height or line cap still applies when declared.

The built CLI passes all 180 fixtures for `qualification/original-project.json` and all 720 fixtures across the 152 component-theme matrix runs. Both strict commands exit successfully. `qualification/reviewed-math-policy.json` pins the exact case inventory, width verdicts, layout assumptions, run results, and totals. The release guard rejects new failures, changed case inputs, incomplete evidence, or an exit status that disagrees with the artifact.

The previous 17 project and 45 matrix findings remain in `qualification/reviewed-math-policy-20261004.json` as historical results from the earlier single-line model. Current passing mathematical estimates do not establish rendered behavior or accessibility conformance.

Run `pnpm check:release:math` after building the CLI. Run the direct guard regressions with `node --test scripts/assess-release-math.test.mjs` against the same build.

## Ownership

- CLI review: public math, fixture contracts, receipt contracts, and CLI.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/proof-driven-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ dk audit --css app.css --json --strict > audit.json

Component fixtures evaluate each requested layout width and declared color-distinctness check. Coverage reports declared, evaluated, and unsupported proof kinds. A passing wide layout does not cancel a failing narrow layout.

Layout defaults to a single line. Recipes can declare `textBehavior` as `wrap`, `wrap-anywhere`, or `scroll` when the component supports that behavior. The results retain the full single-line estimate and report the minimum inline space required after padding, icons, and other reserved controls. Wrapped estimates also report line count and height at each width. Explicit `heights` and `maxLines` budgets can still fail; a control with a minimum height and natural block growth can omit those limits. Scrolling checks the text viewport, not full value visibility.

## Interpret the result

Mathematical evidence includes the following results:
Expand Down
32 changes: 27 additions & 5 deletions examples/sveltekit-starter/tests/desk.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,34 @@ test('public components hydrate and complete release and theme flows', async ({
page.on('pageerror', (error) => errors.push(error.message));
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Releases', exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Create release' }).click();
await expect(page.getByText('Correct the marked fields.')).toBeVisible();
await page.getByRole('textbox', { name: 'Release name', exact: true }).fill('Browser release');
await page.getByRole('textbox', { name: 'Release owner', exact: true }).fill('Rafi');
await page.getByRole('button', { name: 'Create release' }).click();
const releaseName = page.getByRole('textbox', { name: 'Release name', exact: true });
const releaseOwner = page.getByRole('textbox', { name: 'Release owner', exact: true });
const createRelease = page.getByRole('button', { name: 'Create release' });
const initialRows = await page.locator('tbody tr').allTextContents();
/** @type {string[]} */
const createRequests = [];
page.on('request', (request) => {
if (request.method() === 'POST' && new URL(request.url()).search === '?/create') {
createRequests.push(request.url());
}
});
await createRelease.click();
await expect(releaseName).toBeFocused();
expect(await releaseName.evaluate((input) => input instanceof HTMLInputElement && input.validity.valueMissing)).toBe(true);
expect(await releaseOwner.evaluate((input) => input instanceof HTMLInputElement && input.validity.valueMissing)).toBe(true);
expect(createRequests).toEqual([]);
expect(await page.locator('tbody tr').allTextContents()).toEqual(initialRows);
await releaseName.fill('Browser release');
await createRelease.click();
await expect(releaseOwner).toBeFocused();
await expect(releaseName).toHaveValue('Browser release');
expect(await releaseOwner.evaluate((input) => input instanceof HTMLInputElement && input.validity.valueMissing)).toBe(true);
expect(createRequests).toEqual([]);
expect(await page.locator('tbody tr').allTextContents()).toEqual(initialRows);
await releaseOwner.fill('Rafi');
await createRelease.click();
await expect(page.getByRole('status')).toHaveText('Created Browser release.');
expect(createRequests).toHaveLength(1);
await page.getByRole('row').filter({ hasText: 'Browser release' }).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Review release' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"preflight": "pnpm lint && pnpm check:strict && pnpm test && pnpm test:coverage",
"verify": "pnpm release:verify",
"check:component-targets": "node ./scripts/check-component-targets.mjs && node ./scripts/check-component-narrow-layout.mjs",
"check:release:math": "node ./scripts/assess-release-math.mjs"
"check:release:math": "node --test ./scripts/assess-release-math.test.mjs && node ./scripts/assess-release-math.mjs"
},
"devDependencies": {
"@changesets/cli": "^2.29.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { describe, expect, it } from 'vitest';
import { createTheme } from '@dkcli/tokens';

import { createAccordionRegistration, getAccordionRecipeCase } from './accordion.recipe.js';
import { expectKnownLayoutFailures } from '../test-utils/proof-expectations.js';
import { DECLARED_LAYOUT_BEHAVIORS, expectDeclaredLayoutBehavior } from '../test-utils/proof-expectations.js';

describe('accordion recipe', () => {
it('compiles light and dark recipe cases and reports the known narrow-width estimate', () => {
it('compiles light and dark cases with declared panel wrapping and height budget', () => {
const light = createTheme({
name: 'accordion-light',
seed: {
Expand All @@ -31,7 +31,7 @@ describe('accordion recipe', () => {
for (const theme of [light, dark]) {
const registration = createAccordionRegistration(theme);
expect(getAccordionRecipeCase(registration.recipe, { size: 'md' }).caseKey).toContain('size=md');
expectKnownLayoutFailures(registration.recipe.proofFixtures, 2, [{ name: 'accordion-md-open', widths: [320] }]);
expectDeclaredLayoutBehavior(registration.recipe.proofFixtures, 2, DECLARED_LAYOUT_BEHAVIORS.accordion);
}
});
});
6 changes: 3 additions & 3 deletions packages/components/src/lib/accordion/accordion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ export const accordionSpec: ComponentSpec = createComponentSpec({
widths: [320, 420],
heights: [160],
noOverflow: true,
textBehavior: 'wrap-anywhere',
lineHeight: 1.6,
blockSize: literal('128px'),
inlinePadding: slotVar('panel', '--dk-accordion-panel-padding'),
gap: slotVar('trigger', '--dk-accordion-trigger-gap'),
labelFontSize: slotVar('trigger', '--dk-accordion-trigger-size'),
iconSize: slotVar('indicator', '--dk-accordion-indicator-size')
labelFontSize: slotVar('panel', '--dk-accordion-panel-size')
}
},
proofCases: [
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/lib/badge/badge.recipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { createTheme } from '@dkcli/tokens';

import { createBadgeRegistration, getBadgeRecipeCase } from './badge.recipe.js';
import { BADGE_LAYOUT_FAILURES, expectKnownLayoutFailures } from '../test-utils/proof-expectations.js';
import { DECLARED_LAYOUT_BEHAVIORS, expectDeclaredLayoutBehavior } from '../test-utils/proof-expectations.js';

describe('badge recipe', () => {
it('compiles all tone, emphasis, and size cases across light and dark themes', () => {
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('badge recipe', () => {
expect(compiledCase.slots.root.baseVars['--dk-badge-bg']).toMatch(/^#/);
expect(compiledCase.slots.label.baseVars['--dk-badge-label-size']).toContain('clamp');
for (const registration of [lightRegistration, darkRegistration]) {
expectKnownLayoutFailures(registration.recipe.proofFixtures, 15, BADGE_LAYOUT_FAILURES);
expectDeclaredLayoutBehavior(registration.recipe.proofFixtures, 15, DECLARED_LAYOUT_BEHAVIORS.badge);
}
});
});
4 changes: 3 additions & 1 deletion packages/components/src/lib/badge/badge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ export const badgeSpec: ComponentSpec = createComponentSpec({
layout: {
target: 'root',
widths: [120, 200],
heights: [30],
// The badge has a minimum block size and grows when its label wraps.
noOverflow: true,
textBehavior: 'wrap-anywhere',
lineHeight: 1,
blockSize: slotVar('root', '--dk-badge-block-size'),
inlinePadding: slotVar('root', '--dk-badge-inline-padding'),
gap: slotVar('root', '--dk-badge-gap'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { describe, expect, it } from 'vitest';
import { createTheme } from '@dkcli/tokens';

import { createBreadcrumbsRegistration, getBreadcrumbsRecipeCase } from './breadcrumbs.recipe.js';
import { expectKnownLayoutFailures } from '../test-utils/proof-expectations.js';
import { DECLARED_LAYOUT_BEHAVIORS, expectDeclaredLayoutBehavior } from '../test-utils/proof-expectations.js';

describe('breadcrumbs recipe', () => {
it('compiles light and dark breadcrumb cases and reports the known narrow-width estimate', () => {
it('compiles light and dark cases with declared wrapping across breadcrumb items', () => {
const themes = [
createTheme({
name: 'breadcrumbs-light',
Expand All @@ -33,7 +33,7 @@ describe('breadcrumbs recipe', () => {
for (const theme of themes) {
const registration = createBreadcrumbsRegistration(theme);
expect(getBreadcrumbsRecipeCase(registration.recipe, { size: 'md' }).caseKey).toContain('size=md');
expectKnownLayoutFailures(registration.recipe.proofFixtures, 1, [{ name: 'breadcrumbs-md', widths: [220] }]);
expectDeclaredLayoutBehavior(registration.recipe.proofFixtures, 1, DECLARED_LAYOUT_BEHAVIORS.breadcrumbs);
}
});
});
5 changes: 4 additions & 1 deletion packages/components/src/lib/breadcrumbs/breadcrumbs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ export const breadcrumbsSpec: ComponentSpec = createComponentSpec({
layout: {
target: 'root',
widths: [220, 320],
heights: [48],
// The item row wraps naturally; its minimum block size is not a height cap.
noOverflow: true,
textBehavior: 'wrap',
// Two delivered separators reserve at most 12px each in the declared sample.
reservedInlineSize: literal(24),
blockSize: slotVar('item', '--dk-breadcrumbs-item-block-size'),
inlinePadding: slotVar('item', '--dk-breadcrumbs-item-inline-padding'),
gap: slotVar('root', '--dk-breadcrumbs-gap'),
Expand Down
6 changes: 3 additions & 3 deletions packages/components/src/lib/button/button.recipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { createTheme } from '@dkcli/tokens';

import { createButtonRegistration, getButtonRecipeCase } from './button.recipe.js';
import { BUTTON_LAYOUT_FAILURES, expectKnownLayoutFailures } from '../test-utils/proof-expectations.js';
import { DECLARED_LAYOUT_BEHAVIORS, expectDeclaredLayoutBehavior } from '../test-utils/proof-expectations.js';

describe('button recipe', () => {
it('compiles the full button case matrix for light and dark themes', () => {
Expand Down Expand Up @@ -51,12 +51,12 @@ describe('button recipe', () => {
expect(darkCase.slots.root.stateVars.hover?.['--dk-button-bg']).toMatch(/^#/);
});

it('preserves curated fixtures and reports the two narrow-width mathematical failures', () => {
it('preserves curated fixtures with declared label wrapping and natural block growth', () => {
const registration = createButtonRegistration();

expect(registration.recipe.proofFixtures.length).toBeGreaterThan(0);
expect(registration.recipe.proofFixtures.every((fixture) => fixture.resolved)).toBe(true);
expectKnownLayoutFailures(registration.recipe.proofFixtures, 13, BUTTON_LAYOUT_FAILURES);
expectDeclaredLayoutBehavior(registration.recipe.proofFixtures, 13, DECLARED_LAYOUT_BEHAVIORS.button);
expect(registration.recipe.proofFixtures.some((fixture) => fixture.props.as === 'a')).toBe(true);
expect(registration.recipe.proofFixtures.some((fixture) => fixture.states.includes('loading'))).toBe(true);
});
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/lib/button/button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,10 @@ export const buttonSpec: ComponentSpec = createComponentSpec({
layout: {
target: 'root',
widths: [180, 240, 320],
heights: [44, 48, 52],
// Size presets define minimum block sizes; complete labels may add lines.
noOverflow: true,
textBehavior: 'wrap-anywhere',
lineHeight: 1.1,
inlinePadding: slotVar('root', '--dk-button-inline-padding'),
gap: slotVar('root', '--dk-button-gap'),
labelFontSize: slotVar('label', '--dk-button-label-font-size'),
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/lib/checkbox/Checkbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
{name}
id={fieldId}
bind:checked
{required}
{disabled}
aria-label={label}
aria-checked={indeterminate ? 'mixed' : checked ? 'true' : 'false'}
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/lib/checkbox/checkbox.recipe.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import { createCheckboxRegistration, getCheckboxRecipeCase } from './checkbox.recipe.js';
import { CHECKBOX_LAYOUT_FAILURES, expectKnownLayoutFailures } from '../test-utils/proof-expectations.js';
import { DECLARED_LAYOUT_BEHAVIORS, expectDeclaredLayoutBehavior } from '../test-utils/proof-expectations.js';

describe('checkbox recipe', () => {
it('compiles checkbox cases and proof fixtures', () => {
Expand All @@ -10,6 +10,6 @@ describe('checkbox recipe', () => {
expect(Object.keys(registration.recipe.cases)).toHaveLength(3);
const compiledCase = getCheckboxRecipeCase(registration.recipe, { size: 'md' });
expect(compiledCase.slots.control.baseVars['--dk-checkbox-bg']).toMatch(/^#/);
expectKnownLayoutFailures(registration.recipe.proofFixtures, 9, CHECKBOX_LAYOUT_FAILURES);
expectDeclaredLayoutBehavior(registration.recipe.proofFixtures, 9, DECLARED_LAYOUT_BEHAVIORS.checkbox);
});
});
6 changes: 5 additions & 1 deletion packages/components/src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,12 @@ export const checkboxSpec: ComponentSpec = createComponentSpec({
layout: {
target: 'root',
widths: [240, 320],
heights: [44],
// The 44px hit-size minimum does not cap a multiline label's height.
noOverflow: true,
textBehavior: 'wrap',
lineHeight: 1.35,
// The largest 22px control plus both 1.5px borders is persistent chrome.
reservedInlineSize: literal(25),
blockSize: slotVar('root', '--dk-checkbox-hit-size'),
gap: slotVar('root', '--dk-checkbox-gap'),
labelFontSize: slotVar('label', '--dk-checkbox-label-size')
Expand Down
9 changes: 9 additions & 0 deletions packages/components/src/lib/combobox/Combobox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
type Placement
} from '../internal/behavior/index.js';
import { FieldFrame } from '../primitives/index.js';
import { requiredSelection } from '../internal/behavior/required-selection.js';
import {
DEFAULT_COMBOBOX_THEME,
createComboboxRegistration,
Expand Down Expand Up @@ -91,6 +92,12 @@
if (disabled) internalOpen = false;
});

$effect(() => {
if (internalOpen && (!filteredItems[highlightIndex] || filteredItems[highlightIndex].disabled)) {
highlightIndex = firstEnabledIndex(filteredItems);
}
});

async function syncPosition(): Promise<void> {
await tick();
if (!internalOpen || !inputEl || !surfaceEl) return;
Expand Down Expand Up @@ -204,6 +211,7 @@
<div class="combobox-trigger" style={`${slotStyles.input}; ${slotStyles.icon}`}>
<input
bind:this={inputEl}
use:requiredSelection={{ required, disabled, value }}
class="combobox-input"
style={slotStyles.input}
id={fieldId}
Expand All @@ -214,6 +222,7 @@
aria-controls={`${fieldId}-listbox`}
aria-describedby={describedBy}
aria-invalid={invalid ? 'true' : 'false'}
aria-required={required ? 'true' : undefined}
{disabled}
{placeholder}
value={displayQuery}
Expand Down
13 changes: 13 additions & 0 deletions packages/components/src/lib/combobox/Combobox.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import Combobox from './Combobox.svelte';
afterEach(() => document.querySelectorAll('[data-test-outside]').forEach((element) => element.remove()));

describe('Combobox', () => {
it('updates the active option when asynchronous results replace the list', async () => {
const onChange = vi.fn();
const { rerender } = render(Combobox, { props: { items: [{ value: 'one', label: 'One' }, { value: 'two', label: 'Two' }], onChange } });
const input = screen.getByRole('combobox');
input.focus();
await fireEvent.keyDown(input, { key: 'End' });
await rerender({ items: [{ value: 'three', label: 'Three' }] });
const option = screen.getByRole('option', { name: 'Three' });
expect(input.getAttribute('aria-activedescendant')).toBe(option.id);
await fireEvent.keyDown(input, { key: 'Enter' });
expect(onChange).toHaveBeenCalledExactlyOnceWith({ value: 'three' });
});

const items = [
{ value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,71 @@
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { tick } from 'svelte';

import CommandPalette from './CommandPalette.svelte';
import CommandPaletteFocusHarness from './CommandPaletteFocusHarness.svelte';

afterEach(() => {
document.querySelectorAll('[data-command-outside]').forEach((element) => element.remove());
vi.restoreAllMocks();
});

describe('CommandPalette', () => {
it('keeps a usable highlighted command after a controlled query narrows results', async () => {
const onAction = vi.fn();
const { rerender } = render(CommandPalette, { props: { open: true, items: [{ id: 'one', label: 'One' }, { id: 'two', label: 'Two' }], onAction } });
await tick();
const input = screen.getByRole('combobox');
await fireEvent.keyDown(input, { key: 'End' });
await rerender({ query: 'One' });
input.focus();
await fireEvent.keyDown(input, { key: 'Enter' });
expect(onAction).toHaveBeenCalledExactlyOnceWith({ id: 'one' });
});

it('focuses an initially open palette and traps Tab inside its modal surface', async () => {
render(CommandPalette, { props: { open: true, items: [{ id: 'first', label: 'First' }, { id: 'last', label: 'Last' }] } });
await tick();
const input = screen.getByRole('combobox');
expect(document.activeElement).toBe(input);
await fireEvent.keyDown(input, { key: 'Tab', shiftKey: true });
const last = screen.getByRole('option', { name: 'Last' });
expect(document.activeElement).toBe(last);
await fireEvent.keyDown(last, { key: 'Tab' });
expect(document.activeElement).toBe(input);
});

it('dismisses after an outside click without stealing its new focus', async () => {
let now = 0;
vi.spyOn(performance, 'now').mockImplementation(() => now);
render(CommandPaletteFocusHarness);
const opener = screen.getByRole('button', { name: 'Open commands' });
opener.focus();
await fireEvent.click(opener);
await tick();
now = 200;
const outside = document.createElement('button');
outside.dataset.commandOutside = 'true';
document.body.append(outside);
outside.focus();
await fireEvent.click(outside);
await tick();
expect(screen.queryByRole('dialog')).toBeNull();
expect(document.activeElement).toBe(outside);
});

it('keeps Tab navigation inside an open modal palette', async () => {
render(CommandPalette, { props: { open: true, items: [{ id: 'last', label: 'Last' }] } });
await tick();
const input = screen.getByRole('combobox');
input.focus();
await fireEvent.keyDown(input, { key: 'Tab', shiftKey: true });
const last = screen.getByRole('option', { name: 'Last' });
expect(document.activeElement).toBe(last);
await fireEvent.keyDown(last, { key: 'Tab' });
expect(document.activeElement).toBe(input);
});

it('returns focus to the external opener after Escape closes a bound palette', async () => {
render(CommandPaletteFocusHarness);

Expand Down
Loading
Loading