Skip to content

feat(chip): give sd-chip the shared chip look and drop dead API (CH3) - #105

Merged
sdcorejs merged 2 commits into
mainfrom
feat/chip-ch3-visual
Oct 26, 2026
Merged

sdcorejs merged 2 commits into
mainfrom
feat/chip-ch3-visual

Conversation

@sdcorejs

Copy link
Copy Markdown
Owner

Summary

CH3 of the sd-chip work in the design handoff .sdcorejs/handoff/design-icon-chip-table/ (spec in chip.md, items C1–C4, D1 and D2). It gives sd-chip the chip look of sd-chip-calendar through one shared SCSS mixin, marks locked chips, removes the dead autocomplete API and rewrites sd-chip.md. CH1 (#103) and CH2 (#104) are already on main, so this PR is based on main directly.

The work is two commits. The first adds the specs, which fail on their own. The second is the code, npm run sync, docs, CHANGELOG and the showcase.

What changed

Item Change
C1 A sd-chip-row($block, …) mixin at the end of core/form.scss styles mat-form-field.sd-chip-field and .sd-chip-calendar-field. Field 32 / 40 / 48px, chip 20 / 24 / 28px centred, text 12 / 13 / 14px, padding 8 / 8 / 10px, 4px between chips, square suffix buttons 24 / 28 / 32px with 16 / 18 / 20px icons. size="lg" now has a style. The md field is no longer taller than other controls.
C1 (one line) The typing area keeps at least 80px instead of Material's 150px, so md and lg fit three chips plus the clear button on one line. While chips are folded into “+N” (sd-chip-field--folded) the input shrinks and shows no placeholder, so a size="sm" table cell stays 32px.
C2 Radius 6px, --sd-surface background, 1px --sd-border outline, tabular digits, weight 400, transparent state layer.
C3 A chip that removable keeps gets --sd-chip-locked-bg, a lock icon in the place of ✕ with the tooltip “Không xoá được”, and the accessible name “{value}, Không xoá được”. Disabled fields show neither ✕ nor the lock.
C4 chip.component.scss has no ::ng-deep, !important or hex left. c-md, .no-padding-wrapper and the 10px margin-top are gone.
D1 Removed onSelect(), selectable, separatorKeysCodes, the MatAutocompleteModule / MatProgressSpinnerModule imports, the effect that assigned #name and SdRemovableChipPipe. isFocused is a signal, and the sdChanges subscription uses takeUntilDestroyed instead of ngOnDestroy. The label slot renders only with labelPlacement="top", like sd-date and sd-select.
D2 sd-chip.md rewritten: autoIds, the size scale, locked chips, maxVisible, the label slot, model, keyboard, the public API table, visual cues, E2E hooks, accessibility and anti-patterns.

Also in this PR:

  • Tokens: --sd-chip-locked-bg (55% --sd-surface-muted on --sd-surface) and --sd-chip-more-hover-bg (6% --sd-text on --sd-surface-muted), light and dark. --sd-chip-calendar-chip-locked-bg and --sd-chip-calendar-more-hover-bg keep their names and now default to the shared tokens. THEME.md and the contrast test cover both.
  • sd-chip-calendar: its :host ::ng-deep field block moved into the mixin, open state included. The only ::ng-deep left in its SCSS is the existing popup panel rule. This also fixes its clear button icon, which a shared .sd-clear-btn sd-icon rule forced to 16px at every size; it is now 18 / 20px at md / lg.
  • Showcase: “Kích thước” shows sm / md / lg side by side, each with a locked chip and clearable. The demo drops a helperText attribute sd-chip does not have.

Changes for consumers (CHANGELOG [Unreleased])

  • Changed (BREAKING for consumers), with a migration diff in the changelog:
    • onSelect(), selectable and separatorKeysCodes are gone. The template never rendered an autocomplete, so only code that called onSelect() itself is affected.
    • SdRemovableChipPipe (sdRemovableChip) is no longer exported from @sdcorejs/angular/forms/chip.
    • isFocused is a Signal<boolean>, and SdChip no longer has ngAfterViewInit() / ngOnDestroy().
  • Added: --sd-chip-locked-bg, --sd-chip-more-hover-bg.
  • Changed: the chip look and size scale, the 80px typing area, folded table cells, the locked chip, and sd-chip-calendar moving to the mixin.
  • Fixed: the sd-chip-calendar clear button icon size.

API consistency review

Following the mandatory naming and public API consistency review in AGENTS.md.

  • Compared with sd-chip-calendar (closest), sd-date and sd-select.
  • Checked: input/output names and aliases, types, defaults, two-way binding, public members, CSS classes, tokens, i18n keys, autoIds and exports.
  • Removed members are breaking and listed in CHANGELOG with a migration diff: onSelect, selectable, separatorKeysCodes, SdRemovableChipPipe, ngAfterViewInit / ngOnDestroy.
  • Type change: isFocused goes from boolean to Signal<boolean>. sd-chip-calendar still has a plain isFocused field, so the two differ here. Changing sd-chip-calendar is outside CH3.
  • New public members, named after sd-chip-calendar:
    • iconSizes: same name, but no open entry, because sd-chip has no calendar button.
    • lockedLabel: same name and key pattern (core.form.chip.locked, which CH2 added).
    • chipLabel(item): same name. It returns null for a removable chip, so the chip keeps its own text as its name, while sd-chip-calendar always returns the full date.
  • CSS classes follow the sd-chip-calendar BEM pattern: sd-chip-field, sd-chip__chip, sd-chip__chip--locked, sd-chip__lock, sd-chip__suffix, sd-chip-field--folded.
  • Tokens: the new tokens are shared rather than per component; the calendar names stay as aliases, so nothing that sets them breaks.
  • i18n: no new keys. The separate core.form.chip.* and core.form.chip-calendar.* keys stay as they are.
  • Inputs and outputs: unchanged.
  • v19–v22: rolled out with npm run sync. npm run check:sync is green on the committed tree.
  • Independent review: pending.

Deviations from chip.md

  1. Where the mixin lives: it is included in the global core/form.scss, not in each component's SCSS. From a component, reaching the inside of Material's chip and form field needs ::ng-deep, which the handoff forbids, and @use-ing form.scss from a component would copy the global CSS.
  2. Icon size: ✕ and lock are 12 / 14 / 16px, not 14 / 16 / 18px, following the sd-chip-calendar decision of 2026-10-23.
  3. Locked background: a 55% mix like sd-chip-calendar, not plain --sd-surface-muted.
  4. Added beyond the md: the 80px minimum typing area and the folded-field behaviour. Without them md / lg wrapped to two lines and a size="sm" table cell was 56px tall.
  5. 240px chip text: stays in sd-chip's SCSS, next to the tooltip logic for cut chips.
  6. Label slot: documented as labelPlacement="top" only, like sd-date and sd-select.

Verification

All of this ran locally on exact Node 24.22.1:

  • Karma v19 with coverage: npx ng test sdcorejs-angular --watch=false --browsers=ChromeHeadlessCI --code-coverage passes 7947/7947. Statements 90.03%, Branches 79.35%, Functions 90.64%, Lines 92.46%.
  • v22: the chip, chip-calendar and form-generic specs pass 724/724.
  • Other checks, all green:
    • npm run lint
    • npx ng build sdcorejs-angular
    • check:i18n, check:i18n-parity
    • npm run test:theme (53/53), test:theme-token-list (3/3), npm run check:scss-hex (0 findings), npm run test:showcase-examples (17/17)
    • npm run sync + npm run check:sync
  • New specs: 21 cases in chip.visual.spec.ts (with the global theme loaded through LayoutTestTheme) and 3 sd-chip-calendar clear-button cases. Of the first 17 visual cases, 16 failed before the code. The 4 added later came from layout problems found on the showcase (the two-line md / lg field and the 56px folded cell).
  • Showcase (800px pane, measured in the DOM):
    • sm 32 / 20px, md 40 / 24px, lg 48 / 28px, each on one line.
    • The locked chip has its background and the name “Quản trị, Không xoá được”.
    • The size="sm" table cell with [maxVisible]="2" is 32px: 2 chips, “+3”, no placeholder.

Not verified: final screenshots (the preview pane does not paint while hidden), 1280px and 768px widths, dark mode in the UI (the tokens have dark values and pass the contrast test), forced-colors mode and a real screen reader. The sd-chip-calendar showcase page was not loaded; it is covered by the geometry specs. v20 and v21 run in CI.

🤖 Generated with Claude Code

nghiatt15 and others added 2 commits October 26, 2026 07:46
Add chip.visual.spec.ts (21 cases, LayoutTestTheme with the global
theme loaded). It measures the sm / md / lg scale: field 32/40/48,
chip 20/24/28 centred, text 12/13/14, padding, the remove and lock
icons, the clear button with its icon, the "+N" button and the 4px gap.
It also checks that two chips stay on one 40px line, that md and lg fit
three chips and the clear button on one line with a typing area of at
least 80px, that a folded table cell stays 32px without a placeholder
and unfolds on focus, the chip shape (radius, surface, 1px border,
tabular numbers), the locked chip (class, background, lock icon with
tooltip, accessible name) and the D1 cleanup (no onSelect, selectable,
separatorKeysCodes or autocomplete; isFocused is a signal).

Add three chip-calendar cases for the clear button size per size
(24/16, 28/18, 32/20). Drop the onSelect autocomplete spec from
chip.component.spec.ts and read isFocused as a signal. Add the
chip-locked-bg and chip-more-hover-bg pairs to the theme contrast test.

These specs fail until the code commit that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implement design handoff CH3 (C1-C4, D1, D2):

- C1/C2: a sd-chip-row($block, ...) mixin in core/form.scss styles both
  mat-form-field.sd-chip-field and .sd-chip-calendar-field. Field
  32/40/48, chip 20/24/28 centred, text 12/13/14, radius 6px, surface
  background, 1px --sd-border outline, tabular numbers, 4px gap,
  remove / lock icons 12/14/16 and square suffix buttons 24/28/32 with
  16/18/20 icons. size="lg" is now styled. The typing area keeps at
  least 80px so md and lg fit three chips and the clear button on one
  line, and a folded field (sd-chip-field--folded) lets the input
  shrink and drops the placeholder so a table cell stays 32px.
- C3: a chip that removable refuses gets --sd-chip-locked-bg, a lock
  icon with the "Không xoá được" tooltip and the name
  "{value}, Không xoá được".
- C4: chip.component.scss has no ::ng-deep, !important or hex left.
  c-md, .no-padding-wrapper and the 10px margin-top are gone.
- D1: remove onSelect, selectable, separatorKeysCodes, the unused
  autocomplete and progress spinner, the effect that assigned #name and
  SdRemovableChipPipe. isFocused is a signal, and the sdChanges
  subscription uses takeUntilDestroyed instead of ngOnDestroy. The
  label slot renders only with labelPlacement="top", like sd-date and
  sd-select.
- D2: rewrite sd-chip.md.

Add the --sd-chip-locked-bg and --sd-chip-more-hover-bg tokens. The
chip-calendar locked and "+N" hover tokens keep their names and default
to the shared ones. chip-calendar drops its ::ng-deep field block for
the mixin, which also fixes its clear button icon (16px at every size
before, now 18/20 at md/lg). Update THEME.md and CHANGELOG [Unreleased]
(Changed (BREAKING for consumers) with a migration diff, Added,
Changed, Fixed). The showcase size section shows sm / md / lg with a
locked chip. Roll out with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs
sdcorejs merged commit 2c99793 into main Oct 26, 2026
7 checks passed
sdcorejs added a commit that referenced this pull request Oct 26, 2026
…sd-page-list (E1) (#106)

* feat(badge): export SdBadgeData and use it for sd-table useBadge (PR 1)

First step of the page-layouts handoff (doc 02). SdBadgeData describes one
badge passed through an option instead of an <sd-badge> tag; sd-page
[badges] will reuse it in the next step.

- components/badge: new SdBadgeData { title, type, color, icon, label,
  tooltip }, exported from @sdcorejs/angular/components/badge. title keeps
  null so objects of the old internal Badge shape still type-check.
- components/table: UseBadgeFunc / UseBadgeValuesFunc return SdBadgeData;
  SdTableDisplay.badge gains label and tooltip; the cell binds label and
  uses the badge tooltip only when the column has no tooltip.
- Docs: SdBadgeData model section in sd-badge.md, badge shape in
  sd-table.md, CHANGELOG entry.
- Rolled out to v20/v21/v22 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(page): sd-page P0 — backLink, badges, width and new slots (PR 2)

Second step of the page-layouts handoff (doc 01).

sd-page
- backLink (URL or router commands) renders a real <a routerLink> before
  the title; the title, description and badges still come from inputs.
  headerLeft still replaces the left area and then drops backLink with a
  dev-mode warning.
- [badges]: SdBadgeData[] next to the title as md badges, round by
  default; badges without a title are skipped; more than two or a repeated
  type warn in dev mode.
- width: full (default) | wide 1200 | medium 960 | narrow 720. The column
  limit includes 24px side padding; subheader and footer use the same
  padding. The host gets sd-page--wide/medium/narrow.
- Slots: [sdSubheader] (in the header block, outside the scroll area),
  [sdAside] (320px sticky column, under the body below 1200px of
  viewport), [sdFooterLeft]/[sdFooterRight] (footer pinned to the bottom,
  hidden while empty), and [sdHeaderLeft]/[sdHeaderRight] aliases.
- The title renders in <h1 class="T20M"> with its margin reset; the
  component is OnPush.
- The new slots are projected without wrappers, so a page that uses none
  of them keeps its header and body DOM; only a hidden empty <footer> is
  added.

Also
- i18n: core.common.back and core.module.layout.page.actions in the five
  catalogs.
- Theme: component tokens page-divider, page-back-fg, page-back-hover-bg.
- Docs: new sd-page.md, sd-layout.md row, CHANGELOG (Added and Changed).
- Showcase: components/page demo (4 sections) and registry entry. The
  registry spec demo total goes from 450 to 457: 4 new sections plus 3
  chip sections added by #103-#105 without updating the count (showcase
  Karma does not run in CI). Route-shell test counts updated.
- Rolled out to v20/v21/v22 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(page): sd-page-detail and sd-page-form, toc and leave guard (PR 3)

Third step of the page-layouts handoff (docs 04 and 05).

sd-page
- sd-page-detail and sd-page-form are selectors of SdPageComponent; the
  host tag picks the default width (sd-page full, sd-page-detail wide,
  sd-page-form medium). An explicit width still wins.
- aside: 'end' (default) | 'start'. start puts the aside column first
  from 1200px of viewport; below that it stays under the body.
- toc lists the titled top-level sd-sections of the body in the aside
  column as a <nav> (core.module.layout.page.toc), scrolls to a section
  on click and marks the section at the top of the scroll area with
  aria-current. The list is collected after render, because a nested
  section only reaches its parent's DOM when the parent view renders.
  Hidden below 1200px. sd-anchor is not used: it wraps its own items and
  owns its scroll area, so it cannot list sections of the page body.

Leave guard
- sdPageFormLeaveGuard (CanDeactivateFn) asks through
  SdUnsavedChangesService with the Core confirm dialog (SdConfirmService)
  and the doc 05 texts, by passing its own adapter for that prompt.
  SD_UNSAVED_CHANGES_CONFIRMATION_ADAPTER and sdUnsavedChangesGuard keep
  their behaviour.
- SdUnsavedChangesConfirmOptions.adapter: per-call adapter.
- registerSdUnsavedChangesForm(..., { dirty: 'value' }): dirty only while
  the value differs from the snapshot; default 'control' is unchanged.

Also
- i18n: core.module.layout.page.toc and leave.{title,message,stay,leave}
  in the five catalogs.
- Docs: sd-page.md (selectors, aside, toc, leave guard),
  sd-unsaved-changes.md, sd-layout.md row, CHANGELOG.
- Showcase: two more page demos (detail with the aside first, form with
  a table of contents); registry demo total 459.
- Rolled out to v20/v21/v22 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(page-list): sd-page-list with the E1 height chain and sd-table fill mode (PR 4)

Fourth step of the page-layouts handoff (doc 03, acceptance E1 of doc 10).

sd-page-list (modules/layout)
- Wraps an sd-page, forwards headerRight, sdSubheader and the footer
  slots, and renders the router outlet for the detail child route.
- Height chain: the body is a 16px padded flex column (gap 12px) and the
  only scroll area; the main table (the last direct sd-table, or an
  [sdPageListMain] wrapper, chosen again by a MutationObserver) takes the
  space left.
- compact() = autoCompact (default true) and a viewport lower than 720px.
  The main table's external filter then starts collapsed; content reads
  compact() from SD_PAGE_LIST to fold its own summary.
- detail="page" hides the list (display: none) while a child route is
  open, then restores every scroll position it captured and the focus of
  the element that opened the child. detail="drawer" keeps the list.
- SD_PAGE_LIST gives the content compact, childActive and detail.

sd-table
- SD_TABLE_LAYOUT (new, in the table entry so the table does not depend
  on modules/layout) names a fill target. The table in it gets
  sd-table--fill: flex 1 1 0, a measured min-height (fixed parts +
  header + 5 rows, kept in sync by Mutation/ResizeObserver), a data area
  of at least header + 5 rows and a footer of at least 48px.
- filter.externalFilterCollapsed sets the initial collapsed state; the
  user's choice holds until the default changes. Not applied when
  collapsible is false.
- One-line rows are 36px (--mat-table-row-item-container-height),
  instead of Material's 52px, as agreed for every sd-table.

Showcase
- /demo/page-list: full-viewport live list (128 seeded orders) with
  ?cfg= toggles for tabs, score card, quick search, external filter,
  selection, record count, page size, read state, autoCompact and detail;
  child route :id/detail as a page or a 600px drawer.
- e2e/page-list.e2e.ts: E1-1..E1-6 at 1440x960, 1440x800, 1280x768 and
  1440x650 (runs in the asia-bangkok project only; layout does not depend
  on the zone). E1-4 measures the data area because the external filter
  lives inside sd-table.
- components/page-list docs page (2 demos); registry and route-shell
  counts updated.

Docs: sd-page-list.md, sd-table.md (row height, fill mode,
externalFilterCollapsed), sd-layout.md, CHANGELOG (Added; Changed for the
36px rows). Rolled out to v20/v21/v22 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(page-list): wait for page 2 rows before selecting in the E1-6 page test

On the CI runner the paginator label switched to 21-40 on click while the
rows were still those of page 1 until the delayed read landed, so the
test checked ĐH-2400 of page 1 and found the first row of page 2
unchecked after Back. Wait for the first row of page 2 (ĐH-2380) before
selecting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(page): render the sd-page header as <header>; run E1 on a bare sd-table screen

Two items of the handoff left out of the first four commits:
- doc 01 (a11y): the header of sd-page is a <header> element. Classes
  (c-page-header bg-white) and children are unchanged; the legacy DOM
  spec now checks the tag. sd-page.md and the CHANGELOG entry say so.
- doc 10 (E1): also run E1-1/2/3 on a screen with only sd-table (no quick
  search, no filter row, no footer buttons) at the four viewports.

v20-v22 are rolled out together with the merge of main that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: nghiatt15_onemount <nghiatt15@onemount.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants