diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0df1ce..27452ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ npm start The repository pins its Node.js version in `.nvmrc` and its npm version in `package.json`. +For a phone on the same trusted network, use `npm run start:network` and open the printed `Network` URL. + `sushi/` is the published Angular library. `playground/` contains examples and generated documentation. `tools/` contains repository-only generators and build checks. ## Choose the relevant guide diff --git a/docs/styling-and-themes.md b/docs/styling-and-themes.md index 0c98abf..6a9e7d5 100644 --- a/docs/styling-and-themes.md +++ b/docs/styling-and-themes.md @@ -77,16 +77,16 @@ Use the DaisyUI color roles for the application palette. Every background color Sushi derives shared interaction and overlay styles from these semantic tokens. Override them when a theme needs a different treatment instead of changing several components: -| Token | Purpose | -| ------------------------ | ----------------------------- | -| `--sui-color-border` | Default component border | -| `--sui-color-muted` | Secondary text and actions | -| `--sui-color-subtle` | Low-emphasis labels | -| `--sui-color-hover` | Neutral hover surface | -| `--sui-color-overlay` | Dialog and popover surface | -| `--sui-focus-ring-color` | Keyboard focus indicator | -| `--sui-shadow-overlay` | Dialog and popover elevation | -| `--sui-disabled-opacity` | Shared disabled-state opacity | +| Token | Purpose | +| ------------------------ | ---------------------------------------------------------- | +| `--sui-color-border` | Default component border | +| `--sui-color-muted` | Secondary text and actions | +| `--sui-color-subtle` | Low-emphasis labels | +| `--sui-color-hover` | Neutral hover surface | +| `--sui-color-overlay` | Dialog and popover surface | +| `--sui-focus-ring-color` | Optional global focus color; otherwise follows the control | +| `--sui-shadow-overlay` | Dialog and popover elevation | +| `--sui-disabled-opacity` | Shared disabled-state opacity | Component tokens remain the correct choice for a local exception. For example, `--sui-popover-shadow` defaults to `--sui-shadow-overlay` but can be changed on one popover. diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index b5b0f93..cebb9c3 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -27,6 +27,11 @@ export default defineConfig({ name: 'webkit', use: { ...devices['Desktop Safari'] }, }, + { + name: 'mobile-webkit', + testMatch: ['**/responsive-layout.spec.ts', '**/mobile-overlay.spec.ts'], + use: { ...devices['iPhone 13'] }, + }, ], webServer: { command: 'npm run preview:playground', diff --git a/e2e/specs/drawer.spec.ts b/e2e/specs/drawer.spec.ts index bc7a825..c59e132 100644 --- a/e2e/specs/drawer.spec.ts +++ b/e2e/specs/drawer.spec.ts @@ -7,6 +7,14 @@ interface BoxGeometry { readonly y: number; } +interface DrawerGeometry { + readonly left: number; + readonly right: number; + readonly width: number; + readonly viewportWidth: number; + readonly documentWidth: number; +} + test('modal Drawer closes with Escape and restores focus to its trigger', async ({ page }: { page: Page }): Promise => { await page.goto('/drawer'); const trigger: Locator = page.getByRole('button', { name: /Cart/ }); @@ -80,24 +88,27 @@ test('responsive Drawers use one Sidebar as an overlay and persistent desktop la await expect(mobileTrigger).toBeFocused(); }); -test('small Drawers fill narrow viewports without horizontal overflow', async ({ page }: { page: Page }): Promise => { +test('Drawers stay inside narrow viewports without horizontal overflow', async ({ page }: { page: Page }): Promise => { await page.setViewportSize({ width: 360, height: 640 }); await page.goto('/drawer'); await page.getByRole('button', { name: /Cart/ }).click(); const panel: Locator = page.getByRole('dialog', { name: 'Your cart' }); - await expect - .poll(async (): Promise => { - const box: BoxGeometry | null = await panel.boundingBox(); - if (box === null) return false; - - const viewportWidth: number = await page.evaluate((): number => innerWidth); - const horizontalMargin: number = viewportWidth * 0.02; - const fillsViewport: boolean = box.width >= viewportWidth - horizontalMargin; - const staysContained: boolean = box.x >= -horizontalMargin && box.x + box.width <= viewportWidth + horizontalMargin; - return fillsViewport && staysContained; - }) - .toBe(true); - await expect - .poll(async (): Promise => page.evaluate((): boolean => document.documentElement.scrollWidth <= innerWidth)) - .toBe(true); + await expect(panel).toBeVisible(); + await expect(async (): Promise => { + const geometry: DrawerGeometry = await panel.evaluate((element: HTMLElement): DrawerGeometry => { + const bounds: DOMRect = element.getBoundingClientRect(); + return { + left: bounds.left, + right: bounds.right, + width: bounds.width, + viewportWidth: document.documentElement.clientWidth, + documentWidth: document.documentElement.scrollWidth, + }; + }); + const details: string = `Drawer geometry: ${JSON.stringify(geometry)}`; + // Ignore subpixel rounding, not clipping or reserved scrollbar space. + expect(Math.round(geometry.left), details).toBeGreaterThanOrEqual(0); + expect(Math.round(geometry.right), details).toBeLessThanOrEqual(geometry.viewportWidth); + expect(geometry.documentWidth, details).toBeLessThanOrEqual(geometry.viewportWidth); + }).toPass({ timeout: 5_000 }); }); diff --git a/e2e/specs/mobile-overlay.spec.ts b/e2e/specs/mobile-overlay.spec.ts new file mode 100644 index 0000000..f43ab2c --- /dev/null +++ b/e2e/specs/mobile-overlay.spec.ts @@ -0,0 +1,103 @@ +import { expect, Locator, Page, test } from '@playwright/test'; + +interface Bounds { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +async function scrollContainer(trigger: Locator, distance: number): Promise { + await trigger.evaluate((element: HTMLElement, amount: number): void => { + let parent: HTMLElement | null = element.parentElement; + while (parent) { + const overflow: string = getComputedStyle(parent).overflowY; + if (/(auto|scroll)/.test(overflow) && parent.scrollHeight > parent.clientHeight) { + parent.scrollBy(0, amount); + return; + } + parent = parent.parentElement; + } + window.scrollBy(0, amount); + }, distance); +} + +async function expectAnchoredAbove(popup: Locator, trigger: Locator): Promise { + await expect + .poll( + async (): Promise => { + const control: Bounds | null = await trigger.boundingBox(); + const panel: Bounds | null = await popup.boundingBox(); + if (!control || !panel) return false; + + const gap: number = control.y - panel.y - panel.height; + return panel.y >= 0 && gap >= 0 && gap < control.height / 2; + }, + { message: 'The popup should stay above and beside its trigger without clipping.' }, + ) + .toBe(true); +} + +test('large Popover stays within a short viewport when opened at the bottom', async ({ page }: { page: Page }): Promise => { + await page.setViewportSize({ width: 390, height: 360 }); + await page.goto('/popover'); + const trigger: Locator = page.getByRole('button', { name: 'Assign chef', exact: true }); + await trigger.evaluate((element: HTMLElement): void => element.scrollIntoView({ block: 'end' })); + await trigger.click(); + const popup: Locator = page.getByRole('dialog', { name: 'Select a chef' }); + await expect(popup).toBeVisible(); + await expect + .poll(async (): Promise => { + const panel: Bounds | null = await popup.boundingBox(); + return !!panel && panel.y >= 0 && panel.y + panel.height <= 360; + }) + .toBe(true); +}); + +test('Popover stays above its separate target near the bottom edge', async ({ page }: { page: Page }): Promise => { + await page.setViewportSize({ width: 390, height: 740 }); + await page.goto('/popover'); + const trigger: Locator = page.getByRole('button', { name: 'Show details', exact: true }); + const target: Locator = page.getByRole('button', { name: 'Salmon nigiri €12', exact: true }); + await target.evaluate((element: HTMLElement): void => element.scrollIntoView({ block: 'end' })); + await trigger.click(); + const popup: Locator = page.getByRole('dialog', { name: 'Product details' }); + await expect(popup).toBeVisible(); + await expectAnchoredAbove(popup, target); + await scrollContainer(target, 80); + await expectAnchoredAbove(popup, target); +}); + +for (const [route, role, name, popupRole, popupName] of [ + ['select', 'combobox', 'Favorite color', 'listbox', ''], + ['multi-select', 'combobox', 'Message labels', 'listbox', ''], + ['popover', 'button', 'Share menu', 'dialog', 'Share this menu'], + ['popover', 'button', 'Assign chef', 'dialog', 'Select a chef'], +] as const) { + test(`${name} popup stays anchored above its trigger while scrolling a narrow page`, async ({ + page, + isMobile, + }: { + page: Page; + isMobile: boolean; + }): Promise => { + await page.setViewportSize({ width: 390, height: 600 }); + await page.goto(`/${route}`); + const trigger: Locator = page.getByRole(role, { name, exact: true }); + await trigger.evaluate((element: HTMLElement): void => element.scrollIntoView({ block: 'end' })); + if (isMobile) await trigger.tap(); + else await trigger.click(); + + const popup: Locator = popupName ? page.getByRole(popupRole, { name: popupName }) : page.getByRole(popupRole).first(); + await expect(popup).toBeVisible(); + await expectAnchoredAbove(popup, trigger); + + const initial: Bounds | null = await trigger.boundingBox(); + if (!initial) throw new Error('Expected a visible overlay trigger.'); + await scrollContainer(trigger, initial.height * 2); + await expect + .poll(async (): Promise => (await trigger.boundingBox())?.y ?? initial.y) + .toBeLessThan(initial.y - initial.height); + await expectAnchoredAbove(popup, trigger); + }); +} diff --git a/e2e/specs/responsive-layout.spec.ts b/e2e/specs/responsive-layout.spec.ts index e5962ab..079ccec 100644 --- a/e2e/specs/responsive-layout.spec.ts +++ b/e2e/specs/responsive-layout.spec.ts @@ -1,7 +1,68 @@ import { expect, Locator, Page, test } from '@playwright/test'; +interface Bounds { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + test.use({ viewport: { width: 390, height: 844 } }); +test('Input keeps its configured text size when focused', async ({ + page, + isMobile, +}: { + page: Page; + isMobile: boolean; +}): Promise => { + await page.goto('/input'); + for (const placeholder of ['XS input', 'LG input']) { + const input: Locator = page.getByPlaceholder(placeholder, { exact: true }); + await input.scrollIntoViewIfNeeded(); + const fontSize: string = await input.evaluate((element: HTMLElement): string => getComputedStyle(element).fontSize); + if (isMobile) await input.tap(); + else await input.click(); + + await expect(input).toBeFocused(); + await expect(input).toHaveCSS('font-size', fontSize); + } +}); + +for (const width of [390, 1280]) { + test(`Playground scrolls ${width < 1024 ? 'the document on mobile' : 'its content independently on desktop'}`, async ({ + page, + }: { + page: Page; + }): Promise => { + await page.setViewportSize({ width, height: 700 }); + await page.goto('/input'); + const content: Locator = page.getByTestId('playground-content'); + await expect(page.getByRole('textbox', { name: 'Name', exact: true })).toBeVisible(); + + await content.evaluate((element: HTMLElement): void => { + element.scrollTo(0, 200); + window.scrollTo(0, 200); + }); + await expect + .poll((): Promise => + content.evaluate((element: HTMLElement): boolean => { + const documentScroll: number = document.scrollingElement?.scrollTop ?? 0; + return window.innerWidth < 1024 + ? documentScroll > 0 && element.scrollTop === 0 + : documentScroll === 0 && element.scrollTop > 0; + }), + ) + .toBe(true); + }); +} + +async function boundsOf(locator: Locator): Promise { + const bounds: Bounds | null = await locator.boundingBox(); + if (!bounds) throw new Error('Expected a visible layout surface.'); + return bounds; +} + test('collapsible Navbar exposes mobile navigation and restores toggle focus on Escape', async ({ page, }: { @@ -39,11 +100,7 @@ test('Navbar switches at its configured breakpoint without hiding persistent act await expect(action).toBeVisible(); }); -test('responsive Table remains inside a narrow viewport and exposes horizontal scrolling', async ({ - page, -}: { - page: Page; -}): Promise => { +test('Table stays inside a narrow viewport and exposes horizontal scrolling', async ({ page }: { page: Page }): Promise => { await page.goto('/table'); const table: Locator = page.getByRole('table', { name: 'Release queue' }); const viewport: Locator = table.locator('..'); @@ -60,3 +117,175 @@ test('responsive Table remains inside a narrow viewport and exposes horizontal s ) .toBe(true); }); + +test('Table keeps only its first column pinned and aligns the remaining headings with their cells', async ({ + page, +}: { + page: Page; +}): Promise => { + await page.goto('/table'); + const table: Locator = page.getByRole('table', { name: 'Release queue' }); + const viewport: Locator = table.locator('..'); + await expect(table).toBeVisible(); + const pinnedHeading: Locator = table.getByRole('columnheader').first(); + const movingHeading: Locator = table.getByRole('columnheader').nth(1); + const firstRow: Locator = table.locator('tbody tr').first(); + const pinnedBefore: Bounds = await boundsOf(pinnedHeading); + const movingBefore: Bounds = await boundsOf(movingHeading); + + await viewport.evaluate((element: HTMLElement): void => { + element.scrollLeft = element.scrollWidth - element.clientWidth; + }); + + await expect + .poll(async (): Promise => { + const pinned: Bounds = await boundsOf(pinnedHeading); + const rowHeading: Bounds = await boundsOf(firstRow.getByRole('rowheader')); + const moving: Bounds = await boundsOf(movingHeading); + const bodyCell: Bounds = await boundsOf(firstRow.getByRole('cell').first()); + const tolerance: number = pinned.width * 0.03; + + return ( + Math.abs(pinned.x - pinnedBefore.x) < tolerance && + Math.abs(rowHeading.x - pinned.x) < tolerance && + moving.x < movingBefore.x - moving.width / 2 && + Math.abs(moving.x - bodyCell.x) < tolerance + ); + }) + .toBe(true); +}); + +test('Table keeps its heading visible while its rows scroll vertically', async ({ page }: { page: Page }): Promise => { + await page.goto('/table'); + const table: Locator = page.getByRole('table', { name: 'Scrollable team directory' }); + const viewport: Locator = table.locator('..'); + await viewport.scrollIntoViewIfNeeded(); + + const heading: Locator = table.getByRole('columnheader').first(); + const row: Locator = table.locator('tbody tr').first(); + const headingBefore: Bounds = await boundsOf(heading); + const rowBefore: Bounds = await boundsOf(row); + + await viewport.evaluate((element: HTMLElement): void => { + element.scrollTop = element.scrollHeight - element.clientHeight; + }); + + await expect + .poll(async (): Promise => { + const headingAfter: Bounds = await boundsOf(heading); + const rowAfter: Bounds = await boundsOf(row); + return ( + Math.abs(headingAfter.y - headingBefore.y) < headingBefore.height * 0.05 && + rowAfter.y < rowBefore.y - rowBefore.height / 2 + ); + }) + .toBe(true); +}); + +for (const route of ['input', 'file-input', 'fieldset', 'range']) { + test(`${route} controls stay inside their labels and fieldsets on narrow screens`, async ({ + page, + }: { + page: Page; + }): Promise => { + await page.goto(`/${route}`); + const controls: Locator = page.locator( + 'input[suiInput], input[suiInputSurfaceControl], input[suiFileInput], input[suiRange]', + ); + await expect(controls.first()).toBeVisible(); + + for (const width of [320, 390]) { + await page.setViewportSize({ width, height: 844 }); + await expect + .poll(async () => + controls.evaluateAll((elements: Element[]) => + elements + .filter((element: Element): boolean => element.getBoundingClientRect().width > 0) + .flatMap((element: Element) => { + const bounds: DOMRect = element.getBoundingClientRect(); + const parentElement: HTMLElement | null = element.parentElement; + if (!parentElement) throw new Error('Expected a form control inside a layout container.'); + const parent: DOMRect = parentElement.getBoundingClientRect(); + const fieldset: DOMRect | undefined = element.closest('fieldset')?.getBoundingClientRect(); + const right: number = Math.min(parent.right, fieldset?.right ?? innerWidth, innerWidth); + if (bounds.left >= parent.left - 2 && bounds.right <= right + 2) return []; + return [ + { + name: element.getAttribute('aria-label') ?? element.getAttribute('type'), + left: bounds.left, + right: bounds.right, + availableRight: right, + }, + ]; + }), + ), + ) + .toEqual([]); + } + }); +} + +test('Budget controls stack on mobile and remain usable', async ({ + page, + isMobile, +}: { + page: Page; + isMobile: boolean; +}): Promise => { + await page.goto('/input-group'); + const amount: Locator = page.getByRole('spinbutton', { name: 'Budget amount' }); + const period: Locator = page.getByRole('combobox', { name: 'Billing period' }); + const action: Locator = page.getByRole('button', { name: 'Apply', exact: true }); + const group: Locator = amount.locator('../..'); + + await expect + .poll(async (): Promise => { + const input: Bounds = await boundsOf(amount); + const select: Bounds = await boundsOf(period); + const button: Bounds = await boundsOf(action); + const container: Bounds = await boundsOf(group); + const tolerance: number = container.width * 0.03; + return ( + select.y >= input.y + input.height - tolerance && + button.y >= select.y + select.height - tolerance && + input.height >= button.height * 0.9 && + Math.abs(select.width - container.width) < tolerance + ); + }) + .toBe(true); + + await amount.fill('25'); + if (isMobile) await period.tap(); + else await period.click(); + const option: Locator = page.getByRole('option', { name: 'Monthly', exact: true }); + await expect(option).toBeVisible(); + if (isMobile) await option.tap(); + else await option.click(); + await expect(period).toContainText('Monthly'); + await expect(amount).toHaveValue('25'); +}); + +test('Budget controls fit on one row above the desktop breakpoint', async ({ page }: { page: Page }): Promise => { + await page.setViewportSize({ width: 800, height: 844 }); + await page.goto('/input-group'); + const amount: Locator = page.getByRole('spinbutton', { name: 'Budget amount' }); + const period: Locator = page.getByRole('combobox', { name: 'Billing period' }); + const action: Locator = page.getByRole('button', { name: 'Apply', exact: true }); + const group: Locator = amount.locator('../..'); + + await expect + .poll(async (): Promise => { + const input: Bounds = await boundsOf(amount); + const select: Bounds = await boundsOf(period); + const button: Bounds = await boundsOf(action); + const container: Bounds = await boundsOf(group); + const tolerance: number = input.height * 0.05; + return ( + Math.abs(input.y - select.y) < tolerance && + Math.abs(input.y - button.y) < tolerance && + select.x >= input.x + input.width - tolerance && + button.x + button.width <= container.x + container.width + tolerance + ); + }) + .toBe(true); +}); diff --git a/package.json b/package.json index 4b360b5..8e0818d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "preview:playground": "npx serve dist/playground/browser -l 4201 -s", "release": "commit-and-tag-version", "start": "npm run generate:api && ng serve playground", + "start:network": "npm start -- --host 0.0.0.0", "test": "ng test sushi --no-watch && npm run test:tools", "test:coverage": "ng test sushi --no-watch --coverage", "test:e2e": "npm run build:playground && playwright test --config e2e/playwright.config.ts", diff --git a/playground/src/app/app.html b/playground/src/app/app.html index d36d7c9..eebb370 100644 --- a/playground/src/app/app.html +++ b/playground/src/app/app.html @@ -1,11 +1,11 @@ -
+
Basic collection
-

