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/sortable-timeline-mcp-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sanring/cli": minor
---

Add `sortable` for pointer and keyboard reordering. Timeline separators now draw the rail and default marker. MCP adds `get_component_spec` so agents get authoring contracts instead of inventing APIs.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Components are copied directly into your source code — you own them and can mo

## Available Components

accordion · alert · alert-dialog · aspect-ratio · avatar · badge · breadcrumb · button · calendar · card · carousel · checkbox · collapsible · combobox · command · context-menu · date-picker · dialog · divider · dropdown-menu · field · file-upload · hover-card · input · label · link · navigation-menu · otp-input · pagination · popover · progress · radio · resizable · scroll-area · select · sheet · sidebar · skeleton · slider · spinner · stepper · switch · table · tabs · tag · textarea · timeline · toast · toggle · tooltip · transfer · tree
accordion · alert · alert-dialog · aspect-ratio · avatar · badge · breadcrumb · button · calendar · card · carousel · checkbox · collapsible · combobox · command · context-menu · date-picker · dialog · divider · dropdown-menu · field · file-upload · hover-card · input · label · link · navigation-menu · otp-input · pagination · popover · progress · radio · resizable · scroll-area · select · sheet · sidebar · skeleton · slider · sortable · spinner · stepper · switch · table · tabs · tag · textarea · timeline · toast · toggle · tooltip · transfer · tree

---

