From a23b12ba1aadbd3882895d90f14a0efad0e91fb2 Mon Sep 17 00:00:00 2001 From: Sebastian Enns Date: Mon, 28 Sep 2026 11:59:04 +0200 Subject: [PATCH 1/8] feat(sushi): support custom dialog coordinates --- .../app/generated/api-reference.generated.ts | 42 +++++++++++++++++-- .../src/app/pages/dialog/dialog.page.html | 12 ++++++ .../src/app/pages/dialog/dialog.page.ts | 7 +++- .../coordinates/coordinates.example.html | 23 ++++++++++ .../coordinates/coordinates.example.ts | 12 ++++++ sushi/src/lib/dialog/dialog.component.ts | 35 ++++++++++++++-- sushi/src/lib/dialog/dialog.interfaces.ts | 16 ++++++- 7 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 playground/src/app/pages/dialog/examples/coordinates/coordinates.example.html create mode 100644 playground/src/app/pages/dialog/examples/coordinates/coordinates.example.ts diff --git a/playground/src/app/generated/api-reference.generated.ts b/playground/src/app/generated/api-reference.generated.ts index 8e53d59..4093892 100644 --- a/playground/src/app/generated/api-reference.generated.ts +++ b/playground/src/app/generated/api-reference.generated.ts @@ -2960,7 +2960,8 @@ export const apiReference: Readonly<{ kind: 'input', type: 'DialogPosition', defaultValue: "'center'", - description: 'Places the Dialog at a viewport edge or corner.', + description: + 'Named placement or viewport offsets (numbers in pixels, strings in CSS units). Defaults to center. Changes reposition an open Dialog; reopening restores this position after dragging. Custom offsets must leave room for the Dialog.', }, { name: 'ariaLabel', @@ -3067,13 +3068,48 @@ export const apiReference: Readonly<{ members: [], typeParameters: [], }, + { + name: 'DialogCoordinates', + kind: 'interface', + source: 'library', + declaration: + 'interface DialogCoordinates {\n readonly top?: number | string;\n readonly right?: number | string;\n readonly bottom?: number | string;\n readonly left?: number | string;\n}', + description: 'Viewport offsets. Numbers are pixels; strings are CSS lengths. Omitted edges are automatic.', + members: [ + { + name: 'top', + type: 'number | string', + optional: true, + description: 'Distance from the top edge.', + }, + { + name: 'right', + type: 'number | string', + optional: true, + description: 'Distance from the right edge.', + }, + { + name: 'bottom', + type: 'number | string', + optional: true, + description: 'Distance from the bottom edge.', + }, + { + name: 'left', + type: 'number | string', + optional: true, + description: 'Distance from the left edge.', + }, + ], + typeParameters: [], + }, { name: 'DialogPosition', kind: 'type', source: 'library', declaration: - "type DialogPosition = 'center' | 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right';", - description: 'Placement of a Dialog inside the viewport.', + "type DialogPosition = 'center' | 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | DialogCoordinates;", + description: 'Named placement or custom viewport offsets for a Dialog.', members: [], typeParameters: [], }, diff --git a/playground/src/app/pages/dialog/dialog.page.html b/playground/src/app/pages/dialog/dialog.page.html index 7d06448..1dfde2e 100644 --- a/playground/src/app/pages/dialog/dialog.page.html +++ b/playground/src/app/pages/dialog/dialog.page.html @@ -31,6 +31,18 @@

Movable workspace

> +
+
+

Custom coordinates

+

+ Pass top, right, bottom, or left offsets to position. Numbers use pixels; strings accept CSS units. Keep enough space for the dialog + inside the viewport. Updating position moves an open dialog and resets its drag offset. +

+
+ + + +

Form dialog

diff --git a/playground/src/app/pages/dialog/dialog.page.ts b/playground/src/app/pages/dialog/dialog.page.ts index c136cbf..b08b810 100644 --- a/playground/src/app/pages/dialog/dialog.page.ts +++ b/playground/src/app/pages/dialog/dialog.page.ts @@ -8,6 +8,9 @@ import * as advancedCss from './examples/advanced/advanced.example.css' with { l import advancedHtml from './examples/advanced/advanced.example.html'; import * as advancedTs from './examples/advanced/advanced.example.ts' with { loader: 'text' }; import { DialogBasicExample } from './examples/basic/basic.example'; +import { DialogCoordinatesExample } from './examples/coordinates/coordinates.example'; +import coordinatesHtml from './examples/coordinates/coordinates.example.html'; +import * as coordinatesTs from './examples/coordinates/coordinates.example.ts' with { loader: 'text' }; import basicHtml from './examples/basic/basic.example.html'; import * as basicTs from './examples/basic/basic.example.ts' with { loader: 'text' }; import { DialogFormExample } from './examples/form/form.example'; @@ -24,6 +27,7 @@ import * as dynamicTs from './examples/dynamic/dynamic.example.ts' with { loader Badge, DialogAdvancedExample, DialogBasicExample, + DialogCoordinatesExample, DialogDynamicExample, DialogFormExample, ExampleCode, @@ -33,8 +37,9 @@ import * as dynamicTs from './examples/dynamic/dynamic.example.ts' with { loader changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogPage { - protected readonly examples: Readonly> = { + protected readonly examples: Readonly> = { basic: { html: basicHtml, typescript: textSource(basicTs) }, + coordinates: { html: coordinatesHtml, typescript: textSource(coordinatesTs) }, advanced: { css: textSource(advancedCss), html: advancedHtml, typescript: textSource(advancedTs) }, form: { css: textSource(formCss), html: formHtml, typescript: textSource(formTs) }, dynamic: { html: dynamicHtml, typescript: textSource(dynamicTs) }, diff --git a/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.html b/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.html new file mode 100644 index 0000000..d60971f --- /dev/null +++ b/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.html @@ -0,0 +1,23 @@ + + + +

Custom position

+
+

+ Starts 80 pixels from the top and 1.5rem from the right. Drag the header to move it. Close and reopen to restore + these offsets. +

+
+ + + +
+
+
diff --git a/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.ts b/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.ts new file mode 100644 index 0000000..effcfcc --- /dev/null +++ b/playground/src/app/pages/dialog/examples/coordinates/coordinates.example.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; +import { Button, Dialog, DialogBody, DialogHeader, DialogPosition, DialogTrigger } from '@sushi-kit/angular'; + +@Component({ + selector: 'pg-dialog-coordinates-example', + imports: [Button, Dialog, DialogBody, DialogHeader, DialogTrigger], + templateUrl: './coordinates.example.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DialogCoordinatesExample { + protected readonly coordinates: WritableSignal = signal({ top: 80, right: '1.5rem' }); +} diff --git a/sushi/src/lib/dialog/dialog.component.ts b/sushi/src/lib/dialog/dialog.component.ts index b5ae7c7..e98165e 100644 --- a/sushi/src/lib/dialog/dialog.component.ts +++ b/sushi/src/lib/dialog/dialog.component.ts @@ -4,6 +4,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, + computed, DestroyRef, ElementRef, inject, @@ -41,7 +42,7 @@ let nextDialogId: number = 0; [attr.aria-label]="ariaLabel()" [attr.aria-labelledby]="ariaLabelledby()" [attr.aria-describedby]="ariaDescribedby()" - [attr.data-position]="position()" + [attr.data-position]="namedPosition()" [attr.data-draggable]="draggable()" [id]="dialogId" (cancel)="handleCancel($event)" @@ -73,7 +74,7 @@ export class Dialog { public readonly closeOnBackdrop: InputSignalWithTransform = input(true, { transform: booleanAttribute, }); - /** Places the Dialog at a viewport edge or corner. */ + /** Named placement or viewport offsets (numbers in pixels, strings in CSS units). Defaults to center. Changes reposition an open Dialog; reopening restores this position after dragging. Custom offsets must leave room for the Dialog. */ public readonly position: InputSignal = input('center'); /** Accessible name used when no visible heading labels the Dialog. */ public readonly ariaLabel: InputSignal = input(null); @@ -97,12 +98,17 @@ export class Dialog { private readonly element: Signal> = viewChild.required>('nativeDialog'); + protected readonly namedPosition: Signal = computed((): string | null => { + const position: DialogPosition = this.position(); + return typeof position === 'string' ? position : null; + }); private readonly document: Document = inject(DOCUMENT); private readonly destroyRef: DestroyRef = inject(DestroyRef); private dragEnded: Subscription | null = null; private closeReason: DialogCloseReason = 'programmatic'; private readonly drag: Signal = viewChild.required(CdkDrag); private restoreTarget: HTMLElement | null = null; + private appliedPosition: DialogPosition | null = null; public constructor() { afterRenderEffect({ @@ -181,17 +187,38 @@ export class Dialog { private syncOpenState(): void { const dialog: HTMLDialogElement = this.element().nativeElement; - if (this.open() === dialog.open) return; + const position: DialogPosition = this.position(); + if (this.open() === dialog.open) { + if (dialog.open && position !== this.appliedPosition) this.applyPosition(); + return; + } if (this.open()) { this.restoreTarget ??= this.activeElement(); if (this.modal()) dialog.showModal(); else dialog.show(); - this.lockPosition(); + this.applyPosition(); } else { this.close(); } } + private applyPosition(): void { + this.drag().reset(); + this.resetPosition(); + const position: DialogPosition = this.position(); + if (typeof position !== 'string') { + const style: CSSStyleDeclaration = this.element().nativeElement.style; + style.inset = 'auto'; + style.margin = '0'; + for (const edge of ['top', 'right', 'bottom', 'left'] as const) { + const value: number | string | undefined = position[edge]; + if (value !== undefined) style[edge] = typeof value === 'number' ? `${value}px` : value; + } + } + this.lockPosition(); + this.appliedPosition = position; + } + private lockPosition(): void { const dialog: HTMLDialogElement = this.element().nativeElement; const bounds: DOMRect = dialog.getBoundingClientRect(); diff --git a/sushi/src/lib/dialog/dialog.interfaces.ts b/sushi/src/lib/dialog/dialog.interfaces.ts index 38ab6d1..93a49bf 100644 --- a/sushi/src/lib/dialog/dialog.interfaces.ts +++ b/sushi/src/lib/dialog/dialog.interfaces.ts @@ -1,9 +1,21 @@ /** Reason a Dialog closed. */ export type DialogCloseReason = 'backdrop' | 'close' | 'escape' | 'programmatic'; -/** Placement of a Dialog inside the viewport. */ +/** Viewport offsets. Numbers are pixels; strings are CSS lengths. Omitted edges are automatic. */ +export interface DialogCoordinates { + /** Distance from the top edge. */ + readonly top?: number | string; + /** Distance from the right edge. */ + readonly right?: number | string; + /** Distance from the bottom edge. */ + readonly bottom?: number | string; + /** Distance from the left edge. */ + readonly left?: number | string; +} + +/** Named placement or custom viewport offsets for a Dialog. */ export type DialogPosition = - 'center' | 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right'; + 'center' | 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | DialogCoordinates; /** Details emitted after a Dialog closes. */ export interface DialogCloseEvent { From c7d75e61b1e5a541f5e4b1ad7ad2ef50dc961426 Mon Sep 17 00:00:00 2001 From: Sebastian Enns Date: Mon, 28 Sep 2026 11:59:04 +0200 Subject: [PATCH 2/8] test(sushi): cover custom dialog positioning across viewports --- e2e/playwright.config.ts | 2 +- e2e/specs/dialog-position.spec.ts | 87 +++++++++++++++++++++++++++++++ e2e/specs/dialog.spec.ts | 47 +++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 e2e/specs/dialog-position.spec.ts diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index cebb9c3..e5a93ca 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ }, { name: 'mobile-webkit', - testMatch: ['**/responsive-layout.spec.ts', '**/mobile-overlay.spec.ts'], + testMatch: ['**/responsive-layout.spec.ts', '**/mobile-overlay.spec.ts', '**/dialog-position.spec.ts'], use: { ...devices['iPhone 13'] }, }, ], diff --git a/e2e/specs/dialog-position.spec.ts b/e2e/specs/dialog-position.spec.ts new file mode 100644 index 0000000..aced7c5 --- /dev/null +++ b/e2e/specs/dialog-position.spec.ts @@ -0,0 +1,87 @@ +import { expect, Locator, Page, test } from '@playwright/test'; + +interface Geometry { + readonly left: number; + readonly top: number; + readonly right: number; + readonly bottom: number; + readonly width: number; + readonly height: number; + readonly viewportWidth: number; + readonly viewportHeight: number; + readonly documentWidth: number; + readonly rem: number; +} + +async function geometry(dialog: Locator): Promise { + return dialog.evaluate((element: HTMLElement): Geometry => { + const bounds: DOMRect = element.getBoundingClientRect(); + const root: HTMLElement = document.documentElement; + return { + left: bounds.left, + top: bounds.top, + right: bounds.right, + bottom: bounds.bottom, + width: bounds.width, + height: bounds.height, + viewportWidth: root.clientWidth, + viewportHeight: root.clientHeight, + documentWidth: root.scrollWidth, + rem: Number.parseFloat(getComputedStyle(root).fontSize), + }; + }); +} + +async function activate(control: Locator, isMobile: boolean): Promise { + if (isMobile) await control.tap(); + else await control.click(); +} + +async function expectPosition(dialog: Locator, centered: boolean): Promise { + await expect(async (): Promise => { + const bounds: Geometry = await geometry(dialog); + const details: string = `Dialog geometry: ${JSON.stringify(bounds)}`; + expect(Math.round(bounds.left), details).toBeGreaterThanOrEqual(0); + expect(Math.round(bounds.top), details).toBeGreaterThanOrEqual(0); + expect(Math.round(bounds.right), details).toBeLessThanOrEqual(bounds.viewportWidth); + expect(Math.round(bounds.bottom), details).toBeLessThanOrEqual(bounds.viewportHeight); + expect(bounds.documentWidth, details).toBeLessThanOrEqual(bounds.viewportWidth); + if (centered) { + expect(bounds.left + bounds.width / 2, details).toBeCloseTo(bounds.viewportWidth / 2, 0); + expect(bounds.top + bounds.height / 2, details).toBeCloseTo(bounds.viewportHeight / 2, 0); + } else { + // The example explicitly requests these offsets; this is not a decoration measurement. + expect(bounds.top, details).toBeCloseTo(80, 0); + expect(bounds.viewportWidth - bounds.right, details).toBeCloseTo(1.5 * bounds.rem, 0); + } + }).toPass({ timeout: 5_000 }); +} + +for (const width of [360, 390]) { + test(`custom and named Dialog positions stay contained at ${width}px`, async ({ + page, + isMobile, + }: { + page: Page; + isMobile: boolean; + }): Promise => { + await page.setViewportSize({ width, height: 740 }); + await page.goto('/dialog'); + const trigger: Locator = page.getByRole('button', { name: 'Open positioned dialog' }); + const dialog: Locator = page.getByRole('dialog', { name: 'Custom position' }); + await activate(trigger, isMobile); + await expect(dialog).toBeVisible(); + await expectPosition(dialog, false); + + await activate(dialog.getByRole('button', { name: 'Center dialog' }), isMobile); + await expectPosition(dialog, true); + await activate(dialog.getByRole('button', { name: 'Restore offsets' }), isMobile); + await expectPosition(dialog, false); + + await activate(dialog.getByRole('button', { name: 'Close', exact: true }), isMobile); + await expect(dialog).toBeHidden(); + await expect(trigger).toBeFocused(); + await activate(trigger, isMobile); + await expectPosition(dialog, false); + }); +} diff --git a/e2e/specs/dialog.spec.ts b/e2e/specs/dialog.spec.ts index c6808a9..4057260 100644 --- a/e2e/specs/dialog.spec.ts +++ b/e2e/specs/dialog.spec.ts @@ -113,6 +113,53 @@ test('enables native bidirectional resizing', async ({ page }: { page: Page }): await expect(dialog).toHaveCSS('resize', 'both'); }); +test('custom coordinates are restored when reopening after dragging', async ({ page }: { page: Page }): Promise => { + const trigger: Locator = page.getByRole('button', { name: 'Open positioned dialog' }); + const dialog: Locator = page.getByRole('dialog', { name: 'Custom position' }); + await trigger.click(); + const initial: Bounds | null = await dialog.boundingBox(); + if (!initial) throw new Error('Expected a visible custom-positioned Dialog.'); + // These offsets are explicitly configured by the public API, not incidental layout pixels. + await expect(async (): Promise => { + const bounds: Bounds | null = await dialog.boundingBox(); + if (!bounds) throw new Error('Expected a visible custom-positioned Dialog.'); + const viewportWidth: number = await page.evaluate((): number => document.documentElement.clientWidth); + const rem: number = await page.evaluate((): number => Number.parseFloat(getComputedStyle(document.documentElement).fontSize)); + expect(bounds.y).toBeCloseTo(80, 0); + expect(viewportWidth - bounds.x - bounds.width).toBeCloseTo(1.5 * rem, 0); + }).toPass({ timeout: 5_000 }); + + const header: Bounds | null = await dialog.locator('[suiDialogHeader]').boundingBox(); + if (!header) throw new Error('Expected a draggable header.'); + await page.mouse.move(header.x + header.width / 2, header.y + header.height / 2); + await page.mouse.down(); + await page.mouse.move(header.x + header.width / 2 - 100, header.y + header.height / 2 + 40, { steps: 5 }); + await page.mouse.up(); + await expect.poll(async (): Promise => (await dialog.boundingBox())?.x ?? initial.x).toBeLessThan(initial.x); + await dialog.getByRole('button', { name: 'Close', exact: true }).click(); + await expect(dialog).toBeHidden(); + await trigger.click(); + await expect(async (): Promise => { + const reopened: Bounds | null = await dialog.boundingBox(); + expect(reopened?.x).toBeCloseTo(initial.x, 0); + expect(reopened?.y).toBeCloseTo(initial.y, 0); + }).toPass({ timeout: 5_000 }); +}); + +test('custom coordinates can change while the Dialog is open', async ({ page }: { page: Page }): Promise => { + await page.getByRole('button', { name: 'Open positioned dialog' }).click(); + const dialog: Locator = page.getByRole('dialog', { name: 'Custom position' }); + await dialog.getByRole('button', { name: 'Move to bottom left' }).click(); + await expect(async (): Promise => { + const bounds: Bounds | null = await dialog.boundingBox(); + if (!bounds) throw new Error('Expected the repositioned Dialog to remain visible.'); + const viewportHeight: number = await page.evaluate((): number => document.documentElement.clientHeight); + const rem: number = await page.evaluate((): number => Number.parseFloat(getComputedStyle(document.documentElement).fontSize)); + expect(bounds.x).toBeCloseTo(24, 0); + expect(viewportHeight - bounds.y - bounds.height).toBeCloseTo(1.5 * rem, 0); + }).toPass({ timeout: 5_000 }); +}); + test('resizes in place without moving the Dialog origin', async ({ page, browserName, From 3bcd7a729e25d273fc7b4988fe863b55938e18a4 Mon Sep 17 00:00:00 2001 From: Sebastian Enns Date: Wed, 30 Sep 2026 18:53:32 +0200 Subject: [PATCH 3/8] feat(sushi): refine selection and interaction styling --- docs/component-styling.md | 5 ++- .../app/generated/api-reference.generated.ts | 21 ++++++++++- .../theme-token-reference.generated.ts | 2 +- .../input/examples/basic/basic.example.html | 14 +------- .../input/examples/basic/basic.example.ts | 8 ++--- .../listbox/examples/basic/basic.example.html | 7 ++-- .../listbox/examples/basic/basic.example.ts | 8 ++--- .../examples/basic/basic.example.html | 11 +----- .../examples/basic/basic.example.ts | 8 ++--- .../examples/basic/basic.example.html | 36 +------------------ .../examples/basic/basic.example.ts | 31 +++------------- .../select/examples/basic/basic.example.html | 12 +------ .../select/examples/basic/basic.example.ts | 8 ++--- sushi/src/lib/listbox/listbox.component.css | 15 ++++++++ sushi/src/lib/listbox/listbox.component.html | 2 +- sushi/src/lib/listbox/listbox.component.ts | 1 + sushi/src/styles/features/button.styles.css | 16 +++++++++ .../src/styles/features/selection.styles.css | 6 ++-- sushi/src/styles/sushi.themes.css | 2 +- 19 files changed, 80 insertions(+), 133 deletions(-) create mode 100644 sushi/src/lib/listbox/listbox.component.css diff --git a/docs/component-styling.md b/docs/component-styling.md index d81cf71..4e34c5c 100644 --- a/docs/component-styling.md +++ b/docs/component-styling.md @@ -38,7 +38,7 @@ The generator places documented `--sui-*` properties on the feature's Theming pa The global theme reference is generated from `sushi.themes.css`. Document every token in the light theme and define the same token in the dark theme: ```css -/** @group Brand | Primary actions and selection. */ +/** @group Brand | Primary actions and emphasis. */ --color-primary: #7a0712; ``` @@ -57,6 +57,9 @@ The package build compiles this source into the published `styles.css`. The play - Prefer a Sushi component, then a DaisyUI primitive, then Tailwind utilities, then feature CSS. - Keep the public API independent of DaisyUI and Tailwind class names. - Put consumer-layout utilities on consumer-owned elements. +- Use neutral surfaces for ordinary hover and selection states; reserve the primary color for actions and emphasis. +- Keep option height, padding, borders, and radii consistent with adjacent controls. Make stable component dimensions overridable through documented tokens. +- Show the component by itself in the first Playground example; demonstrate composition separately. - Avoid `!important`, broad selectors, duplicated theme colors, and fixed overlay positioning. - Respect `prefers-reduced-motion`. - Check light and dark themes, long content, zoom, and narrow viewports. diff --git a/playground/src/app/generated/api-reference.generated.ts b/playground/src/app/generated/api-reference.generated.ts index 8e53d59..de52c84 100644 --- a/playground/src/app/generated/api-reference.generated.ts +++ b/playground/src/app/generated/api-reference.generated.ts @@ -1322,7 +1322,14 @@ export const apiReference: Readonly<{ typeParameters: [], }, ], - styles: [], + styles: [ + { + name: '--sui-button-pressed-scale', + defaultValue: 'theme default', + exampleValue: null, + description: 'Scale of an enabled button while pressed, including touch input.', + }, + ], }, Card: { className: 'Card', @@ -6937,6 +6944,18 @@ export const apiReference: Readonly<{ }, ], styles: [ + { + name: '--sui-listbox-option-min-height', + defaultValue: '2.5rem', + exampleValue: null, + description: 'Minimum height of a listbox option.', + }, + { + name: '--sui-listbox-option-padding', + defaultValue: '0.5rem 0.75rem', + exampleValue: null, + description: 'Padding inside a listbox option.', + }, { name: '--sui-selection-option-hover-background', defaultValue: 'theme default', diff --git a/playground/src/app/generated/theme-token-reference.generated.ts b/playground/src/app/generated/theme-token-reference.generated.ts index 4cda6a2..560f19a 100644 --- a/playground/src/app/generated/theme-token-reference.generated.ts +++ b/playground/src/app/generated/theme-token-reference.generated.ts @@ -42,7 +42,7 @@ export const THEME_TOKEN_REFERENCES: readonly ThemeTokenReference[] = [ lightValue: '#7a0712', darkValue: '#e35a60', group: 'Brand', - purpose: 'Primary actions and selection.', + purpose: 'Primary actions and emphasis.', }, { token: '--color-primary-content', diff --git a/playground/src/app/pages/input/examples/basic/basic.example.html b/playground/src/app/pages/input/examples/basic/basic.example.html index 8e14160..22cce96 100644 --- a/playground/src/app/pages/input/examples/basic/basic.example.html +++ b/playground/src/app/pages/input/examples/basic/basic.example.html @@ -1,17 +1,5 @@
-
- - - -
+

Current value: {{ form.name().value() || 'Empty' }}

diff --git a/playground/src/app/pages/input/examples/basic/basic.example.ts b/playground/src/app/pages/input/examples/basic/basic.example.ts index 86a1a49..7decbdf 100644 --- a/playground/src/app/pages/input/examples/basic/basic.example.ts +++ b/playground/src/app/pages/input/examples/basic/basic.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; import { FieldTree, form, FormField, required, SchemaPathTree } from '@angular/forms/signals'; -import { Button, Input, InputGroup, JoinItem, Label } from '@sushi-kit/angular'; +import { Input, Label } from '@sushi-kit/angular'; interface InputModel { name: string; @@ -8,7 +8,7 @@ interface InputModel { @Component({ selector: 'pg-input-basic-example', - imports: [FormField, Button, Input, InputGroup, JoinItem, Label], + imports: [FormField, Input, Label], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -17,8 +17,4 @@ export class InputBasicExample { protected readonly form: FieldTree = form(this.model, (schema: SchemaPathTree): void => required(schema.name), ); - - protected reset(): void { - this.form().reset({ name: '' }); - } } diff --git a/playground/src/app/pages/listbox/examples/basic/basic.example.html b/playground/src/app/pages/listbox/examples/basic/basic.example.html index 7d198ab..163070b 100644 --- a/playground/src/app/pages/listbox/examples/basic/basic.example.html +++ b/playground/src/app/pages/listbox/examples/basic/basic.example.html @@ -1,4 +1,4 @@ -
+
Active workspace

Use the arrow keys to move and Space to select. @@ -11,8 +11,5 @@ [options]="workspaces" [formField]="workspaceForm.workspace" /> -

- Selected: {{ workspaceForm.workspace().value() }} - -
+

Selected: {{ workspaceForm.workspace().value() }}

diff --git a/playground/src/app/pages/listbox/examples/basic/basic.example.ts b/playground/src/app/pages/listbox/examples/basic/basic.example.ts index d40000b..9d4e3ef 100644 --- a/playground/src/app/pages/listbox/examples/basic/basic.example.ts +++ b/playground/src/app/pages/listbox/examples/basic/basic.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; import { FieldTree, form, FormField } from '@angular/forms/signals'; -import { Button, Label, Listbox, ListboxModelValue, ListboxOption } from '@sushi-kit/angular'; +import { Label, Listbox, ListboxModelValue, ListboxOption } from '@sushi-kit/angular'; interface WorkspaceForm { workspace: ListboxModelValue; @@ -8,7 +8,7 @@ interface WorkspaceForm { @Component({ selector: 'pg-listbox-basic-example', - imports: [Button, FormField, Label, Listbox], + imports: [FormField, Label, Listbox], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -21,8 +21,4 @@ export class ListboxBasicExample { ]; protected readonly model: WritableSignal = signal({ workspace: 'design' }); protected readonly workspaceForm: FieldTree = form(this.model); - - protected reset(): void { - this.workspaceForm().reset({ workspace: 'design' }); - } } diff --git a/playground/src/app/pages/multi-select/examples/basic/basic.example.html b/playground/src/app/pages/multi-select/examples/basic/basic.example.html index 7eac6b1..f698998 100644 --- a/playground/src/app/pages/multi-select/examples/basic/basic.example.html +++ b/playground/src/app/pages/multi-select/examples/basic/basic.example.html @@ -1,14 +1,5 @@
Message labels -
- - -
+

Selected: {{ labelForm.labels().value().length }} labels

diff --git a/playground/src/app/pages/multi-select/examples/basic/basic.example.ts b/playground/src/app/pages/multi-select/examples/basic/basic.example.ts index 968b3c3..6734335 100644 --- a/playground/src/app/pages/multi-select/examples/basic/basic.example.ts +++ b/playground/src/app/pages/multi-select/examples/basic/basic.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; import { FieldTree, form, FormField, minLength } from '@angular/forms/signals'; -import { Button, Join, JoinItem, Label, MultiSelect, MultiSelectModelValue, MultiSelectOption } from '@sushi-kit/angular'; +import { Label, MultiSelect, MultiSelectModelValue, MultiSelectOption } from '@sushi-kit/angular'; interface LabelForm { labels: MultiSelectModelValue; @@ -8,7 +8,7 @@ interface LabelForm { @Component({ selector: 'pg-multi-select-basic-example', - imports: [FormField, Button, Join, JoinItem, Label, MultiSelect], + imports: [FormField, Label, MultiSelect], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -21,8 +21,4 @@ export class MultiSelectBasicExample { ]; protected readonly model: WritableSignal = signal({ labels: ['important', 'work'] }); protected readonly labelForm: FieldTree = form(this.model, (schema) => minLength(schema.labels, 2)); - - protected reset(): void { - this.labelForm().reset({ labels: ['important', 'work'] }); - } } diff --git a/playground/src/app/pages/pagination/examples/basic/basic.example.html b/playground/src/app/pages/pagination/examples/basic/basic.example.html index 9519d49..a2cd70a 100644 --- a/playground/src/app/pages/pagination/examples/basic/basic.example.html +++ b/playground/src/app/pages/pagination/examples/basic/basic.example.html @@ -1,35 +1 @@ - -
-
-

Release train

-

Upcoming component milestones and their target channels.

-
- {{ releases.length }} releases -
- -
- - - - {{ release.code }} - - {{ release.name }} - Target {{ release.target }} - - {{ release.channel }} - - - -
- - -
+ diff --git a/playground/src/app/pages/pagination/examples/basic/basic.example.ts b/playground/src/app/pages/pagination/examples/basic/basic.example.ts index 5f7a6f7..9444172 100644 --- a/playground/src/app/pages/pagination/examples/basic/basic.example.ts +++ b/playground/src/app/pages/pagination/examples/basic/basic.example.ts @@ -1,35 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, signal, Signal, WritableSignal } from '@angular/core'; -import { Badge, Card, CardTitle, Divider, List, ListItemTemplate, Pagination } from '@sushi-kit/angular'; - -interface Release { - readonly code: string; - readonly id: number; - readonly name: string; - readonly channel: string; - readonly target: string; -} +import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; +import { Pagination } from '@sushi-kit/angular'; @Component({ selector: 'pg-pagination-basic-example', - imports: [Badge, Card, CardTitle, Divider, List, ListItemTemplate, Pagination], + imports: [Pagination], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class PaginationBasicExample { - protected readonly page: WritableSignal = signal(1); - protected readonly pageSize: WritableSignal = signal(3); - protected readonly releases: readonly Release[] = [ - { id: 1, code: 'R-241', name: 'Navigation foundations', channel: 'Stable', target: 'Sep 12' }, - { id: 2, code: 'R-242', name: 'Accessible overlays', channel: 'Stable', target: 'Sep 18' }, - { id: 3, code: 'R-243', name: 'Signal form controls', channel: 'Stable', target: 'Sep 25' }, - { id: 4, code: 'R-244', name: 'Selection patterns', channel: 'Preview', target: 'Oct 2' }, - { id: 5, code: 'R-245', name: 'Data presentation', channel: 'Preview', target: 'Oct 9' }, - { id: 6, code: 'R-246', name: 'Responsive layouts', channel: 'Preview', target: 'Oct 16' }, - { id: 7, code: 'R-247', name: 'Application shell', channel: 'Planned', target: 'Oct 23' }, - { id: 8, code: 'R-248', name: 'Table composition', channel: 'Planned', target: 'Oct 30' }, - ]; - protected readonly visibleReleases: Signal = computed(() => { - const start: number = (this.page() - 1) * this.pageSize(); - return this.releases.slice(start, start + this.pageSize()); - }); + protected readonly page: WritableSignal = signal(1); } diff --git a/playground/src/app/pages/select/examples/basic/basic.example.html b/playground/src/app/pages/select/examples/basic/basic.example.html index b79308d..b024299 100644 --- a/playground/src/app/pages/select/examples/basic/basic.example.html +++ b/playground/src/app/pages/select/examples/basic/basic.example.html @@ -1,15 +1,5 @@
Favorite color -
- - -
+

Selected: {{ colorForm.color().value() }}

diff --git a/playground/src/app/pages/select/examples/basic/basic.example.ts b/playground/src/app/pages/select/examples/basic/basic.example.ts index 02f74f0..d872654 100644 --- a/playground/src/app/pages/select/examples/basic/basic.example.ts +++ b/playground/src/app/pages/select/examples/basic/basic.example.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; import { FieldTree, form, FormField, required } from '@angular/forms/signals'; -import { Button, Join, JoinItem, Label, Select, SelectModelValue, SelectOption } from '@sushi-kit/angular'; +import { Label, Select, SelectModelValue, SelectOption } from '@sushi-kit/angular'; interface ColorForm { color: SelectModelValue; @@ -8,7 +8,7 @@ interface ColorForm { @Component({ selector: 'pg-select-basic-example', - imports: [FormField, Button, Join, JoinItem, Label, Select], + imports: [FormField, Label, Select], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -20,8 +20,4 @@ export class SelectBasicExample { ]; protected readonly model: WritableSignal = signal({ color: 'crimson' }); protected readonly colorForm: FieldTree = form(this.model, (schema) => required(schema.color)); - - protected reset(): void { - this.colorForm().reset({ color: 'crimson' }); - } } diff --git a/sushi/src/lib/listbox/listbox.component.css b/sushi/src/lib/listbox/listbox.component.css new file mode 100644 index 0000000..a508502 --- /dev/null +++ b/sushi/src/lib/listbox/listbox.component.css @@ -0,0 +1,15 @@ +:host { + /** Minimum height of a listbox option. */ + --sui-listbox-option-min-height: 2.5rem; + /** Padding inside a listbox option. */ + --sui-listbox-option-padding: 0.5rem 0.75rem; +} + +.sui-listbox-option { + min-height: var(--sui-listbox-option-min-height); + padding: var(--sui-listbox-option-padding); +} + +.sui-listbox-option::after { + display: none; +} diff --git a/sushi/src/lib/listbox/listbox.component.html b/sushi/src/lib/listbox/listbox.component.html index 49cd5cd..29ed9c5 100644 --- a/sushi/src/lib/listbox/listbox.component.html +++ b/sushi/src/lib/listbox/listbox.component.html @@ -84,7 +84,7 @@
Date: Wed, 30 Sep 2026 23:03:45 +0200 Subject: [PATCH 4/8] feat(playground): redesign component documentation --- playground/src/app/app.css | 38 +++++ playground/src/app/app.html | 35 +++-- playground/src/app/app.ts | 3 +- .../app/pages/accordion/accordion.page.html | 17 +-- playground/src/app/pages/api/api.page.html | 4 +- .../app/pages/auto-focus/auto-focus.page.html | 11 +- .../pages/autocomplete/autocomplete.page.html | 22 +-- .../src/app/pages/avatar/avatar.page.html | 12 +- .../src/app/pages/badge/badge.page.html | 10 +- .../app/pages/breadcrumb/breadcrumb.page.html | 9 +- .../src/app/pages/button/button.page.html | 13 +- playground/src/app/pages/card/card.page.html | 11 +- .../src/app/pages/checkbox/checkbox.page.html | 12 +- playground/src/app/pages/chip/chip.page.html | 20 +-- playground/src/app/pages/code/code.page.html | 13 +- .../pages/color-picker/color-picker.page.html | 18 +-- .../pages/contributor/contributor.page.html | 8 +- .../app/pages/contributor/contributor.page.ts | 2 + .../app/pages/data-view/data-view.page.html | 15 +- .../src/app/pages/dialog/dialog.page.html | 19 +-- .../src/app/pages/divider/divider.page.html | 12 +- .../src/app/pages/drawer/drawer.page.html | 13 +- .../src/app/pages/fieldset/fieldset.page.html | 14 +- .../app/pages/file-drop/file-drop.page.html | 16 +-- .../app/pages/file-input/file-input.page.html | 18 +-- .../src/app/pages/gallery/gallery.page.html | 8 +- playground/src/app/pages/icon/icon.page.html | 10 +- .../app/pages/indicator/indicator.page.html | 12 +- .../pages/input-group/input-group.page.html | 10 +- .../pages/input-number/input-number.page.html | 16 +-- .../app/pages/input-otp/input-otp.page.html | 10 +- .../input-surface/input-surface.page.html | 10 +- .../src/app/pages/input/input.page.html | 22 +-- playground/src/app/pages/join/join.page.html | 10 +- playground/src/app/pages/kbd/kbd.page.html | 11 +- .../src/app/pages/label/label.page.html | 8 +- .../src/app/pages/lightbox/lightbox.page.html | 12 +- playground/src/app/pages/list/list.page.html | 14 +- .../src/app/pages/listbox/listbox.page.html | 22 +-- playground/src/app/pages/mask/mask.page.html | 8 +- playground/src/app/pages/menu/menu.page.html | 24 ++-- .../src/app/pages/message/message.page.html | 12 +- .../pages/multi-select/multi-select.page.html | 28 ++-- .../src/app/pages/navbar/navbar.page.html | 21 +-- .../app/pages/order-list/order-list.page.html | 22 +-- .../src/app/pages/overview/overview.page.css | 99 +++++++++++++ .../src/app/pages/overview/overview.page.html | 76 +++++----- .../src/app/pages/overview/overview.page.ts | 5 +- .../app/pages/pagination/pagination.page.html | 15 +- .../src/app/pages/popover/popover.page.html | 15 +- .../src/app/pages/progress/progress.page.html | 12 +- .../src/app/pages/radio/radio.page.html | 12 +- .../src/app/pages/range/range.page.html | 16 +-- .../select-button/select-button.page.html | 16 +-- .../src/app/pages/select/select.page.html | 26 ++-- .../src/app/pages/sidebar/sidebar.page.html | 13 +- .../src/app/pages/skeleton/skeleton.page.html | 12 +- .../src/app/pages/spinner/spinner.page.html | 12 +- .../src/app/pages/status/status.page.html | 12 +- .../src/app/pages/styling/styling.page.html | 4 +- .../src/app/pages/table/table.page.html | 17 +-- playground/src/app/pages/tabs/tabs.page.html | 13 +- .../src/app/pages/textarea/textarea.page.html | 18 +-- .../src/app/pages/toast/toast.page.html | 10 +- .../toggle-button/toggle-button.page.html | 12 +- .../src/app/pages/toggle/toggle.page.html | 12 +- .../src/app/pages/tooltip/tooltip.page.html | 9 +- .../example-code/example-code.component.css | 2 +- .../example-code/example-code.component.html | 66 +++++---- playground/src/styles.css | 131 +++++++++++++++++- 70 files changed, 794 insertions(+), 496 deletions(-) create mode 100644 playground/src/app/pages/overview/overview.page.css diff --git a/playground/src/app/app.css b/playground/src/app/app.css index 5d4e87f..e2c6971 100644 --- a/playground/src/app/app.css +++ b/playground/src/app/app.css @@ -1,3 +1,41 @@ :host { display: block; } + +.pg-shell__content { + background-color: color-mix(in oklab, var(--color-base-200) 36%, var(--color-base-100)); +} + +.pg-shell__header { + height: 4.0625rem; + border-bottom: 1px solid var(--sui-color-border); + background-color: color-mix(in oklab, var(--color-base-100) 94%, transparent); + backdrop-filter: blur(0.75rem); +} + +.pg-shell__header-inner { + display: flex; + width: 100%; + max-width: 72rem; + height: 100%; + margin-inline: auto; + align-items: center; + padding-inline: 0; +} + +.pg-documentation-navbar { + --sui-navbar-item-active-background: transparent; + --sui-navbar-item-active-color: var(--color-primary); +} + +.pg-library-sidebar { + --sui-sidebar-background: var(--color-base-100); + --sui-sidebar-item-active-background: color-mix(in oklab, var(--color-base-content) 9%, transparent); + --sui-sidebar-item-active-color: var(--color-base-content); +} + +@media (min-width: 40rem) { + .pg-shell__header-inner { + padding-inline: 1rem; + } +} diff --git a/playground/src/app/app.html b/playground/src/app/app.html index eebb370..401503d 100644 --- a/playground/src/app/app.html +++ b/playground/src/app/app.html @@ -1,21 +1,26 @@ -
-
-
+
+
+
-
- - - Sushi - Component Playground - +
+ + + + Sushi Kit + Angular Components + +