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
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ This is a snapshot, not a commitment or a timeline. Items move, get reprioritize

- **More blocks** — remaining page templates (`register`, `forgot-password`, `settings-page`, `detail-page`, `wizard`, `pricing-page`) on top of the starter three.

## Component completeness

Optional leftover sibling gaps, still tracked as P32 in [`TODOLIST.md`](TODOLIST.md): combobox `ariaLabel`, select `required`, and toast `class` docs.

## Ecosystem / team use

- **Registry Directory** — a docs page listing community/third-party registries, so teams can discover each other's component sets.
- **Private registry authentication** — Bearer-token support for company-internal or private-repo registries.

## Recently shipped

- P32 sibling alignment — sheet built-in close and header `align`, radio `size`, a real dropdown-menu submenu, popover four-way `side` plus aria fallback, transfer root `disabled`/`ariaLabel`, and table sticky backgrounds plus loading/column-visibility recipes
- Blocks starter set — `sanring add block/login` (and `dashboard-shell`, `table-page`) installs a page-level template plus its component dependencies
- Open in StackBlitz — each component docs previewer can open a minimal Angular 22 + Tailwind project with that example
- GitHub registries — `--registry github:owner/repo` (optional `#ref` / `@ref`) expands to the raw `registry.json` at the repo root
Expand Down
39 changes: 39 additions & 0 deletions TODOLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,42 @@
**影響**:shadcn 的 registry 生態是目前採用率的核心驅動之一——開發者能找到、安裝、分享社群元件,讓整個 UI library 不只靠官方維護。Angular 生態目前沒有等價物,這是 Sanring 差異化的機會。

**成本**:高。各子項目可獨立交付。GitHub Registries 與 API Reference 已出貨;下一步是 Directory 和 Auth。

---

## P32 — 52 元件產品/API 掃描:對齊既有組件,不新增 primitive

2026-09-22 對 `packages/ui` 52 個正式元件做產品/API 掃描(公開 input/output、兄弟組件對齊、docs 是否超賣能力)。不是重跑 P3/P26/P30 的 a11y 稽核。`check-registry-parity.mjs` 與 `check-registry-sync.mjs` 當日皆綠(52/52)。官方目錄不缺新 primitive;該做的是把兄弟組件對齊,以及把 docs 已經講出去的能力做完。

0.25.0 已出(blocks / dialog header align / field textarea projection)。0.25.1 收斂 input/textarea/combobox count、combobox chip wrap、dialog header 獨立底色,以及 P32 sibling API。

### 該做

- [x] **table sticky 欄背景**:`sticky`/`stickyEnd` 已能傳進 CDK,但 CDK 只加 `position: sticky` + 位移,沒有不透明背景;捲動時文字會透出來。細節與「刻意不做」的 `CdkTextColumn` / CDK flex-layout 見 [packages/ui/src/lib/components/table/todolist.md](packages/ui/src/lib/components/table/todolist.md)
- [x] **table docs 配方**(不寫新元件):loading skeleton rows(既有 `skeleton`);欄位顯隱(dropdown-menu + checkbox + 動態 `sanringRowDefColumns`)。同上 todolist「使用模式」節
- [x] **sheet 對齊 dialog**:dialog content 已有內建 `showClose` / `closeAriaLabel`,header 已有 `align`;sheet 還要自己放 `[sanringSheetClose]`,`sanring-sheet-header` 沒有 `align`
- [x] **radio `size`**:checkbox / switch / otp-input 有 sm/md/lg;radio 寫死 `RADIO_SIZE_CLASS = 'aspect-square h-4 w-4'`(`radio.styles.ts`)
- [x] **dropdown-menu 真 submenu**:docs 鍵盤表寫了左右鍵開關子選單,官網範例卻是 `mouseenter` 切兩欄,不是巢狀 `menu`。checkbox / radio 範例用打勾圖示組出來即可,不必先做成一等 primitive
- [x] **popover `side` + aria fallback**:tooltip / hover-card 已有四向 `side`;popover 只做上/下,而且沒投影 title 時仍綁死 `aria-labelledby`(`popover-content.component.ts`)
- [x] **transfer 根層 `disabled` + `ariaLabel`**:現在只能 disable 單一 item(`TransferItem.disabled`);雙列表是沒名字的 `role="group"`

### 可選(有缺口,但已有組合路徑)

- [ ] **combobox** trigger/input 補 `ariaLabel` / `ariaLabelledBy`(已有 `sanring-combobox-label` 與 field 整合)
- [ ] **select** 補獨立 `required` input(現在只從 `Validators.required` 推導,`aria-required` 吃不到純 template `[required]`)
- [ ] **toast docs** 補 `ToastOptions.class`(型別已有,走 service,不要加元件級 `class` `@Input`)

### 明確不做(查證後不是缺口,避免下次掃描重開)

- 新 primitive(`kbd` / `chart` / `toggle-group` / `empty` 等)——官方方向在 blocks 與 registry 生態,不在再堆元件
- input / textarea 再包一層 `disabled` / `aria-*` input——原生 host 屬性 + `SanringFieldControl` 是刻意的薄 API
- toast 元件級 `class` `@Input`——走 `ToastOptions.class`
- table `CdkTextColumn`、CDK flex-layout `<cdk-table>`——見 table todolist,已明示不做
- dialog 加 `[(isOpen)]`——CDK Dialog 是 service 開啟;sheet / popover 的 `isOpen` model 是另一套 overlay。沒需求不要硬對齊
- 每個控制項都加 `size`——field 高度契約是共用的 `FIELD_SIZE_CLASS`;radio 是唯一跟 checkbox/switch 並排會明顯不齊的

**現況**:52 個元件裡約 44 個判定維持。alert-dialog 沒有自己的 `class` 沒關係(複用 dialog 零件,`showClose` 預設關是對的)。dropdown-menu 的 checkbox/radio docs 範例是組合解法,不是假文件。

**影響**:不處理的話,消費者會在「看起來該有的兄弟 API」上卡關(sheet 關閉鈕、radio 尺寸、popover 左右、transfer 整組停用),或照著 dropdown-menu 鍵盤表做出不能用方向鍵開的子選單。