Expand Down
Binary file added apps/docs/public/avatars/lantern.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/public/avatars/night-lantern.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/public/avatars/path.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/public/avatars/stairs.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions apps/docs/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ export const routes: Routes = [
(m) => m.SliderPageComponent,
),
},
{
path: 'sortable',
loadComponent: () =>
import('./pages/components/sortable/sortable-page.component').then(
(m) => m.SortablePageComponent,
),
},
{
path: 'spinner',
loadComponent: () =>
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const commonTranslations = {
'component.sidebar': 'Sidebar',
'component.skeleton': 'Skeleton',
'component.slider': 'Slider',
'component.sortable': 'Sortable',
'component.stepper': 'Stepper',
'component.switch': 'Switch',
'component.table': 'Table',
Expand Down
9 changes: 7 additions & 2 deletions apps/docs/src/app/i18n/locales/en/components/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const avatarTranslations = {
'avatar.demo.sizes': 'Sizes',
'avatar.demo.statusBadge': 'Status badge',
'avatar.demo.badgeWithIcon': 'Badge with icon',
'avatar.demo.badgeCount': 'Notification count',
'avatar.examples.badgeCount.description':
'Pass count to render a numeric pill. It defaults to the top-end corner so it can sit with a status dot at the bottom. 0 hides the pill; values above 99 render as 99+.',
'avatar.demo.group': 'Avatar group',
'avatar.demo.groupWithIcon': 'Avatar group with icon',
'avatar.examples.description':
Expand All @@ -24,7 +27,9 @@ export const avatarTranslations = {
'avatar.api.delayMs.description': 'Delay in milliseconds before the fallback is shown.',
'avatar.api.status.description': 'Controls badge color: online, offline, away, busy, or default.',
'avatar.api.placement.description':
'Places the badge at the visual start or end edge, respecting RTL direction.',
'Places the badge at start, end, top, or bottom. Status badges default to end (bottom-end); count badges default to top (top-end). start/end follow RTL.',
'avatar.api.badgeCount.description':
'Unread count on [sanringAvatarBadge]. Hidden at 0 or below; values above 99 render as 99+.',
'avatar.api.overlap.description': 'Stack overlap amount in rem for avatar groups.',
'avatar.api.count.description': 'Number displayed by the avatar group count item.',
'avatar.api.clickable.description':
Expand All @@ -33,7 +38,7 @@ export const avatarTranslations = {
'Makes a clickable group count unavailable and removes it from the tab sequence.',
'avatar.api.clicked.description': 'Emitted when an enabled clickable group count is activated.',
'avatar.accessibility.description':
"role='img' on the host. Provide ariaLabel or ariaLabelledBy to name a non-decorative avatar. For purely decorative use — such as next to a user name already present in text — add aria-hidden='true' on <sanring-avatar> to suppress redundant announcements. A clickable group count exposes button semantics and reflects disabled state.",
"role='img' on the host. Provide ariaLabel or ariaLabelledBy to name a non-decorative avatar. For purely decorative use — such as next to a user name already present in text — add aria-hidden='true' on <sanring-avatar> to suppress redundant announcements. A count badge is role='status'; give it an ariaLabel such as '3 unread'. A clickable group count exposes button semantics and reflects disabled state.",
'avatar.keyboard.description':
'Avatars are not focusable by default. A clickable group count responds to Enter and Space.',
'avatar.stateModel.description':
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { sheetTranslations } from './sheet';
import { sidebarTranslations } from './sidebar';
import { skeletonTranslations } from './skeleton';
import { sliderTranslations } from './slider';
import { sortableTranslations } from './sortable';
import { spinnerTranslations } from './spinner';
import { stepperTranslations } from './stepper';
import { switchTranslations } from './switch';
Expand Down Expand Up @@ -91,6 +92,7 @@ export const componentTranslations = {
...sidebarTranslations,
...skeletonTranslations,
...sliderTranslations,
...sortableTranslations,
...spinnerTranslations,
...stepperTranslations,
...switchTranslations,
Expand Down
40 changes: 40 additions & 0 deletions apps/docs/src/app/i18n/locales/en/components/sortable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const sortableTranslations = {
'sortable.description':
'A single-list reorder primitive. Pointer drag uses Angular CDK; keyboard arrows move the focused item. Cards and other content compose through the item directive.',
'sortable.demo.handle': 'Drag handle',
'sortable.demo.horizontal': 'Horizontal',
'sortable.demo.alpha': 'Inbox digest',
'sortable.demo.alphaDescription': 'Summarize unread threads each morning.',
'sortable.demo.beta': 'Release checklist',
'sortable.demo.betaDescription': 'Gate publish on review and smoke tests.',
'sortable.demo.gamma': 'Weekly report',
'sortable.demo.gammaDescription': 'Ship a compact status note on Fridays.',
'sortable.demo.reorder': 'Reorder',
'sortable.installation.description':
'Add the component with the CLI, then import the list, item, and optional handle directives.',
'sortable.usage.description':
'Bind the same array to data and @for. On sorted, replace the array so Angular can reconcile the new order.',
'sortable.composition.description':
'The list owns order. Items are any host element. An optional handle limits pointer dragging to that control.',
'sortable.api.description': 'Inputs, outputs, and directives for a single sortable list.',
'sortable.api.class.description': 'Additional classes merged with the list host.',
'sortable.api.data.description':
'Required array to reorder in place. sorted emits a shallow copy after each move.',
'sortable.api.orientation.description':
'Layout and lock axis for pointer dragging. vertical uses Arrow Up/Down; horizontal uses Arrow Left/Right.',
'sortable.api.disabled.description': 'Disables pointer and keyboard reordering for the whole list.',
'sortable.api.sorted.description': 'Emits a new array after a successful reorder.',
'sortable.api.itemDisabled.description': 'Disables one item without locking the rest of the list.',
'sortable.api.handle.description':
'Optional drag handle. When present, pointer dragging starts from the handle instead of the whole item.',
'sortable.accessibility.description':
'The list uses role=list and items use role=listitem. Disabled items leave the tab order. Prefer a named handle button when the item is a dense card.',
'sortable.keyboard.description': 'Focus an item, then move it with arrows. Tab walks the list.',
'sortable.keyboard.arrowsVertical':
'Moves the focused item up or down in a vertical list.',
'sortable.keyboard.arrowsHorizontal':
'Moves the focused item left or right in a horizontal list.',
'sortable.keyboard.tabShiftTab': 'Moves focus between sortable items in document order.',
'sortable.stateModel.description':
'data is the source of order. Reorder mutates that array and emits sorted with a copy. disabled on the list or an item blocks moves without clearing data.',
} as const;
10 changes: 9 additions & 1 deletion apps/docs/src/app/i18n/locales/en/components/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export const tabsTranslations = {
'tabs.demo.settings': 'Settings',
'tabs.demo.horizontal': 'Horizontal',
'tabs.demo.withIcon': 'With icon',
'tabs.demo.withCount': 'With count',
'tabs.demo.inbox': 'Inbox',
'tabs.demo.drafts': 'Drafts',
'tabs.demo.inboxCountLabel': 'Inbox, 3 unread',
'tabs.demo.inboxContent': 'Unread messages stay in the inbox.',
'tabs.demo.draftsContent': 'Drafts have no unread count, so the badge is omitted.',
'tabs.examples.withCount.description':
'Compose sanringBadge inside a trigger. Put the count in the trigger aria-label. Omit the badge when the count is 0.',
'tabs.demo.vertical': 'Vertical',
'tabs.demo.line': 'Line variant',
'tabs.demo.disabled': 'Disabled tab',
Expand All @@ -37,7 +45,7 @@ export const tabsTranslations = {
'tabs.api.disabled.description':
'Prevents a trigger from being selected or focused by keyboard navigation.',
'tabs.api.valueChange.description': 'Emits when the selected tab value changes.',
'tabs.accessibility.description': "WAI-ARIA Tabs pattern via @angular/aria/tabs. role='tablist' on sanring-tabs-list, role='tab' on each trigger, role='tabpanel' on each content panel, linked with aria-controls and aria-labelledby. aria-selected reflects the active tab.",
'tabs.accessibility.description': "WAI-ARIA Tabs pattern via @angular/aria/tabs. role='tablist' on sanring-tabs-list, role='tab' on each trigger, role='tabpanel' on each content panel, linked with aria-controls and aria-labelledby. aria-selected reflects the active tab. If a trigger shows a count, set aria-label on the trigger so the count is announced (for example 'Inbox, 3 unread').",
'tabs.keyboard.description': 'Arrow navigation within the trigger list; Tab moves into the active panel.',
'tabs.keyboard.arrowLeftRight': 'Navigate between tab triggers (horizontal orientation).',
'tabs.keyboard.arrowUpDown': 'Navigate between tab triggers (vertical orientation).',
Expand Down
9 changes: 6 additions & 3 deletions apps/docs/src/app/i18n/locales/en/components/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ export const timelineTranslations = {
'timeline.description':
'Composable timeline primitives for chronological events, activity feeds, and process milestones.',
'timeline.examples.basic.description':
'Use sanringTimeline with native list markup, then compose separators, markers, connectors, and content.',
'A vertical list with a built-in rail. Leave the separator empty for a default marker, or project an icon or avatar.',
'timeline.usage.description':
'Import the timeline directives and apply them to list or div-based activity markup.',
'Each item is a separator plus content. The separator draws the connector; project a node only when the default marker is not enough.',
'timeline.installation.description':
'Install the timeline primitives and compose item, separator, and content directives where each event renders.',
'timeline.demo.horizontal': 'Horizontal',
'timeline.demo.divBased': 'Div-based timeline',
'timeline.demo.reorder': 'Reorder',
'timeline.demo.releaseActivity': 'Release activity',
'timeline.demo.releaseActivityDescription':
'A compact activity trail for release notes and registry updates.',
Expand Down Expand Up @@ -37,6 +38,8 @@ export const timelineTranslations = {
'timeline.demo.qaTitle': 'Quality review',
'timeline.demo.qaMeta': 'QA pass',
'timeline.demo.qaDescription': 'Visual spacing and empty states were reviewed before publishing.',
'timeline.demo.reorder.description':
'Compose with sortable and drag the grip to reorder. Timeline stays layout; sortable owns order. Also install sortable.',
'timeline.api.description': 'Inputs supported by the Timeline directives.',
'timeline.api.orientation.description':
'Controls whether items stack vertically or horizontally.',
Expand All @@ -46,7 +49,7 @@ export const timelineTranslations = {
'Additional classes merged with each separator wrapper.',
'timeline.api.contentClass.description': 'Additional classes merged with each content container.',
'timeline.accessibility.description':
'Timeline is a visual layout primitive with no built-in ARIA role. When timeline items represent an ordered sequence, wrap the content in a native <ol> or <ul> in your template so screen readers understand the list structure.',
'The root sets role=list and each item sets role=listitem so Tailwind Preflight does not strip list semantics. The separator is aria-hidden. Prefer native ul or ol when the sequence is ordered.',
'timeline.keyboard.description': 'Not focusable unless interactive children are present.',
'timeline.stateModel.description':
'Stateless layout component — no value, selection, or event state.',
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/app/i18n/locales/en/pages/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const mcpTranslations = {
'Starts an MCP server over stdio so AI coding agents — Claude Code, Cursor, Windsurf — can query and install Sanring UI components directly, without shelling out.',
'mcp.overview.title': 'Overview',
'mcp.overview.body':
'The sanring mcp command starts an MCP server over stdio. It reads from the same component registry as the sanring CLI and this documentation site, and exposes five tools an AI agent can call to inspect, plan, and install components in your Angular project.',
'The sanring mcp command starts an MCP server over stdio. It reads from the same component registry as the sanring CLI and this documentation site, and exposes tools an AI agent can call to look up authoring contracts, then plan and install components in your Angular project.',
'mcp.tools.title': 'Tools',
'mcp.tools.body':
'All tool handlers validate their input at runtime and return an MCP error result — rather than throwing — when a component name is not found or a required argument is missing.',
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/app/i18n/locales/zh/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const commonTranslations = {
'component.sidebar': 'Sidebar',
'component.skeleton': '骨架屏',
'component.slider': '滑桿',
'component.sortable': '可排序列表',
'component.stepper': '步驟器',
'component.switch': '開關',
'component.table': '表格',
Expand Down
10 changes: 8 additions & 2 deletions apps/docs/src/app/i18n/locales/zh/components/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export const avatarTranslations = {
'avatar.demo.sizes': '尺寸',
'avatar.demo.statusBadge': '狀態徽章',
'avatar.demo.badgeWithIcon': '包含圖示的徽章',
'avatar.demo.badgeCount': '通知計數',
'avatar.examples.badgeCount.description':
'傳入 count 會渲染數字 pill。預設在右上角,可與右下角狀態點同時存在。0 會隱藏 pill;超過 99 顯示為 99+。',
'avatar.demo.group': '頭像群組',
'avatar.demo.groupWithIcon': '包含圖示的頭像群組',
'avatar.examples.description': '常見頭像模式,包含 fallback、在線狀態與精簡成員群組。',
Expand All @@ -20,14 +23,17 @@ export const avatarTranslations = {
'avatar.api.ariaLabel.description': '無可見文字時,提供單一頭像或頭像群組的無障礙標籤。',
'avatar.api.delayMs.description': 'fallback 顯示前的延遲毫秒數。',
'avatar.api.status.description': '控制徽章顏色,可使用 online、offline、away、busy 或 default。',
'avatar.api.placement.description': '將徽章放在視覺起點或終點,並尊重 RTL 方向。',
'avatar.api.placement.description':
'將徽章放在 start、end、top 或 bottom。狀態徽章預設 end(右下);計數徽章預設 top(右上)。start/end 會跟隨 RTL。',
'avatar.api.badgeCount.description':
'[sanringAvatarBadge] 的未讀計數。小於等於 0 時隱藏;超過 99 顯示為 99+。',
'avatar.api.overlap.description': '頭像群組的堆疊重疊量,單位為 rem。',
'avatar.api.count.description': '頭像群組數量項目顯示的數字。',
'avatar.api.clickable.description': '讓群組數量項目具備按鈕語意,並可由滑鼠與鍵盤操作。',
'avatar.api.disabled.description': '停用可點擊的群組數量項目,並將它移出 Tab 序列。',
'avatar.api.clicked.description': '啟用中的可點擊群組數量項目被觸發時送出。',
'avatar.accessibility.description':
"宿主具有 role='img'。為非裝飾性的頭像提供 ariaLabel 或 ariaLabelledBy。若頭像純屬裝飾性用途(例如緊鄰已出現在文字中的使用者名稱),請在 <sanring-avatar> 上加 aria-hidden='true' 以避免重複播報。可點擊的群組數量項目會提供 button 語意並反映停用狀態。",
"宿主具有 role='img'。為非裝飾性的頭像提供 ariaLabel 或 ariaLabelledBy。若頭像純屬裝飾性用途(例如緊鄰已出現在文字中的使用者名稱),請在 <sanring-avatar> 上加 aria-hidden='true' 以避免重複播報。計數徽章為 role='status',請給它 ariaLabel,例如「3 則未讀」。可點擊的群組數量項目會提供 button 語意並反映停用狀態。",
'avatar.keyboard.description': '頭像預設不可聚焦;可點擊的群組數量項目支援 Enter 與 Space。',
'avatar.stateModel.description':
'無狀態。src 載入圖片;失敗時顯示 fallback 插槽;縮寫字母作為最後備援。沒有內部選取或值狀態。',
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/app/i18n/locales/zh/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { sheetTranslations } from './sheet';
import { sidebarTranslations } from './sidebar';
import { skeletonTranslations } from './skeleton';
import { sliderTranslations } from './slider';
import { sortableTranslations } from './sortable';
import { spinnerTranslations } from './spinner';
import { stepperTranslations } from './stepper';
import { switchTranslations } from './switch';
Expand Down Expand Up @@ -91,6 +92,7 @@ export const componentTranslations = {
...sidebarTranslations,
...skeletonTranslations,
...sliderTranslations,
...sortableTranslations,
...spinnerTranslations,
...stepperTranslations,
...switchTranslations,
Expand Down
Loading
Loading