@teamblind-chorus/ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/agents/AGENTS.md +143 -0
  4. package/agents/DESIGN.md +1311 -0
  5. package/agents/LOVABLE.md +472 -0
  6. package/agents/anti-patterns.md +533 -0
  7. package/agents/catalog.md +232 -0
  8. package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
  9. package/agents/components/avatar-rail/avatar-rail.md +103 -0
  10. package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
  11. package/agents/components/badge/badge.family.json +45 -0
  12. package/agents/components/badge/badge.md +10 -0
  13. package/agents/components/badge/role.md +100 -0
  14. package/agents/components/badge/role.spec.json +75 -0
  15. package/agents/components/badge/update.md +132 -0
  16. package/agents/components/badge/update.spec.json +114 -0
  17. package/agents/components/banner/banner.family.json +28 -0
  18. package/agents/components/banner/banner.md +136 -0
  19. package/agents/components/banner/banner.spec.json +136 -0
  20. package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
  21. package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
  22. package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
  23. package/agents/components/bubble/bubble.family.json +29 -0
  24. package/agents/components/bubble/bubble.md +134 -0
  25. package/agents/components/bubble/bubble.spec.json +91 -0
  26. package/agents/components/button/button.family.json +76 -0
  27. package/agents/components/button/button.md +31 -0
  28. package/agents/components/button/check.md +138 -0
  29. package/agents/components/button/check.spec.json +161 -0
  30. package/agents/components/button/fab.md +161 -0
  31. package/agents/components/button/fab.spec.json +106 -0
  32. package/agents/components/button/icon.md +141 -0
  33. package/agents/components/button/icon.spec.json +164 -0
  34. package/agents/components/button/standard.md +219 -0
  35. package/agents/components/button/standard.spec.json +205 -0
  36. package/agents/components/button/text.md +186 -0
  37. package/agents/components/button/text.spec.json +215 -0
  38. package/agents/components/button/toggle.md +108 -0
  39. package/agents/components/button/toggle.spec.json +124 -0
  40. package/agents/components/button/toolbar.md +189 -0
  41. package/agents/components/button/toolbar.spec.json +109 -0
  42. package/agents/components/carousel/carousel.family.json +41 -0
  43. package/agents/components/carousel/carousel.md +40 -0
  44. package/agents/components/carousel/post.md +148 -0
  45. package/agents/components/carousel/post.spec.json +229 -0
  46. package/agents/components/carousel/profile.md +184 -0
  47. package/agents/components/carousel/profile.spec.json +219 -0
  48. package/agents/components/chip/chip.family.json +37 -0
  49. package/agents/components/chip/chip.md +10 -0
  50. package/agents/components/chip/filter.md +212 -0
  51. package/agents/components/chip/filter.spec.json +124 -0
  52. package/agents/components/chip/tag.md +137 -0
  53. package/agents/components/chip/tag.spec.json +104 -0
  54. package/agents/components/dialog/dialog.family.json +29 -0
  55. package/agents/components/dialog/dialog.md +113 -0
  56. package/agents/components/dialog/dialog.spec.json +156 -0
  57. package/agents/components/directory-list/directory-list.family.json +46 -0
  58. package/agents/components/directory-list/directory-list.md +87 -0
  59. package/agents/components/directory-list/directory-list.spec.json +104 -0
  60. package/agents/components/divider/divider.family.json +28 -0
  61. package/agents/components/divider/divider.md +78 -0
  62. package/agents/components/divider/divider.spec.json +51 -0
  63. package/agents/components/feed/ad.md +108 -0
  64. package/agents/components/feed/ad.spec.json +187 -0
  65. package/agents/components/feed/feed.family.json +48 -0
  66. package/agents/components/feed/feed.md +30 -0
  67. package/agents/components/feed/post.md +240 -0
  68. package/agents/components/feed/post.spec.json +361 -0
  69. package/agents/components/form-field/form-field.family.json +50 -0
  70. package/agents/components/form-field/form-field.md +11 -0
  71. package/agents/components/form-field/input.md +198 -0
  72. package/agents/components/form-field/input.spec.json +202 -0
  73. package/agents/components/form-field/search.md +81 -0
  74. package/agents/components/form-field/search.spec.json +135 -0
  75. package/agents/components/form-field/select.md +101 -0
  76. package/agents/components/form-field/select.spec.json +194 -0
  77. package/agents/components/form-field/textarea.md +89 -0
  78. package/agents/components/form-field/textarea.spec.json +176 -0
  79. package/agents/components/header/header.family.json +43 -0
  80. package/agents/components/header/header.md +18 -0
  81. package/agents/components/header/main.md +101 -0
  82. package/agents/components/header/main.spec.json +117 -0
  83. package/agents/components/header/sub.md +129 -0
  84. package/agents/components/header/sub.spec.json +81 -0
  85. package/agents/components/list/accordion.md +183 -0
  86. package/agents/components/list/accordion.spec.json +201 -0
  87. package/agents/components/list/entry.md +280 -0
  88. package/agents/components/list/entry.spec.json +237 -0
  89. package/agents/components/list/list.family.json +75 -0
  90. package/agents/components/list/list.md +24 -0
  91. package/agents/components/list/radio.md +144 -0
  92. package/agents/components/list/radio.spec.json +186 -0
  93. package/agents/components/list/standard.md +262 -0
  94. package/agents/components/list/standard.spec.json +221 -0
  95. package/agents/components/metadata/compact.md +69 -0
  96. package/agents/components/metadata/compact.spec.json +69 -0
  97. package/agents/components/metadata/metadata.family.json +42 -0
  98. package/agents/components/metadata/metadata.md +26 -0
  99. package/agents/components/metadata/standard.md +104 -0
  100. package/agents/components/metadata/standard.spec.json +152 -0
  101. package/agents/components/nav-card/nav-card.family.json +29 -0
  102. package/agents/components/nav-card/nav-card.md +179 -0
  103. package/agents/components/nav-card/nav-card.spec.json +161 -0
  104. package/agents/components/nav-list/nav-list.family.json +46 -0
  105. package/agents/components/nav-list/nav-list.md +91 -0
  106. package/agents/components/nav-list/nav-list.spec.json +107 -0
  107. package/agents/components/navigation-bar/main.md +201 -0
  108. package/agents/components/navigation-bar/main.spec.json +109 -0
  109. package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
  110. package/agents/components/navigation-bar/navigation-bar.md +21 -0
  111. package/agents/components/navigation-bar/search.md +96 -0
  112. package/agents/components/navigation-bar/search.spec.json +142 -0
  113. package/agents/components/navigation-bar/sub.md +174 -0
  114. package/agents/components/navigation-bar/sub.spec.json +123 -0
  115. package/agents/components/page-shell/page-shell.family.json +22 -0
  116. package/agents/components/page-shell/page-shell.md +51 -0
  117. package/agents/components/profile-header/profile-header.family.json +29 -0
  118. package/agents/components/profile-header/profile-header.md +149 -0
  119. package/agents/components/profile-header/profile-header.spec.json +200 -0
  120. package/agents/components/progress/progress.family.json +27 -0
  121. package/agents/components/progress/progress.md +38 -0
  122. package/agents/components/progress/progress.spec.json +67 -0
  123. package/agents/components/side-sheet/side-sheet.family.json +30 -0
  124. package/agents/components/side-sheet/side-sheet.md +154 -0
  125. package/agents/components/side-sheet/side-sheet.spec.json +109 -0
  126. package/agents/components/skeleton/skeleton.family.json +28 -0
  127. package/agents/components/skeleton/skeleton.md +123 -0
  128. package/agents/components/skeleton/skeleton.spec.json +73 -0
  129. package/agents/components/status-tag/status-tag.family.json +26 -0
  130. package/agents/components/status-tag/status-tag.md +114 -0
  131. package/agents/components/status-tag/status-tag.spec.json +69 -0
  132. package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
  133. package/agents/components/suggestion-list/suggestion-list.md +91 -0
  134. package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
  135. package/agents/components/switch/switch.family.json +27 -0
  136. package/agents/components/switch/switch.md +114 -0
  137. package/agents/components/switch/switch.spec.json +123 -0
  138. package/agents/components/tab-bar/tab-bar.family.json +27 -0
  139. package/agents/components/tab-bar/tab-bar.md +178 -0
  140. package/agents/components/tab-bar/tab-bar.spec.json +184 -0
  141. package/agents/components/tabs/rounded.md +150 -0
  142. package/agents/components/tabs/rounded.spec.json +140 -0
  143. package/agents/components/tabs/segmented.md +114 -0
  144. package/agents/components/tabs/segmented.spec.json +100 -0
  145. package/agents/components/tabs/tabs.family.json +59 -0
  146. package/agents/components/tabs/tabs.md +18 -0
  147. package/agents/components/tabs/underline.md +147 -0
  148. package/agents/components/tabs/underline.spec.json +139 -0
  149. package/agents/components/thumbnail/thumbnail.family.json +28 -0
  150. package/agents/components/thumbnail/thumbnail.md +152 -0
  151. package/agents/components/thumbnail/thumbnail.spec.json +172 -0
  152. package/agents/components/toast/toast.family.json +28 -0
  153. package/agents/components/toast/toast.md +133 -0
  154. package/agents/components/toast/toast.spec.json +89 -0
  155. package/agents/components/tooltip/tooltip.family.json +29 -0
  156. package/agents/components/tooltip/tooltip.md +139 -0
  157. package/agents/components/tooltip/tooltip.spec.json +110 -0
  158. package/agents/compose.md +240 -0
  159. package/agents/icons.json +831 -0
  160. package/agents/images.md +66 -0
  161. package/agents/manifest.json +87 -0
  162. package/agents/patterns/README.md +59 -0
  163. package/agents/patterns/actions.md +50 -0
  164. package/agents/patterns/browsing.md +52 -0
  165. package/agents/patterns/communications.md +56 -0
  166. package/agents/patterns/layout.md +72 -0
  167. package/agents/patterns/modals.md +50 -0
  168. package/agents/patterns/visual.md +55 -0
  169. package/agents/reconstruct.md +55 -0
  170. package/agents/scoped-adoption.md +111 -0
  171. package/agents/tokens.usage.json +1657 -0
  172. package/agents/usage.json +422 -0
  173. package/dist/icons/index.cjs +1332 -0
  174. package/dist/icons/index.cjs.map +1 -0
  175. package/dist/icons/index.d.cts +228 -0
  176. package/dist/icons/index.d.ts +228 -0
  177. package/dist/icons/index.js +1114 -0
  178. package/dist/icons/index.js.map +1 -0
  179. package/dist/index.cjs +5905 -0
  180. package/dist/index.cjs.map +1 -0
  181. package/dist/index.d.cts +896 -0
  182. package/dist/index.d.ts +896 -0
  183. package/dist/index.js +5847 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/styles.css +5765 -0
  186. package/eslint/README.md +79 -0
  187. package/eslint/index.js +78 -0
  188. package/eslint/rules.js +472 -0
  189. package/eslint/test.mjs +135 -0
  190. package/package.json +96 -0
  191. package/placeholder.png +0 -0
