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 .changeset/blocks-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sanring/cli': minor
---

Add a `blocks` registry category so `sanring add block/login` can install page-level templates, and accept `github:owner/repo` as a registry source.
5 changes: 5 additions & 0 deletions .changeset/dialog-header-field-textarea.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sanring/cli': patch
---

Let dialog headers opt into start/center alignment, and project textareas into the field control slot so character-count descriptions sit below the control.
33 changes: 33 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -953,3 +953,36 @@ P30 先前已收完 15 個必修缺口;這輪把剩下 15 組建議項目逐
**測試抓到並修正的真實發布缺陷**:第一次走到 production build 時,乾淨專案無法 resolve `@angular/cdk/a11y`。根因是所有使用 `shared/utils.ts` 的元件都會間接需要 CDK(`uniqueId()` 使用 `_IdGenerator`),但 `registry.json` 的 `utils.peerDependencies` 只列 `clsx`/`tailwind-merge`;repo 與既有 mock tests 本身早已有 CDK,所以缺口一直被遮住。已補 `@angular/cdk: ^22.0.0`,因此 `sanring add button` 現在會自動安裝它。進一步把 `build.test.ts` golden fixture 從只比較 52 個 component metadata 擴大到所有 shared entries 後,又抓出 `collection-controller.ts` 同樣直接 import CDK 卻沒宣告,一併校正;新的 shared peer-dependency comparison 會阻止這一類 drift 再發生。CLI patch changeset 已加入。

**驗證**:本機 fresh run 實際安裝 Angular 22.1.3/CLI 22.1.5,完整 `packed CLI → npm install → sanring init → sanring add button → import installed source → production ng build` 連續通過,production bundle 142.33 kB。`build.test.ts` targeted golden fixture **11/11 passed**,shared/component metadata 零已知落差;完整 CLI suite **19 files / 240 tests passed**。repo 全域 lint、CLI main/schematics TypeScript、registry sync/parity、Changesets status、CI YAML 與 `git diff --check` 皆通過。

---

## P19 — Blocks 起手三個(`dashboard-shell` / `login` / `table-page`)

- [x] `registry/blocks/` + `registry.json` `blocks[]` + CLI `block/` prefix
- [x] 起手三個:`dashboard-shell`(shell)、`login`(page)、`table-page`(page)
- [x] Docs `/blocks` 頁與 `sanring add block/login` 說明

**已完成**:blocks 是獨立的 registry 類別,不進 `packages/ui`(否則會被 8-way `check-registry-sync` 當正式元件抓漏)。名稱扁平(`dashboard-shell` / `login` / `table-page`),CLI 用 `block/` prefix 跟 alias 的 `:` 分開。安裝後落到跟元件一樣的 `src/app/components/ui/<name>/`,互相用 `../card` 這種相對路徑。docs preview 用 `@sanring/ui` 重畫,不能直接 compile registry blocks(因為 `../card` 對不到 `registry/components/card`)。

**踩過的坑**:`login` 不能同時 import `FieldLabelDirective` 與獨立的 `LabelDirective`(同一個 selector)。`add.test.ts` 的 commander singleton 會把前一個測試的 `--dry-run` 留到下一個,block 安裝看起來成功但其實沒寫檔——`beforeEach` 現在會重設 `dryRun`/`check`/`diff`/`view`/`force`/`yes`/`registry`。

**未做**:其餘六個 page block 仍在 TODOLIST。

---

## P22 — Docs component 頁面 Open in StackBlitz

- [x] 每個 component 頁面的 code previewer 旁加「Open in StackBlitz」

**已完成**:共用 `ComponentPageCodePreviewer` 在 URL 是 `/components/:id` 時顯示按鈕,runtime fetch `/registry/registry.json` 與對應 source(`angular.json` 把 `registry/` 當 docs assets),再用 `@stackblitz/sdk` 的 `sdk.openProject({ template: 'node' })` 開一份最小 Angular 22 + Tailwind 專案。不用 EngineBlock 的 `angular-cli` template,那個太舊。原本規劃的 `generate:stackblitz-registry` 預產生腳本沒寫也沒需要,已從 `package.json` 拿掉。

**刻意不做**:docs 頁內嵌可編輯 editor(跟 shadcn 一樣打平);blocks 頁的 previewer 不顯示按鈕(URL 不是 `/components/:id`)。

---

## P25 — GitHub Registries + registry.json API Reference

- [x] CLI `github:owner/repo`(`#ref` / `@ref`)展開成 raw `registry.json`
- [x] Docs Registry 頁補完整欄位定義(型別、必要/選用)

**已完成**:`expandGithubRegistrySource` 在 `fetchRegistry` / `fetchFile` 進路徑判斷前先 normalize,避免 `github:` 被當成本地路徑。Docs 加了 GitHub 範例、root / item / shared / group / migration 五張 API 表。Directory、namespaces、auth、search API、docs 多頁拆分仍在 TODOLIST,這輪不做。
10 changes: 6 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ This is a snapshot, not a commitment or a timeline. Items move, get reprioritize

## Adoption experience

- **Blocks** — installable page-level templates (login page, dashboard shell, settings page) via `sanring add block/dashboard-shell`, so you're not always assembling pages from individual components. This is the biggest adoption-experience gap compared to shadcn today.
- **Interactive theme builder** — a live color/radius preview on the docs site with copy-to-clipboard CSS. (The named starting points this was paired with — Slate, Warm, High-Contrast — already shipped via `sanring init --theme <preset>`.)
- **Try without installing** — an "Open in StackBlitz" shortcut on each component's docs page.
- **More blocks** — remaining page templates (`register`, `forgot-password`, `settings-page`, `detail-page`, `wizard`, `pricing-page`) on top of the starter three.

## Ecosystem / team use

- **Registry Directory** — a docs page listing community/third-party registries, so teams can discover each other's component sets.
- **GitHub registries** — point the CLI at `github:<owner>/<repo>` directly, without hosting a raw `registry.json` yourself.
- **Private registry authentication** — Bearer-token support for company-internal or private-repo registries.

## Recently shipped

- 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
- `registry.json` API reference — field, type, and required/optional docs on the Registry page
- Interactive theme builder — a live color/radius preview on the docs site with copy-to-clipboard CSS
- Packaged CLI end-to-end quality gate — CI installs the local tarball into a freshly scaffolded
Angular app, runs `sanring init` and `sanring add button`, imports the installed source, and
requires a successful production build
Expand Down
42 changes: 7 additions & 35 deletions TODOLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,22 @@

---

## P19 — Blocks:可直接安裝的頁面級組合模板
## P19 — Blocks:其餘六個頁面模板

- [ ] 設計 `blocks/` registry 類別,讓 `sanring add block/dashboard-shell` 可以一次安裝完整頁面片段(login page、settings page、data table page、dashboard layout 等)
起手三個(`dashboard-shell`、`login`、`table-page`)已出貨,見 [DEVLOG.md](DEVLOG.md)。剩下是覆蓋率,不是機制。

**現況**:目前 registry 只有 `components/` 和 `shared/`,沒有 blocks 概念。使用者必須自己把元件組裝成頁面。
- [ ] `register`、`forgot-password`、`settings-page`、`detail-page`、`wizard`、`pricing-page`

**影響**:這是目前與 shadcn 最大的採用體驗差距。開發者的採用決策通常不是「這個 Button 好不好」,而是「我能不能 30 分鐘內搭出一個看起來像樣的登入頁」。Blocks 直接回答這個問題。shadcn blocks 是近兩年對採用率貢獻最大的功能之一。
**頁面類型與元件組合**(2026-08-22 盤點):

**實作方向**:

- `registry/blocks/` 目錄,每個 block 是一個 Angular component(可含多個 child component)
- `registry.json` 加入 `blocks` 陣列(類似 `components`),每筆有 `name`、`description`、`componentDeps`、`files`
- CLI 的 `add` 指令識別 `block/` prefix,路由到 blocks registry
- Block 分兩類,架構不同:**shell**(包住整個 app 的持久性 chrome,例如 `layout/dashboard-shell`,包一個 `<ng-content>`/router-outlet,不是「一頁內容」)與 **page**(一頁完整內容組合,裝進某個 route)

**頁面類型與元件組合**(2026-08-22 盤點,已對照現有 52 個 component 逐一核對,全部可用現有元件組成,沒有缺元件擋路,不必先補元件才能動工):

- `layout/dashboard-shell`(shell):`sidebar` + `dropdown-menu`(user menu)+ `avatar` + `breadcrumb` + `badge`
- `auth/login`(page):`card` + `field` + `input` + `label` + `button` + `checkbox`(remember me)+ `link`(forgot password)+ `divider`(or)+ `alert`(錯誤訊息)
- `auth/register`(page):同 login + `select`(可選:角色/國家)
- `auth/forgot-password`(page):`card` + `field` + `input` + `button` + `alert`
- `layout/settings-page`(page):`tabs`(分區)+ `field` + `input` + `avatar`(頭像上傳)+ `switch` + `select` + `divider` + `button` + `alert-dialog`(刪除確認)
- `data/table-page`(page):`table` + `pagination` + `input`(搜尋)+ `select`(篩選)+ `dropdown-menu`(列操作)+ `checkbox`(批次選取)+ `badge`(狀態)+ `sheet`(新增/編輯抽屜)+ `skeleton` + `toast`
- `content/detail-page`(page):`card` + `avatar` + `badge` + `tabs` + `breadcrumb` + `timeline` + `tag`
- `form/wizard`(page):`stepper` + `field` + `input` + `select` + `date-picker` + `radio` + `file-upload` + `button` + `progress`
- `billing/pricing-page`(page):`card` + `badge` + `table` + `toggle`(月/年切換)+ `button` + `tag`