**成本**:中。各子項可獨立交付;table docs 配方最低、sheet/radio 次之、dropdown-menu submenu 最重(要接 `@angular/aria/menu` 巢狀,不能沿用現在的兩欄 hover)。
6 changes: 6 additions & 0 deletions apps/docs/src/app/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,10 @@ export const commonTranslations = {
'status.maintenance.title': 'Under maintenance',
'status.maintenance.description':
'This component is temporarily under maintenance and hidden from production navigation. It remains reachable directly while we work on it.',
'combobox.demo.multipleSingleLine': 'Multiple, single line',
'combobox.demo.multipleMultiLine': 'Multiple, multi-line',
'combobox.api.chipInputWrap.description':
'When false, chips and the search input stay on one row instead of wrapping.',
'combobox.api.chipsWrap.description':
'When false, selected chips stay on one line and overflow with an ellipsis.',
} as const;
7 changes: 7 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/combobox.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Combobox docs copy. */
export const comboboxTranslations = {
'combobox.description': 'Autocomplete input with a list of suggestions.',
'combobox.examples.basic.description':
Expand Down Expand Up @@ -36,6 +37,12 @@ export const comboboxTranslations = {
'Selected value controlled by the root. Use a string for single select or string array for multiple select.',
'combobox.api.multiple.description':
'Allows selecting more than one item and pairing the field with chips.',
'combobox.demo.multipleSingleLine': 'Multiple, single line',
'combobox.demo.multipleMultiLine': 'Multiple, multi-line',
'combobox.api.chipInputWrap.description':
'When false, chips and the search input stay on one row instead of wrapping.',
'combobox.api.chipsWrap.description':
'When false, selected chips stay on one line and overflow with an ellipsis.',
'combobox.api.disabled.description':
'Disables the combobox input and prevents selection changes.',
'combobox.api.inputId.description':
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/src/app/i18n/locales/en/components/dialog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Dialog docs copy. */
export const dialogTranslations = {
'dialog.description':
'An overlay primitive built on Angular CDK Dialog for modal tasks and focused decisions.',
Expand All @@ -8,7 +9,7 @@ export const dialogTranslations = {
'dialog.demo.noClose': 'No Close Button',
'dialog.demo.stickyFooter': 'Sticky Footer',
'dialog.demo.scrollable': 'Scrollable Content',
'dialog.demo.header': 'Header alignment',
'dialog.demo.header': 'Header alignment and background',
'dialog.examples.description':
'Common dialog patterns for custom actions, hidden close controls, sticky actions, and dense scrollable content.',
'dialog.examples.basic.description':
Expand All @@ -35,6 +36,8 @@ export const dialogTranslations = {
'dialog.api.mediaClass.description': 'Additional classes merged with the dialog media container.',
'dialog.api.headerAlign.description':
'Header text alignment. start is left, center is centered at every breakpoint. The default stays centered on small screens and left-aligned from sm up.',
'dialog.api.headerClass.description':
'Additional classes merged with the header layout. Use this to give the header a different background from sanring-dialog-content, for example bg-[var(--sanring-surface-strong)]. Pair with overflow-hidden p-0 on content so the fill reaches the panel edges.',
'dialog.api.titleClass.description':
'Additional classes merged with the title styles. Use this to change title color, for example text-[var(--sanring-primary-70)].',
'dialog.accessibility.description':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const dropdownMenuTranslations = {
'Controls item tone. Use destructive for actions that remove data or have serious consequences.',
'dropdownMenu.api.class.description':
'Additional classes merged with the corresponding dropdown menu primitive.',
'dropdownMenu.api.submenu.description':
'The nested menu to open, bound to the sub-content export (#ref="sanringDropdownMenuSubContent", then [submenu]="ref.menu"). Arrow Right opens it; Arrow Left closes it.',
'dropdownMenu.api.subTriggerValue.description':
'Required by the underlying ARIA menu item. Use a distinct value from sibling items.',
'dropdownMenu.accessibility.description':
"Built on @angular/aria/menu. The panel has role='menu'; each item has role='menuitem', role='menuitemcheckbox', or role='menuitemradio' as appropriate. The trigger button has aria-haspopup='menu' and aria-expanded managed by the underlying MenuTrigger directive.",
'dropdownMenu.keyboard.description': 'Keyboard navigation follows the WAI-ARIA menu pattern.',
Expand All @@ -41,5 +45,5 @@ export const dropdownMenuTranslations = {
'dropdownMenu.keyboard.openSubmenu': 'Open the focused submenu.',
'dropdownMenu.keyboard.closeSubmenu': 'Close the active submenu.',
'dropdownMenu.stateModel.description':
"Stateless. DropdownMenuItemDirective emits (itemSelected) on activation. DropdownMenuCheckboxItemComponent and DropdownMenuRadioGroupComponent manage their own checked/selected state via the checked input and checkedChange output. Open/closed state is managed internally by @angular/aria/menu.",
'Stateless. DropdownMenuItemDirective emits (itemSelected) on activation. Nest sanring-dropdown-menu-sub with a sub-trigger and sub-content for a real submenu. Checkbox and radio examples compose icons onto ordinary items. Open/closed state is managed internally by @angular/aria/menu.',
} as const;
10 changes: 9 additions & 1 deletion apps/docs/src/app/i18n/locales/en/components/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const popoverTranslations = {
'popover.examples.description':
'Common popover patterns: alignment, user profile overlay, and form controls.',
'popover.demo.align': 'Align',
'popover.demo.side': 'Side',
'popover.api.side.description':
"Preferred side of the trigger: 'top', 'right', 'bottom' (default), or 'left'. The panel flips if it would overflow the viewport.",
'popover.api.sideOffset.description': 'Distance in pixels between the trigger and the panel.',
'popover.api.ariaLabel.description':
'Accessible-name fallback used when no sanring-popover-title is projected and ariaLabelledBy is unset.',
'popover.api.ariaLabelledBy.description':
'Ids of external elements that label the panel. Takes precedence over PopoverTitle and ariaLabel.',
'popover.demo.withHeader': 'With Header',
'popover.demo.profile': 'User profile',
'popover.demo.profileEmail': 'jane@example.com',
Expand All @@ -24,7 +32,7 @@ export const popoverTranslations = {
"Alignment relative to the trigger: 'start', 'center' (default), or 'end'.",
'popover.api.class.description': 'Additional classes merged onto the floating panel.',
'popover.accessibility.description':
"The trigger button has aria-haspopup='dialog', aria-expanded, and aria-controls pointing to the panel id. The panel carries role='dialog'. Include sanringPopoverTitle or sanringPopoverDescription to have aria-labelledby and aria-describedby wired automatically.",
"The trigger button has aria-haspopup='dialog', aria-expanded, and aria-controls pointing to the panel id. The panel carries role='dialog'. Project sanring-popover-title to wire aria-labelledby, or set ariaLabel / ariaLabelledBy when the panel has no title.",
'popover.keyboard.description': 'Focus moves into the panel when it opens.',
'popover.keyboard.escape': 'Close the popover panel and return focus to the trigger.',
'popover.keyboard.tab': 'Move focus to the next focusable element inside the panel.',
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const radioTranslations = {
'Common patterns including labeled items, horizontal layout, and disabled states.',
'radio.demo.withLabel': 'With Labels',
'radio.demo.horizontal': 'Horizontal',
'radio.demo.size': 'Size',
'radio.demo.size.sm': 'Small',
'radio.demo.size.md': 'Medium',
'radio.demo.size.lg': 'Large',
'radio.api.group.size':
'Control size inherited by every item in the group. Matches checkbox and switch: sm, md (default), or lg.',
'radio.demo.disabled': 'Disabled',
'radio.demo.field': 'With Field',
'radio.demo.planFree': 'Free',
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const sheetTranslations = {
'sheet.api.isOpen.description': 'Controls the open state. Supports [(isOpen)] two-way binding.',
'sheet.api.side.description':
"Edge from which the panel slides in: 'top', 'right' (default), 'bottom', or 'left'.",
'sheet.api.showClose.description': 'Controls whether the built-in close button is rendered.',
'sheet.api.closeAriaLabel.description':
'Accessible name for the built-in close button. Defaults to 關閉面板.',
'sheet.api.headerAlign.description':
'Header text alignment. start is left, center is centered at every breakpoint. The default stays centered on small screens and left-aligned from sm up.',
'sheet.demo.customClose': 'Custom Close Button',
'sheet.demo.confirmDelete': 'Confirm deletion',
'sheet.demo.confirmDeleteDescription': 'This action cannot be undone.',
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const tableTranslations = {
'table.demo.sortable': 'Sortable headers',
'table.demo.columnSizing': 'Column sizing',
'table.demo.sticky': 'Sticky columns',
'table.demo.loading': 'Loading skeleton',
'table.demo.columnVisibility': 'Column visibility',
'table.demo.empty': 'Empty state',
'table.demo.selection': 'Row selection',
'table.demo.actions': 'Actions menu',
Expand Down
7 changes: 6 additions & 1 deletion apps/docs/src/app/i18n/locales/en/components/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const transferTranslations = {
'transfer.demo.moveToTarget': 'Move to selected',
'transfer.demo.moveToSource': 'Move to available',
'transfer.demo.disabled': 'Disabled items',
'transfer.demo.disabledGroup': 'Disabled group',
'transfer.api.disabled.description':
'Disables the whole control: items cannot be checked and neither panel can move items.',
'transfer.api.ariaLabel.description':
'Accessible name for the role="group" root. Use this so the dual list is announced as one control.',
'transfer.demo.headerCount': 'Header with live count',
'transfer.demo.customActions': 'Custom action buttons',
'transfer.demo.oneWay': 'One-way transfer',
Expand Down Expand Up @@ -57,7 +62,7 @@ export const transferTranslations = {
'transfer.api.pageNav.description':
'Move to the next/previous page. No-ops past either end of the range.',
'transfer.api.interactive.description':
'False for a target panel in one-way mode; used internally to disable its checkboxes.',
'False when the root is disabled, or for a target panel in one-way mode; used to disable that panel’s checkboxes.',
'transfer.api.selectableItems.description':
'Non-disabled items across the full filtered list (not limited to the current page). Useful for displaying a count in the header.',
'transfer.api.selectAllChecked.description':
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/i18n/locales/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { commonTranslations } from './common';
import { componentTranslations } from './components';
import { pageTranslations } from './pages';

/** Locale catalog for English docs copy. */
export const en = {
...commonTranslations,
...pageTranslations,
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/app/i18n/locales/zh/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,10 @@ export const commonTranslations = {
'status.maintenance.title': '維護中',
'status.maintenance.description':
'此元件目前維護中,已從正式環境的導覽中暫時移除;維護期間仍可透過直接連結訪問。',
'combobox.demo.multipleSingleLine': '多選、單行',
'combobox.demo.multipleMultiLine': '多選、多行',
'combobox.api.chipInputWrap.description':
'設為 false 時,chips 與搜尋框維持單行,不再換行。',
'combobox.api.chipsWrap.description':
'設為 false 時,已選 chips 維持單行,超出輸入框寬度時以刪節號收斂。',
} as const;
10 changes: 9 additions & 1 deletion apps/docs/src/app/i18n/locales/zh/components/combobox.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Combobox docs copy. */
export const comboboxTranslations = {
'combobox.description': '具備建議清單的自動完成輸入元件。',
'combobox.examples.basic.description':
Expand Down Expand Up @@ -33,7 +34,14 @@ export const comboboxTranslations = {
'combobox.api.description': 'combobox primitives 支援的 inputs 與 models。',
'combobox.api.value.description':
'由 root 控制的選取值。單選使用 string,多選使用 string array。',
'combobox.api.multiple.description': '允許選取多個項目,並可搭配 chips 呈現。',
'combobox.demo.multipleSingleLine': '多選、單行',
'combobox.demo.multipleMultiLine': '多選、多行',
'combobox.api.chipInputWrap.description':
'設為 false 時,chips 與搜尋框維持單行,不再換行。',
'combobox.api.chipsWrap.description':
'設為 false 時,已選 chips 維持單行,超出輸入框寬度時以刪節號收斂。',
'combobox.api.multiple.description':
'允許選取多個項目,並可搭配 chips 呈現。',
'combobox.api.disabled.description': '停用 combobox input,並阻止選取狀態變更。',
'combobox.api.inputId.description':
'input 或自訂 trigger 與其 label 共用的 ID;未提供時自動產生,也可覆寫以串接應用程式內的關聯。',
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/src/app/i18n/locales/zh/components/dialog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Dialog docs copy. */
export const dialogTranslations = {
'dialog.description':
'建立在 Angular CDK Dialog 上的 overlay primitive,適合 modal 任務與聚焦決策。',
Expand All @@ -8,7 +9,7 @@ export const dialogTranslations = {
'dialog.demo.noClose': '沒有關閉按鈕',
'dialog.demo.stickyFooter': '固定頁尾',
'dialog.demo.scrollable': '可捲動內容',
'dialog.demo.header': '標題對齊',
'dialog.demo.header': '標題對齊與背景',
'dialog.examples.description':
'常見 Dialog 模式,包含自訂操作、隱藏關閉控制、固定操作區與大量可捲動內容。',
'dialog.examples.basic.description':
Expand All @@ -34,6 +35,8 @@ export const dialogTranslations = {
'dialog.api.mediaClass.description': '與 dialog media 容器合併的額外 class。',
'dialog.api.headerAlign.description':
'標題列文字對齊。start 靠左,center 在所有斷點置中。預設維持小螢幕置中、sm 以上靠左。',
'dialog.api.headerClass.description':
'與 header 版面樣式合併的額外 class。用來讓標題列背景跟 sanring-dialog-content 不同,例如 bg-[var(--sanring-surface-strong)]。搭配 content 的 overflow-hidden p-0,底色才能貼齊面板邊緣。',
'dialog.api.titleClass.description':
'與標題樣式合併的額外 class。用來改標題顏色,例如 text-[var(--sanring-primary-70)]。',
'dialog.accessibility.description':
Expand Down
Loading
Loading