List and grid

-

Switch layout through controlled state while one item template adapts its presentation.

+

Custom layout switcher

+

+ Use suiDataViewHeader for your own controls, bind layout, and set layouts to an empty array to hide the built-in switcher. +

+ + +
+ Products +
+ + +
+
+
@if (layout === 'grid') { diff --git a/playground/src/app/pages/data-view/examples/layout/layout.example.ts b/playground/src/app/pages/data-view/examples/layout/layout.example.ts index 020c10a..4467b24 100644 --- a/playground/src/app/pages/data-view/examples/layout/layout.example.ts +++ b/playground/src/app/pages/data-view/examples/layout/layout.example.ts @@ -1,5 +1,15 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { Badge, Button, Card, CardActions, CardTitle, DataView, DataViewItemTemplate } from '@sushi-kit/angular'; +import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; +import { + Badge, + Button, + Card, + CardActions, + CardTitle, + DataView, + DataViewHeaderTemplate, + DataViewItemTemplate, + DataViewLayout, +} from '@sushi-kit/angular'; import { LucideShoppingCart } from '@lucide/angular'; interface Product { @@ -14,11 +24,23 @@ interface Product { @Component({ selector: 'pg-data-view-layout-example', - imports: [Badge, Button, Card, CardActions, CardTitle, DataView, DataViewItemTemplate, LucideShoppingCart], + imports: [ + Badge, + Button, + Card, + CardActions, + CardTitle, + DataView, + DataViewHeaderTemplate, + DataViewItemTemplate, + LucideShoppingCart, + ], templateUrl: './layout.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewLayoutExample { + protected readonly layout: WritableSignal = signal('list'); + protected readonly products: readonly Product[] = [ { id: 1, diff --git a/playground/src/app/pages/file-input/examples/options/options.example.html b/playground/src/app/pages/file-input/examples/options/options.example.html index 9e33667..32f7fad 100644 --- a/playground/src/app/pages/file-input/examples/options/options.example.html +++ b/playground/src/app/pages/file-input/examples/options/options.example.html @@ -1,5 +1,5 @@ -
-
diff --git a/playground/src/app/pages/input-group/examples/commerce/commerce.example.html b/playground/src/app/pages/input-group/examples/commerce/commerce.example.html index b6c5f5c..733f4fa 100644 --- a/playground/src/app/pages/input-group/examples/commerce/commerce.example.html +++ b/playground/src/app/pages/input-group/examples/commerce/commerce.example.html @@ -1,7 +1,18 @@ -
- Budget - - EUR - - +
+ +
+
+ + EUR +
+ + +
diff --git a/playground/src/app/pages/input-group/examples/commerce/commerce.example.ts b/playground/src/app/pages/input-group/examples/commerce/commerce.example.ts index 61e5755..1f1a03f 100644 --- a/playground/src/app/pages/input-group/examples/commerce/commerce.example.ts +++ b/playground/src/app/pages/input-group/examples/commerce/commerce.example.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { Button, Input, InputGroup, InputGroupAddon, JoinItem, Select, SelectOption } from '@sushi-kit/angular'; +import { Button, Input, InputGroup, InputGroupAddon, JoinItem, Label, Select, SelectOption } from '@sushi-kit/angular'; @Component({ selector: 'pg-input-group-commerce-example', - imports: [Button, Input, InputGroup, InputGroupAddon, JoinItem, Select], + imports: [Button, Input, InputGroup, InputGroupAddon, JoinItem, Label, Select], templateUrl: './commerce.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/playground/src/app/pages/input-group/input-group.page.html b/playground/src/app/pages/input-group/input-group.page.html index 4e8b1c2..87915c8 100644 --- a/playground/src/app/pages/input-group/input-group.page.html +++ b/playground/src/app/pages/input-group/input-group.page.html @@ -13,7 +13,8 @@

Input Group

Addresses and responsive fields

- Use add-ons for fixed text and responsive orientation when controls should stack on narrow screens. + Groups are horizontal by default. Responsive groups stack below 48rem (768px at the default font size). Apply direction-specific + widths and flex sizing at the same breakpoint.

@@ -24,7 +25,7 @@

Addresses and responsive fields

Mixed controls

-

Inputs, add-ons, Select, and Button retain their semantics inside one visual group.

+

Keep the amount and currency together. Stack the period and action below on narrow screens.

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 eea6d47..8e14160 100644 --- a/playground/src/app/pages/input/examples/basic/basic.example.html +++ b/playground/src/app/pages/input/examples/basic/basic.example.html @@ -1,11 +1,11 @@ -
+
-
+
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 f9309f2..86a1a49 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, Join, JoinItem, Label } from '@sushi-kit/angular'; +import { Button, Input, InputGroup, JoinItem, 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, Join, JoinItem, Label], + imports: [FormField, Button, Input, InputGroup, JoinItem, Label], templateUrl: './basic.example.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/playground/src/app/pages/input/examples/composition/composition.example.html b/playground/src/app/pages/input/examples/composition/composition.example.html index de099f6..bfcd124 100644 --- a/playground/src/app/pages/input/examples/composition/composition.example.html +++ b/playground/src/app/pages/input/examples/composition/composition.example.html @@ -3,7 +3,7 @@ Search controls
-
+
Account fields
-