**起手三個**(驗證 CLI 機制,而非追求覆蓋率):`layout/dashboard-shell`(驗證 shell 型 block 的安裝機制跟一般 component block 不同)、`auth/login`(元件最單純,驗證 `blocks/` 目錄結構/`registry.json` blocks schema/CLI routing 三件事都跑得通)、`data/table-page`(驗證單一 block 內多個子元件互相協作的複雜組合)。其餘六個(`register`、`forgot-password`、`settings-page`、`detail-page`、`wizard`、`pricing-page`)待前三個跑通機制後再逐步擴充。

**成本**:高。單個 block 的設計/實作本身不難,但要做出夠多、夠有代表性的 blocks 讓功能有意義,需要持續投入。先做上述 3 個驗證 CLI 流程,再逐步擴充其餘 6 個。
**成本**:中。CLI `block/` prefix、`registry.json` `blocks[]`、docs `/blocks` 頁都已就位,剩下是逐個組裝。

---

Expand All @@ -46,27 +32,13 @@
**子項目(依實作順序)**:

- [ ] **Registry Directory**:Docs 站新增第三方 registry 目錄頁,列出社群維護的 registry(類似 shadcn 的 Registry Directory);初期可由人工審核提交
- [ ] **GitHub Registries**:CLI 支援直接以 `github:<owner>/<repo>` 格式作為 registry source,省去 host 步驟(背後解析為 `https://raw.githubusercontent.com/<owner>/<repo>/main/registry.json`)
- [ ] **Namespaces**:解決多 registry 同名元件衝突,定義 namespace 規則(目前 alias 機制已部分解決,需形式化)
- [ ] **Authentication**:CLI 支援 private registry 的 Bearer token 認證(`sanring.config.json` 加入 `auth` 欄位),讓企業內網或 private GitHub repo 可用
- [ ] **Dynamic Search API**:registry 可選擇暴露搜尋 endpoint(而非只靠靜態 JSON 全量掃描),`sanring search` 優先呼叫 endpoint
- [ ] **API Reference 頁面**:補 `registry.json` 完整 schema 文件(目前 docs registry 頁只有範例,缺欄位定義、型別、必要/選用標記)
- [ ] **Docs 多頁拆分**:registry 頁面從目前的單頁拆成多頁(Introduction、Getting Started、GitHub Registries、Authentication、API Reference 等)

**現況**:目前 registry 頁只有一頁,涵蓋基本的 `sanring build` 工作流程與 `registries` config 設定。CLI 的 multi-registry 支援(alias:name 語法)已完成,但生態系的其餘部分(directory、GitHub source、auth)尚未實作。
**現況**:`github:<owner>/<repo>` source 與 `registry.json` API Reference 已出貨,見 [DEVLOG.md](DEVLOG.md)。CLI 的 multi-registry 支援(alias:name 語法)與 GitHub source 可用。生態系其餘部分(directory、namespaces、auth、search API、docs 多頁拆分)尚未實作。

**影響**:shadcn 的 registry 生態是目前採用率的核心驅動之一——開發者能找到、安裝、分享社群元件,讓整個 UI library 不只靠官方維護。Angular 生態目前沒有等價物,這是 Sanring 差異化的機會。

**成本**:高。各子項目可獨立交付,建議從 GitHub Registries(低實作成本、高使用者價值)和 API Reference 開始,再推進 Directory 和 Auth。

---

## P22 — Docs component 頁面加入 StackBlitz 快捷連結

- [ ] 每個 component 頁面的 code previewer 旁加一個「Open in StackBlitz」按鈕,讓使用者不用本地安裝就能試用

**現況**:Docs 的 code previewer 是靜態展示,使用者若想動手試要先本地建好 Angular 專案並跑完 `sanring init` + `sanring add`。

**差異**:這裡的目標是「一鍵開啟含有該元件的最小 Angular 專案」,而非在 docs 頁面內嵌入可編輯 editor(已確認 shadcn 自己的 docs 也不這樣做,兩邊打平)。StackBlitz 支援從 URL params 或 POST 預填專案內容,可以把 component 程式碼預先注入。

