@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,221 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "List",
4
+ "family": "list",
5
+ "subcomponent": "standard",
6
+ "description": "The default List variant — display or navigation rows over the shared List anatomy. The whole row is the click target; no selection model. A row is text-only by default (no leading slot); it opts into a **leading image** (the image type) by passing a `thumbnail`, which renders a 40px [Thumbnail](../thumbnail/thumbnail.md) at the leading edge with a 12px (`sys.layout.inline.lg`) gap to the text group, or into a **leading icon** (the icon type) by passing an `icon`, which renders a 24px (`sys.icon.lg`) glyph with an 8px (`sys.layout.inline.md`) gap. A row opts into a trailing drill-in chevron with `nav: true` (the drill-in case — the row routes the user to another surface); a per-item `trailingIcon` overrides the auto chevron. A row opts into an inline **count badge** to the right of the label via `count` — separated from the label by `sys.layout.inline.sm` (4), the unread / status-count case — and it composes with the chevron / trailing slot on the same row. For the richer directory shape (selectable 32/48/56 avatar + stacked `secondary` identity line) reach for [list/entry](./entry.md); Standard's leading image is single-density at the 40 rung.",
7
+ "element": "ul",
8
+ "props": {
9
+ "embedded": {
10
+ "type": "boolean",
11
+ "default": false,
12
+ "description": "Composition mode flag. When `true` (or when the List is a direct child of `.chorus-carousel` / `.chorus-feed`), enters **embedded mode**: zeroes its own `background` + `padding` so chrome defers to the host. Pass explicitly inside `<Carousel>` / `<Feed>` for the contract to be visible in JSX; the DOM-ancestry safety net in styles.css also activates the mode when omitted. See `compositionModes` in `list.family.json`."
13
+ },
14
+ "items": {
15
+ "type": "node",
16
+ "required": true,
17
+ "description": "Array of row descriptors. A row carrying `thumbnail` props forwards them verbatim to the leading Thumbnail (the image type)."
18
+ }
19
+ },
20
+ "slots": {
21
+ "container": {
22
+ "required": true,
23
+ "description": "Outer scroll surface. Vertical stack with a transparent fill (inherits parent container tone); rows separated by a 1px outlineVariant divider, not a gap."
24
+ },
25
+ "row": {
26
+ "required": true,
27
+ "description": "Single list item. Whole row is the interactive target."
28
+ },
29
+ "leading": {
30
+ "required": false,
31
+ "fallbackOnMissingSrc": "sys.color.surfaceContainerHigh",
32
+ "description": "The leading slot — omitted by default (no reserved leading whitespace), hosting one of two mutually exclusive types. **Image type** (`thumbnail`): a [Thumbnail](../thumbnail/thumbnail.md) at the 40 rung, vertically centred against the label column, with a 12px (`sys.layout.inline.lg`) gap to the text group; `thumbnail` props (`src`, `alt`, `updateDot`, `logoBadge`) forward verbatim, and `fallbackOnMissingSrc` is the dim-tone fill it paints when `src` is empty / fails to load (at scaffold time, agents fill `src` with `/placeholder.png` rather than relying on the fallback). **Icon type** (`icon`): a 24px (`sys.icon.lg`) decorative glyph in `onSurfaceVariant`, vertically centred, with an 8px (`sys.layout.inline.md`) gap to the text group — the category-mark shape for settings / menu rows. The slot enforces the 24 rung regardless of the glyph's own `size`. A row passes `thumbnail` or `icon`, never both.",
33
+ "accepts": [
34
+ "thumbnail",
35
+ "icon"
36
+ ]
37
+ },
38
+ "label": {
39
+ "required": true,
40
+ "description": "Primary row text. 16px / Regular / onSurface. Single line; truncates with ellipsis. Pairs flush with the inline `count` slot — `sys.layout.inline.sm` (4) horizontal separation, baseline-aligned, on the primary line; the label shrinks first so a long label truncates against the count.",
41
+ "accepts": [
42
+ "text"
43
+ ]
44
+ },
45
+ "count": {
46
+ "required": false,
47
+ "description": "Optional inline node painted to the right of the label on the same line — the unread / status-count case. Canonical fill is a numeric [Badge](../badge/badge.md). Separated from the label by `sys.layout.inline.sm` (4); the label shrinks first so a long label truncates against the count, which stays pinned at its intrinsic width. Distinct from `trailingIcon`: `count` tiles tight to the label inside the text group, while `trailingIcon` / `navChevron` sit at the row's trailing edge — so the two compose (a drill-in row can carry a count next to the label and keep its trailing chevron). For the avatar-anchored directory shape with an inline count, reach for [list/entry](./entry.md) instead.",
48
+ "accepts": [
49
+ "badge"
50
+ ]
51
+ },
52
+ "supportingText": {
53
+ "required": false,
54
+ "description": "Secondary line under label. 14px / Regular / onSurfaceVariant, sits directly under the label with no extra gap.",
55
+ "accepts": [
56
+ "text"
57
+ ]
58
+ },
59
+ "trailingIcon": {
60
+ "required": false,
61
+ "description": "Consumer-supplied node at the trailing edge. A 16px icon (e.g. an external-link mark replacing the drill-in chevron), or — on a leading-image row — a small text button (Follow / Invite). A status **badge** does not belong here — it tiles next to the label via the `count` slot so every badge in the list reads at the same position; the trailing edge stays for icons / buttons. Taps on the trailing slot stop propagating before they reach the row's `onClick`, so it is a separate hit target. Overrides the auto-rendered nav chevron on the same row.",
62
+ "accepts": [
63
+ "icon",
64
+ "button"
65
+ ]
66
+ },
67
+ "navChevron": {
68
+ "required": false,
69
+ "description": "Auto-rendered 16px right-pointing chevron at the trailing edge, painted when the row sets `nav: true` — the drill-in affordance signalling the row routes to another surface. `onSurfaceVariant` tone, decorative (`aria-hidden`); never a separate hit target — the whole row is the click target. A per-item `trailingIcon` replaces it."
70
+ }
71
+ },
72
+ "rowProps": {
73
+ "value": {
74
+ "type": "string",
75
+ "required": true
76
+ },
77
+ "label": {
78
+ "type": "string",
79
+ "required": true
80
+ },
81
+ "supportingText": {
82
+ "type": "string",
83
+ "optional": true
84
+ },
85
+ "count": {
86
+ "type": "node",
87
+ "optional": true,
88
+ "description": "Inline count node painted to the right of the label on the same line. Canonical fill: `<Badge>{n}</Badge>`. Composes with `nav` / `trailingIcon` — the count sits by the label, the chevron / trailing node stays at the row's trailing edge."
89
+ },
90
+ "thumbnail": {
91
+ "type": "object",
92
+ "optional": true,
93
+ "description": "The image type — when present, renders a leading 40px Thumbnail (props `src`, `alt`, optional `updateDot`, optional `logoBadge` forwarded verbatim). Omit for a text-only row. Mutually exclusive with `icon`."
94
+ },
95
+ "icon": {
96
+ "type": "node",
97
+ "optional": true,
98
+ "description": "The icon type — when present, renders a leading 24px (`sys.icon.lg`) decorative glyph in `onSurfaceVariant`, 8px (`sys.layout.inline.md`) from the text group. Canonical fill: a category icon (`<BellIcon />`, `<BookmarkIcon />`). The slot forces the 24 rung, so `size` on the passed glyph is ignored. Mutually exclusive with `thumbnail`."
99
+ },
100
+ "trailingIcon": {
101
+ "type": "node",
102
+ "optional": true,
103
+ "description": "Trailing-edge node. Wrapped in a slot that stops click propagation so the slot is its own hit target separate from the row's `onClick`."
104
+ },
105
+ "divider": {
106
+ "type": "boolean",
107
+ "default": true,
108
+ "description": "Per-row bottom-divider opt-out. Pass `divider: false` to suppress the hairline `outlineVariant` rule beneath the row; the row's footprint and inline padding stay unchanged. Reach for it when a visual group ends mid-stack and the divider would visually fence off the next group from its label. The last row already omits its divider via `:not(:last-child)`."
109
+ },
110
+ "nav": {
111
+ "type": "boolean",
112
+ "default": false,
113
+ "description": "Drill-in case — when `true`, the row auto-renders a trailing right-pointing chevron marking that it routes to another surface. Overridden by a per-item `trailingIcon`."
114
+ },
115
+ "disabled": {
116
+ "type": "boolean",
117
+ "default": false
118
+ },
119
+ "strong": {
120
+ "type": "boolean",
121
+ "default": false,
122
+ "description": "Per-row label-emphasis opt-in (family-wide contract — see [list.md § Cross-sub contract](./list.md)). When `true`, the row's label weight promotes from `body.md` Regular (400) to `label.lg` Semibold (600) at the same 16px size and 1.5 line-height — geometry stays identical, only the glyph stroke thickens. Use sparingly to mark one primary row inside a denser scan; a stack where every row is `strong` defeats the marker."
123
+ },
124
+ "onClick": {
125
+ "type": "function",
126
+ "optional": true
127
+ },
128
+ "forcedState": {
129
+ "type": "literal",
130
+ "values": [
131
+ "hovered",
132
+ "pressed",
133
+ "focused"
134
+ ],
135
+ "optional": true,
136
+ "description": "Docs-only — pins the row to a single visual state via `data-force-state` so the contract can be inspected in a static preview. Not for production use."
137
+ }
138
+ },
139
+ "sizing": {
140
+ "rowPaddingBlock": "ref.space.100",
141
+ "rowPaddingInline": "ref.space.200",
142
+ "rowMinHeight": "ref.space.600",
143
+ "leadingTextGap": "sys.layout.inline.lg",
144
+ "leadingTextGapNote": "12px (`sys.layout.inline.lg`) between a leading 40-rung Thumbnail (the image type, an image leading) and the text group — the image-leading rung of the family's role-based row spacing, wider than the 8px icon rung so the avatar and the label column read as two distinct blocks. Applies only to rows that carry a `thumbnail`; text-only rows have no leading slot.",
145
+ "leadingIconSize": "sys.icon.lg",
146
+ "leadingIconGap": "sys.layout.inline.md",
147
+ "leadingIconGapNote": "8px (`sys.layout.inline.md`) between a leading 24px (`sys.icon.lg`) icon (the icon type) and the text group — the icon-leading rung of the family's role-based row spacing (the base leading gap), narrower than the 12px image rung. Applies only to rows that carry an `icon`.",
148
+ "trailingActionGap": "sys.layout.inline.md",
149
+ "trailingActionGapNote": "Fixed 8px (`sys.layout.inline.md`) between the text group and a trailing icon / nav chevron — the family-wide trailing gap, identical across every List variant.",
150
+ "labelToCountGap": "sys.layout.inline.sm",
151
+ "labelToCountGapNote": "4px (`sys.layout.inline.sm`) between the label and an inline `count` badge — they tile flush on the primary line as one label+count block, narrower than the 8px trailing gap. Mirrors list/entry's identity-group spacing.",
152
+ "dividerWidth": "sys.borderWidth.hairline",
153
+ "dividerColor": "sys.color.outlineVariant",
154
+ "dividerPerRowOptOut": "Pass `divider: false` on a row to suppress its bottom divider.",
155
+ "leadingThumbnailSize": 40,
156
+ "labelTypo": "sys.typo.body.md",
157
+ "labelColor": "sys.color.onSurface",
158
+ "supportingTypo": "sys.typo.body.sm",
159
+ "supportingColor": "sys.color.onSurfaceVariant",
160
+ "supportingOffset": "0",
161
+ "trailingIconSize": "16 × 16",
162
+ "trailingIconColor": "sys.color.onSurfaceVariant",
163
+ "navChevronSize": "16 × 16",
164
+ "navChevronColor": "sys.color.onSurfaceVariant"
165
+ },
166
+ "states": {
167
+ "default": {
168
+ "overlay": null
169
+ },
170
+ "hovered": {
171
+ "overlay": {
172
+ "color": "label",
173
+ "opacity": "sys.state.hover"
174
+ }
175
+ },
176
+ "pressed": {
177
+ "overlay": {
178
+ "color": "label",
179
+ "opacity": "sys.state.pressed"
180
+ }
181
+ },
182
+ "disabled": {
183
+ "containerOpacity": "sys.state.disabled",
184
+ "containerOpacityScope": "Dims the row content only — the inter-row divider and the focus overlay keep full opacity, so a disabled row never fades the hairline rule between it and the next row.",
185
+ "pointerEvents": "none"
186
+ }
187
+ },
188
+ "focusIndicator": {
189
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the row is in.",
190
+ "composition": "inward",
191
+ "compositionReason": "Rows tile the column flush with only a hairline `outlineVariant` divider between them; an outward ring would overlap the divider and the neighbouring row.",
192
+ "overlay": {
193
+ "color": "label",
194
+ "opacity": "sys.state.focus"
195
+ },
196
+ "ring": {
197
+ "outerWidth": "sys.borderWidth.thin",
198
+ "outerColor": "sys.color.focus",
199
+ "outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
200
+ "insetWidth": "sys.borderWidth.hairline",
201
+ "insetColor": "sys.color.focusInset",
202
+ "insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
203
+ "implementation": "inset box-shadow on the row's `::before` overlay (the `::after` carries the inter-row divider). Constrained strictly inside the row's footprint and never exceeds it."
204
+ },
205
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
206
+ },
207
+ "behavior": {
208
+ "keyboardNavigation": "Arrow up/down moves focus between rows. Home and End jump to first / last.",
209
+ "rowClickTarget": "Whole row is clickable when an `onClick` is bound to the item. A leading thumbnail is never a separate hit target.",
210
+ "navDrillIn": "A row with `nav: true` renders the trailing chevron and routes the user to another surface on click (consumer wires `onClick`). The chevron is decorative — the whole row is the single click target, never the chevron alone.",
211
+ "trailingHitTarget": "Clicks inside `trailingIcon` stop propagating before they reach the row — wire a favorite / mute / pin toggle or a Follow button there without it committing the row's primary action.",
212
+ "truncationNotWrap": "Both label and supportingText truncate; the row never grows to fit long text."
213
+ },
214
+ "forbidden": [
215
+ "row leading content (thumbnail) as a separate hit area — the entire row is the click target",
216
+ "nav chevron as a separate hit target — the drill-in chevron is decorative; the whole row is the click target",
217
+ "leading thumbnail at a size other than the row's intrinsic 40 rung",
218
+ "compact directory rows (selectable 32/48/56 avatar + stacked secondary line + optional toggle) built as a leading-image Standard row — that directory anatomy is [list/entry](./entry.md); Standard's image type is single-density at the 40 rung. (Standard does carry an inline `count` badge next to the label for unread / status counts, but not the avatar-rung directory shape.)",
219
+ "raw `border:` on the row — list seam is the family's bottom divider via outlineVariant"
220
+ ]
221
+ }
@@ -0,0 +1,69 @@
1
+ # Compact
2
+
3
+ One-line author attribution for comment and reply rows. The [Standard](./standard.md) head with its primary line (avatar + entity name + timestamp + follow) omitted — only the secondary meta-link row renders, with the posting time relocated to the line's trailing edge: company name · nickname (optional role badge) · timestamp.
4
+
5
+ **Reach for this when** attributing a comment or reply where the vertical budget is a single text line and the author identity is company + nickname. **Skip when** composing the head of a Feed Post or Feed Ad — the avatar + two-line cluster (use [Standard](./standard.md)) — or when the row is a generic entity directory entry with a trailing commit (use [list/entry](../list/entry.md)).
6
+
7
+ **Layout inset.** `inline` — Compact ships no padding of its own. Sits inside whichever comment / reply row composes it; the host pays surrounding rhythm, divider, and tap target.
8
+
9
+ ## Default
10
+
11
+ The canonical fill — company name and nickname as independent links, plain timestamp last. Each middot keeps the family's `line-height: 1` rule so the dot never inflates the text line.
12
+
13
+ ```preview
14
+ metadata/compact/default
15
+ ---
16
+ import { Metadata } from '@teamblind-chorus/ui';
17
+
18
+ <Metadata
19
+ variant="compact"
20
+ meta={['Maple Mill Bakery', 'ryestarter']}
21
+ timestamp="35m"
22
+ />
23
+ ```
24
+
25
+ ## Use cases
26
+
27
+ ### With role badge
28
+
29
+ An object identity item carries `badge` — a single presentational mark rendered after the nickname's link, outside the `<a>`, at the middots' 4px gap. At most one badge rides the nickname, never a stack.
30
+
31
+ ```preview
32
+ metadata/compact/badge
33
+ ---
34
+ import { Metadata, Badge } from '@teamblind-chorus/ui';
35
+
36
+ <Metadata
37
+ variant="compact"
38
+ meta={[
39
+ 'Maple Mill Bakery',
40
+ { label: 'ryestarter', href: '#', badge: <Badge variant="role">Verified</Badge> },
41
+ ]}
42
+ timestamp="35m"
43
+ />
44
+ ```
45
+
46
+ ## Slots
47
+
48
+ - **container** — outer row hosting the single text line. `min-width: 0` so the line truncates inside a flexed comment row.
49
+ - **meta** — identity link row: company name then nickname, each its own `<a>`; siblings separate by middot; never wraps. An object item may carry `badge` — one presentational mark after the link, outside the `<a>`.
50
+ - **timestamp** — trailing plain-text posting time after the last middot. Never a link.
51
+
52
+ ## Anatomy
53
+
54
+ | Slot | Token bindings |
55
+ |---------------|----------------|
56
+ | meta | `sys.typo.label.sm` / `sys.color.onSurfaceVariant`, links inherit; underline on hover |
57
+ | timestamp | `sys.typo.label.sm` / `sys.color.outline` |
58
+ | dot separator | `·` glyph, `color: sys.color.outline`, **`line-height: 1`** so its line-box equals its font-size — never inflates the text line |
59
+
60
+ ## States
61
+
62
+ Compact Metadata itself has no lifecycle state. Each identity link carries its own state contract; the timestamp is inert.
63
+
64
+ ## Behavior
65
+
66
+ - **Dot height constrained.** Every middot separator (between identity items and before the timestamp) uses `line-height: 1` so its line-box matches its font-size — same family-wide rule as the standard head.
67
+ - **Independent affordances.** Identity items are independent `<a>` links — taps land on the link, not the host row. The timestamp is plain text and never intercepts the row's own tap target.
68
+ - **Single line.** The cluster never wraps. When the host row is narrower than the cluster, the identity links truncate; the timestamp keeps its full width.
69
+ - **Time last.** The canonical order is company · nickname · timestamp — the posting time always closes the line.
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Metadata",
4
+ "family": "metadata",
5
+ "subcomponent": "compact",
6
+ "description": "One-line author attribution for comment / reply rows — the [Standard](standard.md) head with its primary line (avatar + entity name + timestamp + follow) omitted. Only the secondary meta-link row renders, with the posting time relocated to the line's trailing edge: company name · nickname (bare, no @ prefix, optional single role badge) · timestamp. The identity items keep the standard meta-row grammar (independent `<a>` links, middot separators, badge outside the link); the timestamp is plain text in `sys.color.outline` so the line reads identity-first, time-last. `layoutInset: inline` — atom-shaped, pays no padding of its own; the host comment row owns the gutter / divider / click target.",
7
+ "element": "div",
8
+ "props": {
9
+ "variant": {
10
+ "type": "literal",
11
+ "value": "compact"
12
+ },
13
+ "meta": {
14
+ "type": "node",
15
+ "required": true,
16
+ "description": "Array of independently-linked identity items — canonical fill is `[company name, nickname]`, the nickname canonically last, displayed bare (no @ prefix). 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). Items separate by middot. Same grammar as the standard sub's `meta`; here it is the whole cluster, so it is required."
17
+ },
18
+ "timestamp": {
19
+ "type": "string",
20
+ "required": true,
21
+ "description": "Posting time at the line's trailing edge — plain text (never a link), preceded by a middot, in `label.sm` / `sys.color.outline` so it recedes behind the identity links. Required: the timestamp is what distinguishes a compact attribution from a bare identity row."
22
+ }
23
+ },
24
+ "slots": {
25
+ "container": {
26
+ "required": true,
27
+ "description": "Outer row — hosts the single text line. `min-width: 0` so the line can truncate inside a flexed comment row.",
28
+ "intrinsic": true
29
+ },
30
+ "meta": {
31
+ "required": true,
32
+ "description": "Identity meta-link row — company name then nickname, each its own `<a>`; siblings separate by middot; `flex-wrap: nowrap` so the cluster stays a single text line. Hover paints the underline on the link alone — the middot stays unstyled.",
33
+ "intrinsic": true
34
+ },
35
+ "timestamp": {
36
+ "required": true,
37
+ "description": "Trailing plain-text posting time after the last middot. `sys.typo.label.sm` / `sys.color.outline` — one tonal step further than the identity links.",
38
+ "accepts": [
39
+ "text"
40
+ ]
41
+ }
42
+ },
43
+ "sizing": {
44
+ "metaTypo": "sys.typo.label.sm",
45
+ "metaColor": "sys.color.onSurfaceVariant",
46
+ "timestampTypo": "sys.typo.label.sm",
47
+ "timestampColor": "sys.color.outline",
48
+ "dotColor": "sys.color.outline",
49
+ "dotLineHeight": "1",
50
+ "dotLineHeightNote": "Same family-wide rule as the standard sub: 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 — the dot never inflates the single text line.",
51
+ "metaSeparatorInset": "sys.layout.inline.sm"
52
+ },
53
+ "states": {
54
+ "note": "Compact Metadata itself has no lifecycle states — each identity link carries its own state contract; the timestamp is inert."
55
+ },
56
+ "behavior": {
57
+ "dotHeight": "Every middot separator (between identity items and before the timestamp) uses `line-height: 1` so its line-box matches its font-size — same family-wide rule as the standard sub.",
58
+ "linkAffordances": "Identity items render as independent `<a>` elements — taps land on the link, not the host row. The timestamp is plain text and never intercepts the row's own tap target.",
59
+ "singleLine": "The cluster is one text line — the meta row never wraps. When the host row is narrower than the cluster, the identity links truncate; the timestamp keeps its full width.",
60
+ "metaBadge": "An object identity item's `badge` node renders after the item's `<a>` at the middots' 4px (`inline.sm`) gap — the mark annotates the nickname without becoming link content."
61
+ },
62
+ "forbidden": [
63
+ "avatar, entity-name primary line, follow toggle, subtitle, or trailing slot painted on the compact form — those belong to the [Standard](standard.md) head; compact is the one-line text-only shape",
64
+ "timestamp omitted or rendered as a link — the plain trailing timestamp anchors the line and is what makes it an attribution",
65
+ "timestamp placed before the identity items — the canonical order is company · nickname · timestamp, time last",
66
+ "nickname displayed with an @ prefix — nicknames render bare, family-wide",
67
+ "more than one badge riding the nickname — at most one role mark, never a stack"
68
+ ]
69
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "metadata",
4
+ "name": "Metadata",
5
+ "description": "Author / brand attribution cluster shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and an optional secondary line (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). Optional trailing slot hosts a row-level affordance like the dismiss × button on ads. The one-line [Compact](compact.md) sub drops the primary line for comment / reply attribution: company name · nickname (optional role badge) · trailing timestamp. The middot separator's line-box is constrained to its font-size so the dot never inflates the surrounding text line.",
6
+ "useCases": [
7
+ "feed post author attribution",
8
+ "feed ad brand attribution",
9
+ "sponsored placement attribution",
10
+ "comment / reply author attribution (compact)"
11
+ ],
12
+ "visualReuse": "open",
13
+ "layoutInset": "inline",
14
+ "wrapperGuidance": "Atom — pays no inline padding of its own. Sits inside whichever Feed sub-component composes it (Post / Ad card head); the host pays surrounding rhythm. Do NOT wrap Metadata in a per-row padding-inline div.",
15
+ "usage": {
16
+ "note": "Both shapes are the single `Metadata` export — the one-line comment shape is `variant=\"compact\"`, NOT a separate `<MetadataCompact>`. Leading thumbnail is the `avatar` prop (NOT `thumbnail`); the secondary line is either a `subtitle` string or a `meta` string array — not children.",
17
+ "subs": {
18
+ "standard": {
19
+ "import": "Metadata",
20
+ "example": "<Metadata avatar={{ src, alt }} name=\"…\" nameHref=\"#\" timestamp=\"2h\" meta={[\"…\"]} />"
21
+ },
22
+ "compact": {
23
+ "variant": "compact",
24
+ "example": "<Metadata variant=\"compact\" meta={[\"Maple Mill Bakery\", \"ryestarter\"]} timestamp=\"35m\" />"
25
+ }
26
+ }
27
+ },
28
+ "spec": "metadata.md",
29
+ "subcomponents": [
30
+ {
31
+ "slug": "standard",
32
+ "spec": "standard.spec.json",
33
+ "md": "standard.md",
34
+ "default": true
35
+ },
36
+ {
37
+ "slug": "compact",
38
+ "spec": "compact.spec.json",
39
+ "md": "compact.md"
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,26 @@
1
+ # Metadata
2
+
3
+ Author / brand attribution cluster. The family covers two shapes riding the same `label.sm` text grammar: **[Standard](./standard.md)** — the avatar + two-line head shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md) (entity name + optional timestamp / follow on top, subtitle or meta-link row below); **[Compact](./compact.md)** — the one-line comment / reply attribution (company name · nickname · timestamp, no avatar). Per-sub intent lives on each sub's page.
4
+
5
+ **Layout inset.** `inline` — Metadata ships no padding of its own. Sits inside whichever host composes it (Feed Post / Ad card head, comment row); the host pays surrounding rhythm, divider, and tap target.
6
+
7
+ ## Cross-sub contract
8
+
9
+ Three rules hold across every family member.
10
+
11
+ ### Dot height
12
+
13
+ Every 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 — the U+00B7 glyph's natural vertical extent never inflates the host text line. The dot paints in `sys.color.outline` and is always inline, never a block element.
14
+
15
+ ### Bare nickname, single badge
16
+
17
+ The user's nickname is canonically the last identity item and displays bare — no @ prefix. An identity item may carry at most ONE presentational mark node (canonical fill: [badge/role](../badge/badge.md)), rendered after the item's link and outside the `<a>` so the mark never becomes link content.
18
+
19
+ ### Independent link affordances
20
+
21
+ Every identity item (entity name, meta items) is its own `<a>` — taps land on the link, not the host row. Plain-text parts (timestamp, subtitle) never intercept the row's tap target.
22
+
23
+ ## Sub-components
24
+
25
+ - **[Standard](./standard.md)** *(default)* — Avatar + two-line attribution head for Feed Post / Feed Ad. Primary line (name + timestamp + follow) over an optional secondary line (subtitle or meta-link row); optional trailing affordance.
26
+ - **[Compact](./compact.md)** — One-line comment / reply attribution: company name · nickname (optional role badge) · trailing plain timestamp. Reached as `variant="compact"` on the same `Metadata` export.
@@ -0,0 +1,104 @@
1
+ # Standard
2
+
3
+ The default attribution head of the [Metadata](./metadata.md) family, shared by [Feed Post](../feed/post.md) and [Feed Ad](../feed/ad.md). A leading 32-rung [Thumbnail](../thumbnail/thumbnail.md), a primary line (entity name + optional inline timestamp + optional follow toggle), and an optional secondary line (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). Optional trailing slot hosts a row-level affordance like the dismiss × button on ads.
4
+
5
+ **Reach for this when** composing the head of a Feed Post or Feed Ad — the cluster owns the avatar + identity + meta. **Skip when** you need a one-line comment / reply attribution — company name · nickname · timestamp, no avatar (use [Compact](./compact.md)) — a generic entity directory row with up to three text lines and a single trailing commit (use [list/entry](../list/entry.md)), a labelled-region heading (use [Header](../header/header.md)), or a profile / channel page hero (use [Profile header](../profile-header/profile-header.md)).
6
+
7
+ **Layout inset.** `inline` — Metadata ships no padding of its own. Sits inside whichever Feed sub-component composes it; the host pays surrounding rhythm.
8
+
9
+ ## Default
10
+
11
+ The Feed Post shape — avatar + channel name with timestamp + meta link row (location, job, nickname — bare, no @ prefix).
12
+
13
+ ```preview
14
+ metadata/standard/default
15
+ ---
16
+ import { Metadata } from '@teamblind-chorus/ui';
17
+
18
+ <Metadata
19
+ avatar={{ src: '/placeholder.png', alt: 'Sourdough Bakers' }}
20
+ name="Sourdough Bakers"
21
+ nameHref="#"
22
+ timestamp="2h"
23
+ meta={['Brooklyn, NY', 'Home baker', 'crustcrumb']}
24
+ />
25
+ ```
26
+
27
+ ## Use cases
28
+
29
+ ### With follow action
30
+
31
+ The inline follow toggle. A middot separates it from the timestamp; the toggle paints in `sys.color.primary` at rest and `sys.color.onSurfaceVariant` when active. The dot's line-box stays inside its font-size so the row's text-line stays tight.
32
+
33
+ ```preview
34
+ metadata/standard/follow
35
+ ---
36
+ import { Metadata } from '@teamblind-chorus/ui';
37
+
38
+ <Metadata
39
+ avatar={{ src: '/placeholder.png', alt: 'Indie Game Devs' }}
40
+ name="Indie Game Devs"
41
+ nameHref="#"
42
+ timestamp="4h"
43
+ followAction
44
+ meta={['Solo dev', 'First release', 'sidequest']}
45
+ />
46
+ ```
47
+
48
+ ### Sponsored (ad)
49
+
50
+ Feed Ad shape — `subtitle="Sponsored"` paints a plain caption-tone line under the brand name (no link affordance, no meta row). Pair with `trailing` to host the dismiss × button.
51
+
52
+ ```preview
53
+ metadata/standard/sponsored
54
+ ---
55
+ import { Metadata } from '@teamblind-chorus/ui';
56
+ import { XIcon } from '@teamblind-chorus/ui/icons';
57
+
58
+ <Metadata
59
+ avatar={{ src: '/placeholder.png', alt: 'Acme Coffee' }}
60
+ name="Acme Coffee"
61
+ subtitle="Sponsored"
62
+ trailing={
63
+ <button type="button" aria-label="Dismiss ad" onClick={() => {}}>
64
+ <XIcon size={16} />
65
+ </button>
66
+ }
67
+ />
68
+ ```
69
+
70
+ ## Slots
71
+
72
+ - **container** — outer flex row. `align-items: center`, `sys.layout.inline.md` (8px) gap between avatar, text column, and trailing slot.
73
+ - **avatar** *(optional)* — leading [Thumbnail](../thumbnail/thumbnail.md) at `size={32}`. Forwards every Thumbnail prop verbatim.
74
+ - **text** — two-line text column. Primary line on top, optional secondary line below. `flex: 1 1 auto`, `min-width: 0` so both lines truncate.
75
+ - **name** — entity name. `<a>` when `nameHref` is set, `<span>` otherwise. `sys.typo.label.sm` / `sys.color.onSurface`. Single line; truncates.
76
+ - **timestamp** *(optional)* — inline timestamp after the name. `sys.typo.label.sm` / `sys.color.outline`.
77
+ - **followAction** *(optional)* — bare-text follow toggle at the primary line's trailing edge. Preceded by a middot.
78
+ - **subtitle** *(optional, ad)* — plain caption-tone secondary line. Mutually exclusive with `meta`.
79
+ - **meta** *(optional, post)* — secondary line meta-link row. Each item is its own `<a>`; siblings separate by middot. The last item is canonically the user's nickname, displayed bare (no @ prefix). An object item may carry `badge` — a single presentational mark node rendered after the link, outside the `<a>` (canonical fill: [badge/role](../badge/badge.md) on the nickname — at most one badge rides the nickname, never a stack).
80
+ - **trailing** *(optional)* — trailing-edge slot. Canonical fill is the Feed Ad dismiss × button.
81
+
82
+ ## Anatomy
83
+
84
+ | Slot | Token bindings |
85
+ |---------------|----------------|
86
+ | container | Flex row, `sys.layout.inline.md` (8) gap, `align-items: center` |
87
+ | avatar | [Thumbnail](../thumbnail/thumbnail.md) `size={32}` |
88
+ | name | `sys.typo.label.sm` / Semibold / `sys.color.onSurface`, single-line ellipsis |
89
+ | timestamp | `sys.typo.label.sm` / `sys.color.outline` |
90
+ | dot separator | `·` glyph, `color: sys.color.outline`, **`line-height: 1`** so its line-box equals its font-size — never inflates the text line |
91
+ | follow | `sys.typo.label.sm` / Semibold / `sys.color.primary` (active → `sys.color.onSurfaceVariant`) |
92
+ | subtitle | `sys.typo.label.sm` / `sys.color.onSurfaceVariant` |
93
+ | meta | `sys.typo.label.sm` / `sys.color.onSurfaceVariant`, links inherit; underline on hover |
94
+
95
+ ## States
96
+
97
+ Metadata itself has no lifecycle state. Each interactive child (name link, follow toggle, meta links, trailing button) carries its own state contract.
98
+
99
+ ## Behavior
100
+
101
+ - **Dot height constrained.** Every middot separator (between primary-line items and between meta items) uses `line-height: 1` so its line-box matches its font-size. The U+00B7 glyph's natural vertical extent never inflates the parent's text-line.
102
+ - **Independent affordances.** Name and meta items are independent `<a>` links — taps land on the link, not the row. Trailing slot's clicks stop propagating so the dismiss button never commits the surrounding row.
103
+ - **Meta badge outside the link.** An object meta item's `badge` node renders after the item's `<a>` at the middots' 4px (`inline.sm`) gap — the mark annotates the nickname without becoming link content, so the link's hover / focus target stays text-only.
104
+ - **Secondary line mutex.** `subtitle` and `meta` are mutually exclusive — pass one, not both. When both are passed, `meta` wins.