@@ -0,0 +1,152 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Metadata",
4
+ "family": "metadata",
5
+ "subcomponent": "standard",
6
+ "description": "Author / brand attribution cluster shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). Composes a leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and a secondary line (either a plain `subtitle` text — canonical 'Sponsored' for ads — or an array of independently-linked `meta` items: location, job function, and — canonically last — the user's nickname, displayed bare with no @ prefix). An optional trailing slot hosts a row-level affordance (dismiss × for ads). The dot separator is constrained so its line-box never exceeds its font-size — middle-dot glyphs that would otherwise inflate the row's line-height stay flush with the surrounding text. `layoutInset: inline` — atom-shaped, pays no padding of its own; the host (Feed Post / Feed Ad card head) owns the row gutter / divider / click target.",
7
+ "element": "div",
8
+ "props": {
9
+ "avatar": {
10
+ "type": "object",
11
+ "optional": true,
12
+ "description": "Forwarded to [Thumbnail](../thumbnail/thumbnail.md) verbatim at `size={32}`. When omitted, the Thumbnail renders its image-area fallback over `surfaceContainerHigh`."
13
+ },
14
+ "name": {
15
+ "type": "string",
16
+ "required": true,
17
+ "description": "Primary line entity name — channel topic (Post) or brand (Ad). Single line; truncates with ellipsis."
18
+ },
19
+ "nameHref": {
20
+ "type": "string",
21
+ "optional": true,
22
+ "description": "Destination URL for the entity name. When present, the name renders as an `<a>`; otherwise as a `<span>`."
23
+ },
24
+ "timestamp": {
25
+ "type": "string",
26
+ "optional": true,
27
+ "description": "Inline timestamp painted after the name on the primary line, in `label.sm` / `sys.color.outline` — one tonal step further than the name. Reach for it on Feed Post; omit on Feed Ad."
28
+ },
29
+ "followAction": {
30
+ "type": "boolean",
31
+ "default": false,
32
+ "description": "Whether to paint the inline follow toggle after the timestamp. When `true`, a dot separator + Follow/Following text toggle render at the primary line's trailing edge. State is controlled via `followed` + `onFollowChange`."
33
+ },
34
+ "followed": {
35
+ "type": "boolean",
36
+ "default": false,
37
+ "description": "Whether the viewer follows this entity. Drives the inline follow toggle's label and `aria-pressed`."
38
+ },
39
+ "onFollowChange": {
40
+ "type": "function",
41
+ "optional": true,
42
+ "description": "Fires with the next `followed` value when the inline follow toggle is tapped."
43
+ },
44
+ "subtitle": {
45
+ "type": "string",
46
+ "optional": true,
47
+ "description": "Secondary line plain text — canonical use is 'Sponsored' on Feed Ad. Mutually exclusive with `meta` (when both are passed, `meta` wins)."
48
+ },
49
+ "meta": {
50
+ "type": "node",
51
+ "optional": true,
52
+ "description": "Array of independently-linked metadata items rendered on the secondary line. Each entry is either a string (renders as a stub-href link) or a `{ label, href, badge }` object — `badge` is an optional SINGLE presentational mark node rendered AFTER the item's link, outside the <a> (canonical fill: Badge variant=\"role\" on the trailing nickname item; at most one badge rides the nickname). The last item is canonically the user's nickname, displayed bare (no @ prefix). Items separate by middot. Reach for it on Feed Post; omit on Feed Ad."
53
+ },
54
+ "trailing": {
55
+ "type": "node",
56
+ "optional": true,
57
+ "description": "Trailing-edge slot. Canonical fill: the Feed Ad dismiss × button. The slot's hit target is its own — taps on it never propagate to the entity-name link."
58
+ }
59
+ },
60
+ "slots": {
61
+ "container": {
62
+ "required": true,
63
+ "description": "Outer flex row — `align-items: center`, `sys.layout.inline.md` (8px) gap between avatar, text block, and trailing slot.",
64
+ "intrinsic": true
65
+ },
66
+ "avatar": {
67
+ "required": false,
68
+ "description": "Leading [Thumbnail](../thumbnail/thumbnail.md) at `size={32}`. Every Thumbnail prop forwarded verbatim.",
69
+ "accepts": [
70
+ "thumbnail"
71
+ ],
72
+ "rendersAs": "thumbnail:32"
73
+ },
74
+ "text": {
75
+ "required": true,
76
+ "description": "Two-line text column. Primary line (name + optional timestamp + optional follow) on top; optional secondary line (subtitle text OR meta-link row) below. `flex: 1 1 auto`, `min-width: 0` so both lines truncate.",
77
+ "intrinsic": true
78
+ },
79
+ "name": {
80
+ "required": true,
81
+ "description": "Entity name. `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurface`. Renders as `<a>` when `nameHref` is set, `<span>` otherwise. Single line; truncates with ellipsis.",
82
+ "accepts": [
83
+ "text"
84
+ ]
85
+ },
86
+ "timestamp": {
87
+ "required": false,
88
+ "description": "Inline timestamp after the name. `sys.typo.label.sm` (12 / Semibold) / `sys.color.outline` — one tonal step further than the name so the timestamp recedes.",
89
+ "accepts": [
90
+ "text"
91
+ ]
92
+ },
93
+ "followAction": {
94
+ "required": false,
95
+ "description": "Inline follow toggle at the primary line's trailing edge. Bare text affordance (no chrome) — `sys.color.primary` at rest, `sys.color.onSurfaceVariant` when active. A `·` separator precedes it.",
96
+ "intrinsic": true
97
+ },
98
+ "subtitle": {
99
+ "required": false,
100
+ "description": "Secondary line plain text. `sys.typo.label.sm` / `sys.color.onSurfaceVariant`. Mutually exclusive with `meta`.",
101
+ "accepts": [
102
+ "text"
103
+ ]
104
+ },
105
+ "meta": {
106
+ "required": false,
107
+ "description": "Secondary line meta-link row. Each item is its own `<a>` link; sibling items separate by middot. Hover paints the underline on the link alone — the middot stays unstyled.",
108
+ "intrinsic": true
109
+ },
110
+ "trailing": {
111
+ "required": false,
112
+ "description": "Trailing-edge slot — typically a dismiss × button on Feed Ad. Its own hit target; clicks don't propagate to the surrounding row.",
113
+ "accepts": [
114
+ "button",
115
+ "icon"
116
+ ]
117
+ }
118
+ },
119
+ "sizing": {
120
+ "containerGap": "sys.layout.inline.md",
121
+ "containerAlign": "center",
122
+ "avatarSize": 32,
123
+ "nameTypo": "sys.typo.label.sm",
124
+ "nameColor": "sys.color.onSurface",
125
+ "timestampTypo": "sys.typo.label.sm",
126
+ "timestampColor": "sys.color.outline",
127
+ "subtitleTypo": "sys.typo.label.sm",
128
+ "subtitleColor": "sys.color.onSurfaceVariant",
129
+ "metaTypo": "sys.typo.label.sm",
130
+ "metaColor": "sys.color.onSurfaceVariant",
131
+ "dotColor": "sys.color.outline",
132
+ "dotLineHeight": "1",
133
+ "dotLineHeightNote": "The middot separator (`·`) inherits the surrounding text's font-size but uses `line-height: 1` so its line-box never exceeds the glyph's font-size — keeps the row's text-line tight even when the inherited line-height would otherwise allow extra vertical space around the middot.",
134
+ "primaryRowGap": "sys.layout.inline.md",
135
+ "metaRowGap": "0",
136
+ "metaSeparatorInset": "sys.layout.inline.sm"
137
+ },
138
+ "states": {
139
+ "note": "Metadata itself has no lifecycle states — each interactive child (name link, follow toggle, meta links, trailing button) carries its own state contract."
140
+ },
141
+ "behavior": {
142
+ "dotHeight": "Every middot separator inside Metadata (between name/timestamp/follow on the primary line, and between meta items on the secondary line) uses `line-height: 1` so its line-box matches its font-size. This prevents the U+00B7 glyph's natural vertical extent from inflating the parent's line-box.",
143
+ "linkAffordances": "Name and meta items render as `<a>` elements when an `href` is supplied — each is an independent affordance. Taps on the trailing slot stop propagating.",
144
+ "compositionInsideFeed": "Both [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md) compose Metadata at the head of the card. Post passes `timestamp` + `meta`; Ad passes `subtitle` + `trailing` (dismiss button)."
145
+ },
146
+ "forbidden": [
147
+ "Metadata rendered outside a Feed Post / Feed Ad host — the cluster is the author / brand attribution primitive that pairs with Feed; for generic 'who / what this is + commit' rows, use [list/entry](../list/entry.md)",
148
+ "subtitle paired with meta — the secondary line is mutually exclusive; pick one (meta for Post, subtitle for Ad)",
149
+ "middot separators painted as block elements — the dot is always inline so it inherits the surrounding text's baseline",
150
+ "name rendered as a button — the name is a link to the entity page, never a commit"
151
+ ]
152
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "nav-card",
4
+ "name": "NavCard",
5
+ "description": "Bounded single-row card \u2014 an outlined rounded surface with a label, an optional supporting line, and an optional trailing affordance. Two variants select the trailing shape: `default` ships no trailing icon (bare labelled tile), `nav` auto-renders the right-pointing chevron (the drill-in form). The whole card is the tap target. Reach for it when a [List](../list/list.md) drill-in row (`text` with `nav: true`) needs to read as its own discrete affordance rather than as one entry in a stack \u2014 a labelled scope tile (default), a standalone settings drill-in (nav), a picker trigger, or an entity entry card. Single-spec family.",
6
+ "useCases": [
7
+ "standalone labelled tile (default)",
8
+ "single settings drill-in (nav)",
9
+ "picker trigger",
10
+ "channel / scope selector card",
11
+ "bounded-surface drill-in"
12
+ ],
13
+ "visualReuse": "open",
14
+ "layoutInset": "inline",
15
+ "wrapperGuidance": "Inline card. NavCard has its own internal padding + border-radius + outline + 48 min-height, and `width: 100%` so it fills the host column. It ships no outer margin and does NOT claim the page rail. The host owns the surrounding inset: when placed at the page-shell level, the shell's `layout.page.md` gutter provides the 16px horizontal safe zone; when wrapped inside another host (Section body, BottomSheet content slot, SideSheet column, NavCardGroup stack), that host's container padding governs the inset. Vertical spacing between NavCards is paid by `NavCardGroup`'s `gap: var(--sys-layout-stack-xs)` (8px) \u2014 never paint per-child `margin-block` on NavCard.",
16
+ "usage": {
17
+ "note": "The trailing drill-in chevron comes from `variant=\"nav\"`, not a trailing child; `leading` takes a node (Icon / Thumbnail).",
18
+ "example": "<NavCard variant=\"nav\" label=\"…\" supportingText=\"…\" leading={<Icon />} href=\"#\" />"
19
+ },
20
+ "spec": "nav-card.md",
21
+ "subcomponents": [
22
+ {
23
+ "slug": "nav-card",
24
+ "spec": "nav-card.spec.json",
25
+ "md": "nav-card.md",
26
+ "default": true
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,179 @@
1
+ # Nav card
2
+
3
+ A bounded single-row card — outlined rounded surface with a label, optional supporting line, and optional trailing affordance. Two variants pick the trailing shape: `default` ships no trailing icon (bare labelled tile), `nav` auto-renders the right-pointing chevron (explicit drill-in). The whole card is the tap target.
4
+
5
+ **Reach for this when** one row needs to read as its own discrete affordance — a labelled scope tile (`default`), a standalone settings drill-in (`nav`), a picker trigger, or a channel / sub-brand entry card. **Skip when** rows stack into a vertical column (use a [List](../list/list.md) drill-in row — `nav: true`), the action is a commit (use [Button](../button/button.md)), or the surface is purely informational (use [Banner](../banner/banner.md)).
6
+
7
+ **Layout inset.** `inline` — carries its own internal `16px inline / 8px block` padding (`layout.container.md` / `layout.container.xs`), `radius.md` corners, hairline outline, 48px min-height, and `width: 100%` so it fills the host column. Ships no outer margin and does not claim the page rail. The host owns the surrounding inset — page shell gutter at the route level, container padding inside a `<Section>` / `<BottomSheet>` / `<SideSheet>` / `<NavCardGroup>`. Vertical spacing between cards comes from `NavCardGroup`'s `gap: var(--sys-layout-stack-xs)` (8) — never paint per-child `margin-block` on NavCard.
8
+
9
+ ## Default
10
+
11
+ The bare form — label only, no trailing icon. Reach for it when the card is a labelled tile (scope label, status card, informational entry) and the drill-in chevron would over-promise navigation.
12
+
13
+ ```preview
14
+ nav-card/default
15
+ ---
16
+ import { NavCard } from '@teamblind-chorus/ui';
17
+
18
+ <NavCard label="Cell label here" href="#" />
19
+ ```
20
+
21
+ ## Use cases
22
+
23
+ ### Nav (with trailing chevron)
24
+
25
+ `variant="nav"` auto-renders the right-pointing chevron — the explicit drill-in form. Reach for it when the card routes into another surface (settings detail, picker, sub-flow).
26
+
27
+ ```preview
28
+ nav-card/nav
29
+ ---
30
+ import { NavCard } from '@teamblind-chorus/ui';
31
+
32
+ <NavCard variant="nav" label="Cell label here" href="#" />
33
+ ```
34
+
35
+ ### With supporting text
36
+
37
+ Two-line variant — primary label on top, supporting metadata below at `onSurfaceVariant`. Works with either variant; pair with `nav` when the drill-in is metadata-bearing.
38
+
39
+ ```preview
40
+ nav-card/supporting
41
+ ---
42
+ import { NavCard } from '@teamblind-chorus/ui';
43
+
44
+ <NavCard
45
+ variant="nav"
46
+ label="Saved posts"
47
+ supportingText="47 posts across 9 channels"
48
+ href="#"
49
+ />
50
+ ```
51
+
52
+ ### With leading icon
53
+
54
+ A leading 16 × 16 glyph at the inline padding edge. The icon vertically centres on the row's parent block — same `align-items: center` axis as the label column and trailing slot, so the glyph sits on the same midline as the label (one-line) or label + supportingText block (two-line).
55
+
56
+ ```preview
57
+ nav-card/leading-icon
58
+ ---
59
+ import { NavCard } from '@teamblind-chorus/ui';
60
+ import { BellIcon } from '@teamblind-chorus/ui/icons';
61
+
62
+ <NavCard
63
+ label="Notifications"
64
+ leading={<BellIcon size={16} />}
65
+ supportingText="3 unread mentions"
66
+ href="#"
67
+ />
68
+ ```
69
+
70
+ ### With leading thumbnail
71
+
72
+ A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md) — used when the drill target is an entity (channel, person, brand) rather than a chrome action. The thumbnail block-centres on the row's vertical midline, same as an icon leading.
73
+
74
+ ```preview
75
+ nav-card/leading-thumbnail
76
+ ---
77
+ import { NavCard, Thumbnail } from '@teamblind-chorus/ui';
78
+
79
+ <NavCard
80
+ variant="nav"
81
+ label="Hyundai Motor"
82
+ supportingText="Private · My company"
83
+ leading={<Thumbnail size={32} alt="Hyundai" />}
84
+ href="#"
85
+ />
86
+ ```
87
+
88
+ ### Group
89
+
90
+ Multiple NavCards stacked vertically as a `NavCardGroup` — each card stays its own outlined affordance, separated by `sys.layout.stack.xs` (8px) gap. Use when several drill-in cards share a section but should read as discrete cards (vs a List drill-in rail — `text` rows with `nav: true` — where rows tile flush with hairline dividers).
91
+
92
+ ```preview
93
+ nav-card/group
94
+ ---
95
+ import { NavCard, NavCardGroup } from '@teamblind-chorus/ui';
96
+ import { BellIcon, BookmarkIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
97
+
98
+ <NavCardGroup aria-label="Account">
99
+ <NavCard variant="nav" label="Profile" supportingText="Display name, avatar, bio" leading={<ProfileIcon size={16} />} href="#" />
100
+ <NavCard variant="nav" label="Saved posts" supportingText="47 posts across 9 channels" leading={<BookmarkIcon size={16} />} href="#" />
101
+ <NavCard variant="nav" label="Notifications" leading={<BellIcon size={16} />} href="#" />
102
+ </NavCardGroup>
103
+ ```
104
+
105
+ ### Surface (opaque tier on a non-`surface` host)
106
+
107
+ `appearance="surface"` paints the card with its own `sys.color.surface` fill so it reads as an opaque tier. Reach for it when the card sits on a transparent / non-`surface` host (coloured hero, tonal band, BottomSheet content slot) and the default transparent fill would let the card blend in.
108
+
109
+ ```preview
110
+ nav-card/surface
111
+ ---
112
+ import { NavCard, NavCardGroup } from '@teamblind-chorus/ui';
113
+ import { BellIcon, BookmarkIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
114
+
115
+ <div
116
+ style={{
117
+ background: 'var(--sys-color-surfaceContainerLow)',
118
+ padding: 'var(--sys-layout-container-md)',
119
+ borderRadius: 'var(--sys-radius-lg)',
120
+ }}
121
+ >
122
+ <NavCardGroup aria-label="Account">
123
+ <NavCard variant="nav" appearance="surface" label="Profile" supportingText="Display name, avatar, bio" leading={<ProfileIcon size={16} />} href="#" />
124
+ <NavCard variant="nav" appearance="surface" label="Saved posts" supportingText="47 posts across 9 channels" leading={<BookmarkIcon size={16} />} href="#" />
125
+ <NavCard variant="nav" appearance="surface" label="Notifications" leading={<BellIcon size={16} />} href="#" />
126
+ </NavCardGroup>
127
+ </div>
128
+ ```
129
+
130
+ ## Slots
131
+
132
+ - **container** — outlined rounded box. `surface` fill, `radius.md` corners, hairline `outlineVariant` stroke painted as inset box-shadow (never `border:`).
133
+ - **leading** *(optional)* — 16px icon (`currentColor`) or 32-rung [Thumbnail](../thumbnail/thumbnail.md). Block-centred on the row's vertical midline — same contract for icon and thumbnail. Omitted by default; label flushes to the inline padding edge.
134
+ - **labelCol** — vertical column holding label and (optional) supportingText. `min-width: 0` so both lines truncate.
135
+ - **label** — primary card text. 14px / Regular / `onSurface`. Single line; truncates.
136
+ - **supportingText** *(optional)* — secondary line under label. 12px / Regular / `onSurfaceVariant`. Sits flush under the label with no extra top margin — line-height alone separates the two rows.
137
+ - **trailingIcon** *(optional)* — on `variant="nav"`, an auto-rendered right-pointing chevron at 16px, `onSurfaceVariant`. On `variant="default"` no trailing renders. A `trailingIcon` prop overrides either case. Slot stays block-centred regardless of one-line / two-line body.
138
+
139
+ ## Anatomy
140
+
141
+ | Slot | Token bindings |
142
+ |----------------|----------------|
143
+ | container | `surface` fill, `radius.md` corners, hairline `outlineVariant` inset box-shadow, `48px` min-height, `8px` block / `16px` inline padding |
144
+ | leading | 16 × 16 (`sys.icon.md`) glyph in `currentColor` or 32 × 32 [Thumbnail](../thumbnail/thumbnail.md). Block-centred on the row's vertical midline (same contract for icon and thumbnail). `sys.layout.inline.md` (8px) gap to label column |
145
+ | labelCol | Flex column, `min-width: 0`, no inter-line margin (line-height carries the rhythm) |
146
+ | label | `sys.typo.body.sm` (14 / Regular) / `onSurface` |
147
+ | supportingText | `sys.typo.label.sm` (12 / Semibold) / `onSurfaceVariant`, no top margin |
148
+ | trailingIcon | 16 × 16, `onSurfaceVariant`, `sys.layout.inline.md` (8px) gap to label column. Auto-rendered chevron on `variant="nav"`; omitted on `variant="default"` unless `trailingIcon` is supplied. Always block-centred. |
149
+ | group | `NavCardGroup` flex column, `sys.layout.stack.xs` (8px) gap between cards |
150
+
151
+ ## Appearance
152
+
153
+ | Appearance | Container fill | When to reach |
154
+ |------------|---------------------------------|----------------|
155
+ | `default` | `transparent` | The canonical form. NavCard's identity is the outlined chrome (hairline + radius + label + chevron); host surface tone reads through. State overlays mix on the transparent base. |
156
+ | `surface` | `sys.color.surface` (opaque) | NavCard on a transparent / non-`surface` host (coloured hero, tonal band, BottomSheet content). Outline, label, chevron, and state overlays unchanged. |
157
+
158
+ ## Sizes
159
+
160
+ A single rung. Min-height 48 (touch-target floor); consumers cannot shrink or grow the card.
161
+
162
+ ## States
163
+
164
+ | State | Overlay | Additional |
165
+ |------------|-------------------------------|------------|
166
+ | `default` | — | Outlined surface at rest. |
167
+ | `hovered` | label tone at `sys.state.hover` | Overlay paints across the container. |
168
+ | `pressed` | label tone at `sys.state.pressed` | Overlay deepens; no other shift. |
169
+ | `disabled` | overlay suppressed | Container at `sys.state.disabled` opacity; `pointer-events: none`. |
170
+
171
+ ## Focus indicator
172
+
173
+ Outward 3-layer ring painted on the container's outer edge via an `::after` overlay (rest stroke sits on `::before`). Trigger: `:focus-visible`. NavCard sits as its own bounded surface with margin to siblings, so an outward ring reads cleanly — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
174
+
175
+ ## Behavior
176
+
177
+ - **Whole card is the click target.** The trailing chevron is never a separate hit target.
178
+ - **Element swap.** Renders `<button>` by default; `<a href>` when `href` is set.
179
+ - **Truncation, not wrap.** Both label and supportingText truncate with ellipsis; the card never grows to fit long text.
@@ -0,0 +1,161 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "NavCard",
4
+ "family": "nav-card",
5
+ "description": "Outlined, rounded single-row card. Single label (optional supporting line) over a `surface`-toned box with a hairline `outlineVariant` inset-shadow stroke and `radius.md` corners. Two variants select the trailing affordance: `default` ships no trailing icon (bare labelled card — settings entry, scope tile, informational drill-target), `nav` auto-renders the right-pointing chevron for explicit drill-in semantics. Whole card is the click target — keyboard focus, hover overlay, and tap commit all sit on the card. Reach for it when one row needs to read as its own discrete affordance rather than as one entry in a [List](../list/list.md) drill-in stack (`text` rows with `nav: true`).",
6
+ "element": "button",
7
+ "props": {
8
+ "variant": {
9
+ "type": "enum",
10
+ "values": ["default", "nav"],
11
+ "default": "default",
12
+ "description": "Trailing affordance. `default` ships NO trailing icon — the card reads as a labelled tile (settings entry, scope card, informational drill-target). `nav` auto-renders the right-pointing chevron — the explicit drill-in form. A consumer-supplied `trailingIcon` prop still overrides on either variant (e.g. an external-link arrow, an expand-down chevron)."
13
+ },
14
+ "label": {
15
+ "type": "string",
16
+ "required": true,
17
+ "description": "Primary card text. Single line; truncates with ellipsis."
18
+ },
19
+ "supportingText": {
20
+ "type": "string",
21
+ "optional": true,
22
+ "description": "Secondary line under the label. Single line; truncates with ellipsis."
23
+ },
24
+ "leading": {
25
+ "type": "node",
26
+ "optional": true,
27
+ "description": "Optional leading slot — a 16px icon glyph (renders in `currentColor`) or a 32-rung [Thumbnail](../thumbnail/thumbnail.md). Block-centred on the row's vertical midline — both icon and thumbnail share the same contract regardless of one-line / two-line body. When omitted the label sits flush at the inline padding edge."
28
+ },
29
+ "trailingIcon": {
30
+ "type": "node",
31
+ "optional": true,
32
+ "description": "Overrides the variant's trailing affordance. Pass a custom 16px glyph when the action isn't a navigation drill-in (inline expand-down arrow, external-link arrow). On `variant=\"default\"` no trailing renders unless `trailingIcon` is supplied; on `variant=\"nav\"` the auto-chevron is replaced."
33
+ },
34
+ "appearance": {
35
+ "type": "enum",
36
+ "values": ["default", "surface"],
37
+ "default": "default",
38
+ "description": "Container fill. `default` is transparent — the card's identity is the outlined chrome (hairline + radius + label + chevron) and the host surface tone reads through. `surface` paints `sys.color.surface` so the card reads as its own opaque tier; reach for it when the card sits on a transparent / non-`surface` host (between bare-surface sections, on a tonal band the card needs to break out of)."
39
+ },
40
+ "href": {
41
+ "type": "string",
42
+ "optional": true,
43
+ "description": "When provided, renders as `<a href>` instead of `<button>`. Mutually exclusive with `onClick` as the primary commit, though either may be combined for analytics."
44
+ },
45
+ "onClick": {
46
+ "type": "function",
47
+ "optional": true
48
+ },
49
+ "disabled": {
50
+ "type": "boolean",
51
+ "default": false
52
+ },
53
+ "forcedState": {
54
+ "type": "literal",
55
+ "values": ["hovered", "pressed", "focused"],
56
+ "optional": true,
57
+ "description": "Docs-only — pins the card to a single visual state via `data-force-state`. Not for production use."
58
+ }
59
+ },
60
+ "slots": {
61
+ "container": {
62
+ "required": true,
63
+ "description": "Outlined rounded box. Transparent fill by default (host tone reads through) — `surface` fill via `appearance=\"surface\"`. `radius.md` corners, hairline `outlineVariant` stroke painted as inset box-shadow (never `border:`). Whole container is the interactive target.",
64
+ "intrinsic": true
65
+ },
66
+ "leading": {
67
+ "required": false,
68
+ "description": "Optional leading slot at 16 × 16 (icon) or 32 × 32 (Thumbnail). Icon glyphs render in `currentColor`. Block-centred on the row's vertical midline — both icon and thumbnail share the same contract regardless of one-line / two-line body.",
69
+ "accepts": ["icon", "thumbnail"]
70
+ },
71
+ "labelCol": {
72
+ "required": true,
73
+ "description": "Vertical column holding label and (optional) supportingText. `min-width: 0` so both lines truncate.",
74
+ "intrinsic": true
75
+ },
76
+ "label": {
77
+ "required": true,
78
+ "description": "Primary card text. 16px / Regular / `onSurface`. Single line; truncates with ellipsis.",
79
+ "accepts": ["text"]
80
+ },
81
+ "supportingText": {
82
+ "required": false,
83
+ "description": "Secondary line under the label. 14px / Regular / `onSurfaceVariant`, 2px below the label. Single line; truncates.",
84
+ "accepts": ["text"]
85
+ },
86
+ "trailingIcon": {
87
+ "required": false,
88
+ "description": "Trailing affordance. On `variant=\"nav\"` an auto-rendered right-pointing chevron at 16px, `onSurfaceVariant`. On `variant=\"default\"` no trailing renders. A `trailingIcon` prop overrides either case. Slot stays block-centred regardless of one-line / two-line body so the chevron reads as the row's commit affordance.",
89
+ "accepts": ["icon"]
90
+ }
91
+ },
92
+ "sizing": {
93
+ "radius": "sys.radius.md",
94
+ "minHeight": "ref.space.600",
95
+ "paddingBlock": "sys.layout.container.xs",
96
+ "paddingInline": "sys.layout.container.md",
97
+ "outlineWidth": "sys.borderWidth.hairline",
98
+ "outlineColor": "sys.color.outlineVariant",
99
+ "leadingGap": "sys.layout.inline.md",
100
+ "trailingGap": "sys.layout.inline.md",
101
+ "leadingIconSize": "sys.icon.md",
102
+ "labelTypo": "sys.typo.body.sm",
103
+ "labelColor": "sys.color.onSurface",
104
+ "supportingTypo": "sys.typo.label.sm",
105
+ "supportingColor": "sys.color.onSurfaceVariant",
106
+ "supportingOffset": "0",
107
+ "trailingIconSize": "sys.icon.md",
108
+ "trailingIconColor": "sys.color.onSurfaceVariant",
109
+ "groupGap": "sys.layout.stack.xs"
110
+ },
111
+ "appearances": {
112
+ "default": {
113
+ "background": "transparent",
114
+ "note": "No fill — the host surface tone reads through. The canonical NavCard: its identity is the outlined chrome (hairline + radius + label + chevron), not a fill. Hover / pressed paint as label-tone overlays mixed on the transparent base so the host tone keeps reading through underneath the state paint."
115
+ },
116
+ "surface": {
117
+ "background": "sys.color.surface",
118
+ "note": "Opaque `surface` fill. Reach for it when the card sits on a transparent / non-`surface` host (a `surfaceContainerLow` tonal band, a coloured hero, a BottomSheet's content slot) and should read as its own opaque tier rather than blending into the host. Outline, label, chevron, and state overlays are unchanged."
119
+ }
120
+ },
121
+ "states": {
122
+ "default": { "overlay": null },
123
+ "hovered": {
124
+ "overlay": { "color": "label", "opacity": "sys.state.hover" }
125
+ },
126
+ "pressed": {
127
+ "overlay": { "color": "label", "opacity": "sys.state.pressed" }
128
+ },
129
+ "disabled": {
130
+ "containerOpacity": "sys.state.disabled",
131
+ "pointerEvents": "none"
132
+ }
133
+ },
134
+ "focusIndicator": {
135
+ "description": "Keyboard-focus visual painted as a three-layer ring on the card's outer edge. Composes over whichever lifecycle state the card is in.",
136
+ "composition": "outward",
137
+ "compositionReason": "NavCard sits as its own bounded surface with margin to siblings; an outward ring reads cleanly without colliding with a neighbouring row's stroke.",
138
+ "ring": {
139
+ "outerWidth": "sys.borderWidth.thin",
140
+ "outerColor": "sys.color.focus",
141
+ "insetWidth": "sys.borderWidth.hairline",
142
+ "insetColor": "sys.color.focusInset",
143
+ "implementation": "outset box-shadow on the container's `::after` overlay; the rest stroke stays painted on `::before` so the two layers don't fight."
144
+ },
145
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
146
+ },
147
+ "behavior": {
148
+ "clickTarget": "Whole card is clickable. The trailing chevron is never a separate hit target.",
149
+ "truncationNotWrap": "Both label and supportingText truncate; the card never grows to fit long text.",
150
+ "elementSwap": "Renders `<button>` by default; `<a href>` when `href` is set."
151
+ },
152
+ "forbidden": [
153
+ "border: painted as the rest stroke — must be inset box-shadow (no-layout strokes rule, DESIGN.md)",
154
+ "rest outline removed — the outlined chrome IS the defining shape of NavCard; without it the component is a List drill-in row in disguise",
155
+ "stacked vertically with raw <div> gaps — group via the NavCardGroup wrapper so the 8px gap stays consistent across compositions",
156
+ "tightly stacked into a hairline-divider rail — for that anatomy use a List drill-in stack (`text` rows with `nav: true`); NavCards in a NavCardGroup must each remain a discrete outlined card",
157
+ "text-decoration underline on label or any descendant on hover — the whole card is the hit target with its own hover overlay; an underline doubles the affordance",
158
+ "trailing chevron replaced with a non-navigational glyph when the action is not a drill-in — use button/icon or a different component instead",
159
+ "destructive commit fired on card tap — destructive commits open a Dialog / BottomSheet, never fire inline"
160
+ ]
161
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "nav-list",
4
+ "name": "NavList",
5
+ "description": "Vertical label-only nav list — labelled block where each row carries a label (and optional secondary line) plus a trailing chevron Icon Button, and routes via `href` / `onClick`. **Preset wrapper** over `<Header /> + <List variant=\"entry\">` rendered label-only (no thumbnail) with a default chevron Icon Button in the trailing slot — no new tokens, no new selection model. If you need any divergence (mixing label-only and thumbnail-bearing rows, swapping the trailing affordance, omitting the header), drop down to `<Header /> + <List variant=\"entry\" />` and compose freely. Reach for `<NavList />` on category indexes, settings menus, and 'pick a sub-page' surfaces where the preset matches verbatim.",
6
+ "useCases": [
7
+ "category index",
8
+ "settings menu",
9
+ "directory of route targets",
10
+ "drill-down navigation list"
11
+ ],
12
+ "visualReuse": "open",
13
+ "layoutInset": "full-bleed",
14
+ "wrapperGuidance": "Owns its inline padding internally. Place as a direct child of the page-shell <main> (or any host that pays the gutter once). Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} — the page rail is paid once at the shell, never on the full-bleed child. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out — see AGENTS.md § Composition rules.",
15
+ "compositionModes": {
16
+ "standalone": {
17
+ "default": true,
18
+ "chrome": {
19
+ "background": "sys.color.surface",
20
+ "padding": "sys.layout.container.lg block / sys.layout.container.md inline"
21
+ },
22
+ "context": "Direct child of the page shell or any host that pays the page-rail gutter once."
23
+ },
24
+ "embedded": {
25
+ "trigger": "prop `embedded={true}` on <NavList /> OR direct child of `.chorus-carousel` / `.chorus-feed` (DOM-ancestry safety net).",
26
+ "chrome": {
27
+ "background": "transparent",
28
+ "padding": "0"
29
+ },
30
+ "context": "Composed inside another rail-responsible host. Row content takes over from the host's content-box edge."
31
+ }
32
+ },
33
+ "usage": {
34
+ "note": "Rows are a label-only `items` array (value/label + href or onClick) — no thumbnail key, and no List or row children; the trailing chevron is automatic.",
35
+ "example": "<NavList label=\"…\" items={[{ value, label, href }]} />"
36
+ },
37
+ "spec": "nav-list.md",
38
+ "subcomponents": [
39
+ {
40
+ "slug": "nav-list",
41
+ "spec": "nav-list.spec.json",
42
+ "md": "nav-list.md",
43
+ "default": true
44
+ }
45
+ ]
46
+ }