**成本**:中。StackBlitz SDK 有 `sdk.openProject()` API,需要為每個元件準備一份最小化的 Angular 專案 template + 注入對應的元件程式碼。可以先做成通用 template,再逐元件補範例程式碼。
**成本**:高。各子項目可獨立交付。GitHub Registries 與 API Reference 已出貨;下一步是 Directory 和 Auth。
5 changes: 5 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
"glob": "CHANGELOG.md",
"input": "packages/cli",
"output": "/data/cli-changelog"
},
{
"glob": "**/*",
"input": "registry",
"output": "/registry"
}
],
"styles": ["apps/docs/src/styles.css"]
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export const routes: Routes = [
(m) => m.RegistryPageComponent,
),
},
{
path: 'blocks',
loadComponent: () =>
import('./pages/blocks/blocks-page.component').then((m) => m.BlocksPageComponent),
},
{
path: 'changelog',
redirectTo: 'version-notes',
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/src/app/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const commonTranslations = {
'actions.copyPageOptions': 'Copy page options',
'actions.viewAsMarkdown': 'View as Markdown',
'actions.openFailed': 'Open failed',
'actions.openInStackBlitz': 'Open in StackBlitz',
'actions.openingStackBlitz': 'Opening…',
'actions.clearSearch': 'Clear search',
'actions.previousPage': 'Previous page',
'actions.nextPage': 'Next page',
Expand All @@ -37,6 +39,7 @@ export const commonTranslations = {
'sidebar.skills': 'Skills',
'sidebar.mcpServer': 'MCP Server',
'sidebar.registry': 'Registry',
'sidebar.blocks': 'Blocks',
'sidebar.forms': 'Forms',
'sidebar.changelog': 'Version Notes',
'sidebar.roadmap': 'Roadmap',
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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.examples.description':
'Common dialog patterns for custom actions, hidden close controls, sticky actions, and dense scrollable content.',
'dialog.examples.basic.description':
Expand All @@ -32,6 +33,10 @@ export const dialogTranslations = {
'dialog.api.closeResult.description':
'Optional result value emitted when sanringDialogClose closes the dialog.',
'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.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':
"The CDK Dialog container receives role='dialog' and aria-modal='true'. Projected titles and descriptions are wired automatically; ariaLabel provides a fallback name for untitled content. Angular CDK's FocusTrap keeps Tab and Shift+Tab cycling within the open dialog.",
'dialog.keyboard.description': 'Focus is trapped inside the dialog while it is open.',
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/i18n/locales/en/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const inputTranslations = {
'input.demo.disabled': 'Disabled',
'input.demo.file': 'File',
'input.demo.validation': 'Validation state',
'input.demo.characterCount': 'Character count',
'input.examples.description':
'Common input patterns for editable text, disabled fields, and file uploads.',
'input.examples.basic.description':
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/i18n/locales/en/components/textarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const textareaTranslations = {
'Use sanringTextarea on native textarea elements and keep value, disabled, rows, and form bindings native.',
'textarea.demo.disabled': 'Disabled',
'textarea.demo.resize': 'Resizable',
'textarea.demo.characterCount': 'Character count',
'textarea.api.description': 'Inputs supported by the sanringTextarea directive.',
'textarea.api.class.description': 'Additional classes merged with the base textarea styles.',
'textarea.accessibility.description': 'A transparent styling directive that preserves native <textarea> semantics. Works with standard HTML attributes — aria-label, aria-labelledby, aria-describedby — applied directly to the textarea element. Pair with sanring-field for automatic label association and validation wiring.',
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/src/app/i18n/locales/en/pages/blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const blocksTranslations = {
'blocks.page.description':
'Installable page-level templates. One command copies the block and the components it is built from.',
'blocks.overview.title': 'Overview',
'blocks.overview.body':
'Blocks are composed from existing Sanring components. They are not part of the UI package — the CLI copies them into your app so you can edit the source. Use the block/ prefix when you want to be explicit, or the bare name when it does not collide with a component.',
'blocks.install.title': 'Install',
'blocks.dashboard.title': 'Dashboard shell',
'blocks.dashboard.body':
'Persistent app chrome: sidebar, breadcrumbs, and a user menu. Project your page into ng-content.',
'blocks.login.title': 'Login',
'blocks.login.body': 'A sign-in card with email, password, remember-me, and an error alert.',
'blocks.table.title': 'Table page',
'blocks.table.body':
'A data table with search, status filter, row selection, a create sheet, loading skeletons, and toast.',
} as const;
Loading
Loading