@teamblind-chorus/ui 1.0.1 โ†’ 1.2.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 (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
@@ -0,0 +1,69 @@
1
+ # EmptyState
2
+
3
+ The centered composition a surface paints when it holds no data yet โ€” an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA that performs the one action that fills the surface.
4
+
5
+ **Reach for this when** a feed, list, inbox, or search result would otherwise paint blank โ€” a fresh account's empty feed, a search returning nothing, a notifications surface with nothing unread. **Skip when** the data is still loading (use [Skeleton](../skeleton/skeleton.md) โ€” an in-flight tonal placeholder) or when the message is a transient confirmation (use [Toast](../toast/toast.md)).
6
+
7
+ **Layout inset.** inline โ€” EmptyState ships no surface fill or chrome of its own and claims no page rail. It centers its column inside the host surface that would otherwise hold the data; the host supplies the surface tier and the bounding box. The surrounding inset is the host's responsibility.
8
+
9
+ ## Default
10
+
11
+ The full composition โ€” illustration, headline, body, and a primary CTA. Three lines of copy at most: what the surface is for, why it is empty, and the one action that fills it. The CTA is the surface's primary action, composed as a default-size primary [Button](../button/button.md).
12
+
13
+ ```preview
14
+ empty-state/default
15
+ ---
16
+ import { EmptyState } from '@teamblind-chorus/ui';
17
+ import { ChatIcon } from '@teamblind-chorus/ui/icons';
18
+
19
+ <EmptyState
20
+ illustration={<ChatIcon />}
21
+ headline="No posts yet"
22
+ body="Conversations you start or join will appear here."
23
+ action={{ label: 'Start a post', onClick: () => {} }}
24
+ />
25
+ ```
26
+
27
+ ## Use cases
28
+
29
+ ### Without illustration
30
+
31
+ The illustration is optional โ€” omit it for a tighter, text-led zero state where a glyph would add nothing. The headline still leads; the body and CTA follow the same stack rhythm.
32
+
33
+ ```preview
34
+ empty-state/no-illustration
35
+ ---
36
+ import { EmptyState } from '@teamblind-chorus/ui';
37
+
38
+ <EmptyState
39
+ headline="No results"
40
+ body="No channels match that search. Try a different keyword."
41
+ action={{ label: 'Clear search', onClick: () => {} }}
42
+ />
43
+ ```
44
+
45
+ ## Slots
46
+
47
+ - **container** โ€” centered flex column holding the whole composition. `align-items: center`, `text-align: center`. No surface fill of its own. `role="status"` so the empty state is announced without yanking focus.
48
+ - **illustration** *(optional)* โ€” centered glyph / illustration above the headline. `ref.space.600` (48) box, painted monochrome in `sys.color.onSurfaceVariant`.
49
+ - **headline** โ€” required lead line. `sys.typo.heading.sm` / `sys.color.onSurface`.
50
+ - **body** *(optional)* โ€” supporting line below the headline. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`.
51
+ - **action** *(optional)* โ€” the primary CTA, composed from the `action` object as a default-size primary [Button](../button/button.md). There is no free `cta` slot.
52
+
53
+ ## Anatomy
54
+
55
+ | Slot | Token bindings |
56
+ |--------------|----------------|
57
+ | container | Centered flex column, `align-items: center`, `text-align: center`, no fill; `role="status"` |
58
+ | illustration | `ref.space.600` (48 ร— 48) box, `sys.color.onSurfaceVariant` (`currentColor`), `sys.layout.stack.sm` (12) below it to the headline |
59
+ | headline | `sys.typo.heading.sm`, `sys.color.onSurface` |
60
+ | body | `sys.typo.body.sm`, `sys.color.onSurfaceVariant`, `sys.layout.stack.2xs` (4) above it from the headline |
61
+ | action | Default-size primary [Button](../button/button.md), `sys.layout.stack.md` (16) above it from the body |
62
+
63
+ ## Behavior
64
+
65
+ - **Centered in the host.** The composition is centered block + inline inside the surface that would otherwise hold the data. EmptyState owns no surface fill โ€” the host supplies the surface tier and bounding box.
66
+ - **Headline required, the rest optional.** Only the headline is required. The illustration, body, and CTA fill in as the copy needs them.
67
+ - **Three lines of copy, one action.** Headline + body read as at most three lines (what the surface is for ยท why it is empty ยท the one action that fills it). The CTA is that one action.
68
+ - **CTA is composed, not slotted.** Pass `action={{ label, href?/onClick? }}` โ€” EmptyState renders the default-size primary Button so the fill action always reads as primary.
69
+ - **Not a loading state.** For data that is still arriving, reach for [Skeleton](../skeleton/skeleton.md); EmptyState is the durable no-data surface.
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "EmptyState",
4
+ "family": "empty-state",
5
+ "description": "The centered composition a surface paints when it holds no data yet โ€” an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA โ€” stacked and centered inside the space the real content would occupy. Three lines of copy at most: what the surface is for, why it is empty, and the one action that fills it (the CTA). Reach for it whenever a feed, list, inbox, or search result would otherwise paint blank; the system hard-rule forbids leaving a no-data surface empty. Distinct from Skeleton (an in-flight tonal placeholder for data that is loading), this is the durable zero-state surface.",
6
+ "element": "div",
7
+ "props": {
8
+ "illustration": {
9
+ "type": "node",
10
+ "optional": true,
11
+ "description": "Optional leading glyph or illustration, centered above the headline. Sized to a `ref.space.600` (48) box โ€” larger than `sys.icon.lg` (24), realizing DESIGN.md's `icon.xl` or larger intent (no `icon.xl` icon-size rung exists; the icon scale stops at `lg`). Painted in `sys.color.onSurfaceVariant` via `currentColor` so it reads as quiet, monochrome chrome โ€” illustrations stay monochrome unless they carry deliberate brand-moment intent. Separated from the headline by `sys.layout.stack.sm` (12)."
12
+ },
13
+ "headline": {
14
+ "type": "node",
15
+ "required": true,
16
+ "description": "The required lead line. `sys.typo.heading.sm` in `sys.color.onSurface`. Names what the surface is for / why it is empty in one short line (e.g. 'No posts yet')."
17
+ },
18
+ "body": {
19
+ "type": "node",
20
+ "optional": true,
21
+ "description": "Optional supporting line below the headline. `sys.typo.body.sm` in `sys.color.onSurfaceVariant`, separated from the headline by `sys.layout.stack.2xs` (4). One sentence โ€” the second of the three lines (e.g. 'Conversations you start or join will appear here')."
22
+ },
23
+ "action": {
24
+ "type": "object",
25
+ "optional": true,
26
+ "description": "{ label, href?, onClick? } โ€” the primary CTA. Renders a default-size primary `Button` (the surface's primary action โ€” the one thing that fills the empty surface). Placed below the body with a `sys.layout.stack.md` (16) gap. There is NO `cta` slot to fill with a custom button; pass the action object so the primary Button is composed for you."
27
+ }
28
+ },
29
+ "slots": {
30
+ "container": {
31
+ "required": true,
32
+ "description": "Centered flex column holding the whole composition. `align-items: center`, `text-align: center`. Ships no surface fill or chrome of its own โ€” it sits inside the host surface that would otherwise hold the data. `role='status'` so assistive tech announces the empty state without yanking focus.",
33
+ "intrinsic": true
34
+ },
35
+ "illustration": {
36
+ "required": false,
37
+ "description": "Optional centered glyph / illustration above the headline. `ref.space.600` (48) box, painted in `sys.color.onSurfaceVariant` (monochrome). `sys.layout.stack.sm` (12) below it to the headline.",
38
+ "accepts": ["icon"]
39
+ },
40
+ "headline": {
41
+ "required": true,
42
+ "description": "Required headline line. `sys.typo.heading.sm` / `sys.color.onSurface`.",
43
+ "accepts": ["text"]
44
+ },
45
+ "body": {
46
+ "required": false,
47
+ "description": "Optional supporting line. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. `sys.layout.stack.2xs` (4) above it from the headline.",
48
+ "accepts": ["text"]
49
+ },
50
+ "action": {
51
+ "required": false,
52
+ "description": "Optional primary CTA. A default-size primary `Button` composed from the `action` object. `sys.layout.stack.md` (16) above it from the body.",
53
+ "accepts": ["button"]
54
+ }
55
+ },
56
+ "sizing": {
57
+ "containerAlign": "center",
58
+ "illustrationSize": "ref.space.600",
59
+ "illustrationColor": "sys.color.onSurfaceVariant",
60
+ "illustrationGap": "sys.layout.stack.sm",
61
+ "headlineTypo": "sys.typo.heading.sm",
62
+ "headlineColor": "sys.color.onSurface",
63
+ "bodyTypo": "sys.typo.body.sm",
64
+ "bodyColor": "sys.color.onSurfaceVariant",
65
+ "bodyGap": "sys.layout.stack.2xs",
66
+ "actionGap": "sys.layout.stack.md"
67
+ },
68
+ "appearance": {
69
+ "illustration": "sys.color.onSurfaceVariant",
70
+ "headline": "sys.color.onSurface",
71
+ "body": "sys.color.onSurfaceVariant",
72
+ "note": "No emphasis axis โ€” EmptyState has one quiet appearance. The illustration and body sit in the muted `onSurfaceVariant` tone; the headline steps up to `onSurface`. The only chromatic emphasis is the CTA, which is a primary `Button` (its own `sys.color.primary` fill) so the single fill-the-surface action reads as primary both in intent and visually."
73
+ },
74
+ "behavior": {
75
+ "centered": "The whole composition is centered (block + inline) inside the host surface that would otherwise hold the data. EmptyState owns no surface fill โ€” the host supplies the surface tier and the bounding box; EmptyState only centers its column inside it.",
76
+ "role": "Container carries `role='status'` so the empty state is announced to assistive tech without grabbing focus.",
77
+ "ctaComposition": "The CTA is not a free slot โ€” pass `action={{ label, href?/onClick? }}` and EmptyState composes a default-size primary Button. This keeps the 'one primary action' rule enforced (one CTA, primary appearance) rather than letting callers drop in an arbitrary control."
78
+ },
79
+ "forbidden": [
80
+ "a no-data surface left blank โ€” every empty surface paints an EmptyState (illustration optional, headline + the one fill action required by the copy rule); the system hard-rule forbids an unaddressed empty surface",
81
+ "a dead-end empty state with no path forward โ€” when the surface has a fill action, omit neither the body's explanation nor the `action` CTA; do not strand the user on a blank wall with only a headline",
82
+ "more than three lines of copy โ€” headline + body must read as at most three lines total (what the surface is for ยท why it is empty ยท the one action that fills it); longer prose belongs in a Banner or a help surface",
83
+ "the CTA rendered as anything but a default-size primary Button โ€” the fill action is the surface's primary action and must read as primary; do not down-rank it to a text or outlined button, and do not render two competing CTAs",
84
+ "EmptyState used as a loading placeholder for data that is still arriving โ€” that is `skeleton` (an in-flight tonal block). EmptyState is the durable no-data surface, not a transient one",
85
+ "the illustration painted in a chromatic tone for non-brand empty states โ€” it stays monochrome `sys.color.onSurfaceVariant` unless it deliberately carries a brand moment"
86
+ ]
87
+ }
@@ -1,5 +1,7 @@
1
1
  # Ad
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/feed/ad.md`](../../../i18n/ko/schema/components/feed/ad.md)
4
+
3
5
  Sub-component of the [Feed](./feed.md) family. A sponsored placement riding the same scrolling column as [Feed ยท Post](./post.md). The header trades a channel/author row for a brand row (32-rung [Thumbnail](../thumbnail/thumbnail.md) + brand name + `Sponsored` subtitle + trailing close affordance); the body stays the same shape as Feed's title + excerpt; hero media and CTA bond into a single rounded slab at the foot. No engagement row โ€” ads are not authored content.
4
6
 
5
7
  **Reach for this when** a sponsored placement must ride inline in the feed column alongside authored posts โ€” a promoted brand post, an in-feed ad unit. **Skip when** the content is authored by a user (use [Feed ยท Post](./post.md)) or the promotion belongs in a dedicated surface outside the feed stream.
@@ -1,5 +1,7 @@
1
1
  # Feed
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/feed/feed.md`](../../../i18n/ko/schema/components/feed/feed.md)
4
+
3
5
  The unit of a scrolling stream. The family covers two card shapes that ride the same column: **[Post](./post.md)** โ€” the authored content card (channel header, title + body, optional thumbnail / poll / offer / citation / mention, engagement footer); **[Ad](./ad.md)** โ€” the sponsored placement (brand row, optional title + body, a hero + CTA slab, no engagement row).
4
6
 
5
7
  Editorial collections of popular posts ride alongside Feed cards via the [Section ยท Post Carousel](../carousel/post.md) sub of the [Carousel](../carousel/carousel.md) family โ€” that pair owns the curated-collection placement, not Feed itself.
@@ -1,5 +1,7 @@
1
1
  # Post
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/feed/post.md`](../../../i18n/ko/schema/components/feed/post.md)
4
+
3
5
  Sub-component of the [Feed](./feed.md) family. The authored-content card โ€” the unit of a scrolling feed. Composes a flag, author row, body block (title + clamped excerpt + optional thumbnail), optional inline modules (poll, offer, citation, mention), and an engagement footer.
4
6
 
5
7
  **Reach for this when** rendering a single user-authored entry โ€” text post, poll, offer evaluation, link share. **Skip when** the placement is sponsored ([Feed ยท Ad](./ad.md)) or the row is a metric summary rather than authored content.
@@ -1,6 +1,8 @@
1
1
  # Form field
2
2
 
3
- The text-entry primitives โ€” controls the user types into or picks a value from. **Input** is single-line text; **Search Bar** is its search-shaped sibling; **Select** is the Input-shaped picker that opens a sheet. Cross-family contract: bordered surface-toned container, hairline rest stroke that thickens while active, `error` appearance re-tones the whole field.
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/form-field/form-field.md`](../../../i18n/ko/schema/components/form-field/form-field.md)
4
+
5
+ The text-entry primitives โ€” controls the user types into or picks a value from. **Input** is single-line text; **Search Bar** is its search-shaped sibling; **Select** is the Input-shaped picker that opens a sheet. Cross-family contract: bordered surface-toned container, hairline rest stroke that thickens while active, `error` appearance re-tones the whole field. The trailing clear ("ร—") is an independent nested action โ€” while the pointer hovers / presses it, the field's own hover stroke and pressed overlay are suppressed so the large field never reads as pressed alongside the small control. (The Select chevron is exempt: it fires the same action as clicking the field, so it lights the field.)
4
6
 
5
7
  **Layout inset.** `inline` โ€” slot atom. No page-rail responsibility; the surrounding container places it. Sits inside a labelled form layout (vertical FormField stack) or another component's slot (NavigationBar centre for Search, BottomSheet body for quick-entry) โ€” never a sibling of `full-bleed` page rows. The form column pays the page gutter, not the field.
6
8
 
@@ -1,5 +1,7 @@
1
1
  # Input
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/form-field/input.md`](../../../i18n/ko/schema/components/form-field/input.md)
4
+
3
5
  Single-line text field โ€” a bordered, transparent-fill box for short values. Optional `label`, `helper`, or `maxLength` compose it into a labeled group; `appearance="error"` re-tones fill, text, and stroke.
4
6
 
5
7
  **Reach for this when** capturing a short single-line value โ€” name, email, search query, comment subject. **Skip when** the value is multi-line (use a textarea), a one-of-many selection (use a select), or a free-form search with built-in results (use the [search](./search.md) sub).
@@ -151,15 +151,23 @@
151
151
  "overlay": {
152
152
  "color": "text",
153
153
  "opacity": "sys.state.pressed"
154
- }
154
+ },
155
+ "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button โ€” that 'ร—' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
155
156
  },
156
157
  "active": {
158
+ "isFocusState": true,
157
159
  "overlay": null,
158
160
  "border": "borderActive",
159
161
  "strokeWeight": "activeStrokeWeight",
160
162
  "caret": "visible",
161
163
  "showsClearWhenValue": true,
162
- "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` โ€” but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
164
+ "focusRing": {
165
+ "composition": "outward",
166
+ "layer": "::after overlay โ€” position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
167
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
168
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
169
+ },
170
+ "note": "This IS the field's keyboard/input-focus state โ€” `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` โ€” but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
163
171
  },
164
172
  "disabled": {
165
173
  "overlay": null,
@@ -1,5 +1,7 @@
1
1
  # Search bar
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/form-field/search.md`](../../../i18n/ko/schema/components/form-field/search.md)
4
+
3
5
  Search-shaped single-line field โ€” sibling of [Input](./input.md) with a leading `SearchIcon` and `sys.radius.full` pill corner. Box, stroke, placeholder rule, clear button, and focus ring inherited from Input unchanged. **Bare box only โ€” no `label`, `helper`, `maxLength`, or `error` appearance.** Error reporting belongs to a labelled Input.
4
6
 
5
7
  **Reach for this when** the rung is a query against an open set โ€” directory search, post filter, command palette entry. **Skip when** the value is a labelled form field ([Input](./input.md)), the user picks from a known closed set ([Select](./select.md)), or the surface needs error reporting (a bare search rung has nowhere to host it โ€” use a labelled [Input](./input.md)).
@@ -92,15 +92,23 @@
92
92
  "overlay": {
93
93
  "color": "text",
94
94
  "opacity": "sys.state.pressed"
95
- }
95
+ },
96
+ "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button โ€” that 'ร—' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
96
97
  },
97
98
  "active": {
99
+ "isFocusState": true,
98
100
  "overlay": null,
99
101
  "border": "borderActive",
100
102
  "strokeWeight": "activeStrokeWeight",
101
103
  "caret": "visible",
102
104
  "showsClearWhenValue": true,
103
- "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` โ€” but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
105
+ "focusRing": {
106
+ "composition": "outward",
107
+ "layer": "::after overlay โ€” position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
108
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
109
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
110
+ },
111
+ "note": "This IS the field's keyboard/input-focus state โ€” `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` โ€” but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
104
112
  },
105
113
  "disabled": {
106
114
  "overlay": null,
@@ -1,5 +1,7 @@
1
1
  # Select
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/form-field/select.md`](../../../i18n/ko/schema/components/form-field/select.md)
4
+
3
5
  Input-shaped picker โ€” same box, label, helper, and error re-tone as [Input](./input.md), but read-only and ending in an `ArrowDownIcon` chevron. Clicking opens a Bottom Sheet with the option list; chosen value is echoed back through `value`.
4
6
 
5
7
  **Reach for this when** the user picks one value from a known set that's too long for inline chips โ€” country, currency, sort order, equity tier. **Skip when** the value is free text ([Input](./input.md)), the user searches an open set ([Search bar](./search.md)), or the list is short enough to surface inline as a [Radio list](../list/radio.md).
@@ -150,9 +150,17 @@
150
150
  }
151
151
  },
152
152
  "active": {
153
+ "isFocusState": true,
153
154
  "overlay": null,
154
155
  "border": "borderActive",
155
- "strokeWeight": "activeStrokeWeight"
156
+ "strokeWeight": "activeStrokeWeight",
157
+ "focusRing": {
158
+ "composition": "outward",
159
+ "layer": "::after overlay โ€” position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
160
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
161
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
162
+ },
163
+ "note": "This IS the trigger's keyboard-focus / open state โ€” `:focus-visible` and the engaged (open) state coincide for the select trigger, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke re-tones to `borderActive` at `activeStrokeWeight` (2px) as an inset box-shadow, pixel-stable (no reflow)."
156
164
  },
157
165
  "disabled": {
158
166
  "overlay": null,
@@ -1,5 +1,7 @@
1
1
  # Textarea
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/form-field/textarea.md`](../../../i18n/ko/schema/components/form-field/textarea.md)
4
+
3
5
  Multi-line cousin of [input](./input.md) โ€” identical chrome contract (transparent fill, inset box-shadow stroke, optional `label` / `helper` / `maxLength` group rungs); the inner element is a `<textarea>` with configurable `rows` (default 4) and vertical-only resize.
4
6
 
5
7
  **Reach for this when** the value naturally spans multiple lines: compose surfaces, bug reports, profile bios, comment composers. **Skip when** the value is single-line ([input](./input.md)), needs a leading magnifier glyph ([search](./search.md)), or opens a sheet-driven option list ([select](./select.md)).
@@ -133,14 +133,22 @@
133
133
  "hovered": { "overlay": null, "border": "borderHover" },
134
134
  "pressed": {
135
135
  "border": "borderHover",
136
- "overlay": { "color": "text", "opacity": "sys.state.pressed" }
136
+ "overlay": { "color": "text", "opacity": "sys.state.pressed" },
137
+ "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button โ€” that 'ร—' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
137
138
  },
138
139
  "active": {
140
+ "isFocusState": true,
139
141
  "overlay": null,
140
142
  "border": "borderActive",
141
143
  "strokeWeight": "activeStrokeWeight",
142
144
  "caret": "visible",
143
- "note": "Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow โ€” same pixel-stable contract as input."
145
+ "focusRing": {
146
+ "composition": "outward",
147
+ "layer": "::after overlay โ€” position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
148
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
149
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
150
+ },
151
+ "note": "This IS the field's keyboard/input-focus state โ€” `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow โ€” same pixel-stable contract as input."
144
152
  },
145
153
  "disabled": {
146
154
  "overlay": null,
@@ -1,5 +1,7 @@
1
1
  # Header
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/header/header.md`](../../../i18n/ko/schema/components/header/header.md)
4
+
3
5
  The labelled header that names a region and exposes its one highest-priority affordance. Two members share this contract and split on a **tone/weight axis**: **[Main](./main.md)** (`<Header>`) โ€” the louder `onSurface` section heading at 16 / 20 that owns the See-all action, the icon drill-in, and the sort/filter dropdown โ€” and **[Sub](./sub.md)** (`<SubHeader>`) โ€” the quieter muted `onSurfaceVariant` group label at 14 that names the rows beneath it and carries at most a single Text Button action.
4
6
 
5
7
  **Layout inset.** `full-bleed` โ€” both members have a transparent background and own their `container.md` (16) inline rail, so the label lands on the same rail as the List rows / Feed items it heads with no host help. Drop the header directly above the labelled region as a sibling; do **not** wrap it in a `padding-inline` / `px-*` / `style={{ padding }}` div, and do not pay the rail again on the host. A bundling host (Carousel, DirectoryList, NavList, SideSheet column) stays full-bleed and absorbs only the header's block padding into its stack rhythm. See [`AGENTS.md` ยง Composition rules](../../../AGENTS.md#composition-rules).
@@ -1,5 +1,7 @@
1
1
  # Main
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/header/main.md`](../../../i18n/ko/schema/components/header/main.md)
4
+
3
5
  The louder member of the [Header](./header.md) family โ€” a labelled `onSurface` heading + an optional trailing affordance, reached as `<Header>`. The composable header anatomy reused across [Carousel](../carousel/carousel.md), in-sheet sub-sections, bounded cards, [SideSheet](../side-sheet/side-sheet.md) drawer columns, and any host that needs a leading title + one trailing commit.
4
6
 
5
7
  **Reach for this when** a labelled block needs an `onSurface` heading and at most one trailing commit (See-all link, drill-in chevron, or sort dropdown). **Skip when** the block needs only a quiet muted group label โ€” that is the family's [Sub](./sub.md) (`<SubHeader>`); when the block has multiple actions; the trailing affordance is a destructive commit; or the surrounding host already owns a heading at the same rung.
@@ -1,5 +1,7 @@
1
1
  # Sub
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/header/sub.md`](../../../i18n/ko/schema/components/header/sub.md)
4
+
3
5
  Quiet section-dividing label โ€” a 14px (`sys.typo.label.md`, 14 / Semibold) line in the muted `sys.color.onSurfaceVariant` tone that names the group of rows beneath it ("Following", "More Topics to follow"). The muted tone is the dividing device: it reads as a region label rather than a heading that competes with the page title, while the label-weight typo keeps it from dissolving into body copy. Rendered as a semantic `<h3>` by default so screen-reader heading navigation lands on the group label.
4
6
 
5
7
  It may carry one quiet trailing affordance โ€” a single [Text Button](../button/text.md) `action` ("See all", "Edit", "Manage") โ€” for a region-level commit that stays subordinate to the muted label. Icon drill-in and dropdown disclosure are *not* SubHeader's: those belong on [Main](./main.md), whose louder `onSurface` heading earns the heavier affordance set.
@@ -1,5 +1,7 @@
1
1
  # Accordion
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/list/accordion.md`](../../../i18n/ko/schema/components/list/accordion.md)
4
+
3
5
  Expandable-row List sub. Each item exposes a List-row trigger (label + auto-rendered trailing chevron that rotates `180ยฐ` on expand) and a body that paints below it when open. Rows tile flush with the family hairline `outlineVariant` divider between them; an extra rule paints between the open trigger and its child row group.
4
6
 
5
7
  **Reach for this when** a list of titled sections is too long to keep open at once โ€” FAQs, T&C sections, expandable filter groups, settings groups with infrequent edits, hierarchical menus (companies โ†’ channels, regions โ†’ cities). **Skip when** bodies are short enough to read inline (use [Carousel](../carousel/carousel.md) per group), the user needs to act on labels (use a [List/standard](./standard.md) drill-in row or [List/radio](./radio.md)), or every item should be visible at once (stack [Carousel](../carousel/carousel.md)s).
@@ -158,6 +158,15 @@
158
158
  "pressed": {
159
159
  "overlay": { "color": "label", "opacity": "sys.state.pressed" }
160
160
  },
161
+ "focused": {
162
+ "focusRing": {
163
+ "composition": "inward",
164
+ "layer": "::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
165
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
166
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
167
+ },
168
+ "note": "Keyboard-focus (:focus-visible) visual โ€” a three-layer inward ring inside the trigger's footprint, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the trigger is in."
169
+ },
161
170
  "disabled": {
162
171
  "containerOpacity": "sys.state.disabled",
163
172
  "pointerEvents": "none"
@@ -1,5 +1,7 @@
1
1
  # Entry
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/list/entry.md`](../../../i18n/ko/schema/components/list/entry.md)
4
+
3
5
  Directory-entry [List](./list.md) sub โ€” an entity row pairing an optional leading [Thumbnail](../thumbnail/thumbnail.md) avatar with an identity group (label, optional inline `count` Badge, optional `secondary` line and `description`). Same click semantics as the [Standard sub](./standard.md); geometry, state overlays, and inward focus ring delegate to the [family-wide rules](./list.md). Slots and Anatomy below carry the rung and slot detail.
4
6
 
5
7
  **Reach for this when** rendering an entity-row directory โ€” follow suggestion, member directory, subscription / channel / topic / playlist directory, mention / recipient picker, entity search result โ€” or, with `thumbnail` omitted, label-only nav rows (settings menu, category index). **Skip when** you need a Feed-specific attribution cluster (use [Metadata](../metadata/metadata.md) for Post / Ad card heads).
@@ -194,6 +194,26 @@
194
194
  "opacity": "sys.state.pressed"
195
195
  }
196
196
  },
197
+ "focused": {
198
+ "overlay": {
199
+ "color": "label",
200
+ "opacity": "sys.state.focus"
201
+ },
202
+ "focusRing": {
203
+ "composition": "inward",
204
+ "layer": "::after/::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
205
+ "innerCounterRing": {
206
+ "width": "sys.borderWidth.hairline",
207
+ "color": "sys.color.focusInset"
208
+ },
209
+ "outerRing": {
210
+ "width": "sys.borderWidth.thin",
211
+ "color": "sys.color.focus"
212
+ }
213
+ },
214
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the row is in; never via plain mouse click."
215
+ },
216
+ "nestedActionScope": "The hover / pressed overlay is suppressed while the pointer sits on the independent trailing action (a favorite / follow / overflow control). The small control owns the state; the large row does NOT also read as hovered / pressed. The visual-state boundary matches the event boundary (the trailing action already stops propagation).",
197
217
  "disabled": {
198
218
  "containerOpacity": "sys.state.disabled",
199
219
  "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.",
@@ -224,7 +244,7 @@
224
244
  "rowClickTarget": "Whole row is clickable when an `onClick` is bound to the item. The thumbnail is never a separate hit target.",
225
245
  "identityGroupFlush": "Label and inline count tile flush on the primary line with `sys.layout.inline.sm` (4) horizontal separation and no vertical gap โ€” they read as one tight identity block.",
226
246
  "descriptionSingleLine": "Description always renders on a single line and truncates with ellipsis. The row never grows to fit longer description copy โ€” overflow truncates against the trailing slot if present.",
227
- "trailingSlotIndependent": "Clicks inside `trailingIcon` stop propagating before they reach the row's `onClick`. Wire a favorite / follow / overflow action there without it committing the row's primary action."
247
+ "trailingSlotIndependent": "Clicks inside `trailingIcon` stop propagating before they reach the row's `onClick`. Wire a favorite / follow / overflow action there without it committing the row's primary action. The slot is also visually independent: while the pointer hovers / presses it, the row's own hover / pressed overlay is suppressed so the large row never reads as active alongside the small control."
228
248
  },
229
249
  "forbidden": [
230
250
  "thumbnail rendered as a raw <img> outside the chorus-thumbnail slot wrapper",
@@ -1,5 +1,7 @@
1
1
  # List
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/list/list.md`](../../../i18n/ko/schema/components/list/list.md)
4
+
3
5
  A vertically-stacked sequence of rows for menus, settings panels, picker sheets, inline option groups, directory entries, and expandable hierarchies. Four sub-components share one anatomy and diverge on the leading slot, the selection contract, and the expand contract.
4
6
 
5
7
  **Reach for this when** the rows are same-kind chrome โ€” settings entries, menu options, single-select picker rows, drill-in navigation, directory rows, expandable section headers. **Skip when** the rows are authored content with author + body + footer (use [Feed](../feed/feed.md)) or a horizontal collection of curated cards (use [Carousel](../carousel/carousel.md)). Pick the sub by the trailing affordance and the leading slot: display / navigation rows โ†’ [Standard](./standard.md) (no `thumbnail` for text-only; pass `thumbnail` for the 40px avatar-leading image type, label + optional `supportingText`); single-select radio โ†’ [Radio](./radio.md); 32 / 40 / 48 avatar leading with identity-group (label + inline `count`) + optional single-line description โ†’ [Entry](./entry.md); drill-in chevron โ†’ any [Standard](./standard.md) / [Radio](./radio.md) row with `nav: true`; expand chevron that rotates on click โ†’ [Accordion](./accordion.md).
@@ -12,7 +14,7 @@ A vertically-stacked sequence of rows for menus, settings panels, picker sheets,
12
14
  - **Row geometry.** 8px block / 16px inline padding (`layout.container.xs` / `layout.container.md`); min-height 48px. Row spacing is **role-based**, not a single flex gap: the **text group โ†’ trailing action** gap is a fixed `layout.inline.md` (8px) in every sub, while the **leading โ†’ text group** gap depends on the leading *type* โ€” `layout.inline.md` (8px) for an icon leading (Radio's indicator), `layout.inline.lg` (12px) for an image leading (a Standard row's `thumbnail` image type / Entry Thumbnail). A label-only Entry row (no `thumbnail`) drops the leading gap to 0. Row grows when `supportingText` is present.
13
15
  - **Label column.** Label: 16px / Regular / `onSurface` (sub-list rows compressed inside an accordion render at 14px / Regular โ€” see [accordion.md](./accordion.md) ยง Nested list; Entry rows promote the label to 14px / Semibold so the inline `count` reads as part of the identity group). SupportingText: 14px / Regular / `onSurfaceVariant`, sits directly under the label with no extra gap โ€” the two lines stack on the label-column's intrinsic line-box rhythm. The Entry sub replaces the second line with a single-line `description` (12px / Regular / `onSurfaceVariant`, separated from the identity group by `ref.space.25` (2) โ€” see [entry.md](./entry.md)). All secondary lines truncate with ellipsis.
14
16
  - **Strong-label opt-in.** Pass `strong={true}` on a row (`<Accordion.Item strong>` on the accordion sub) to promote the label's weight from Regular (`body.*-weight`, 400) to Semibold (`label.*-weight`, 600) at the same size and line-height โ€” `body.md โ†’ label.lg` at the 16 rung, `body.sm โ†’ label.md` at the 14 rung. The row's geometry (height, dividers, slot positions) is unchanged; only the label glyphs gain stroke weight. Reach for it when one row needs to read as the primary entry within a denser scan โ€” the active company in a directory, the canonical answer in an FAQ, the user's own row in a member list. Use sparingly โ€” a stack where every row is strong reads as the default again, defeating the marker.
15
- - **States.** `selected` exists only on Radio. The whole row is the interactive surface.
17
+ - **States.** `selected` exists only on Radio. The whole row is the interactive surface โ€” except where a trailing slot carries an *independent action* (a Follow / mute / favorite button, an overflow control). That action is a nested touch target: it stops click / key propagation so it never commits the row, and while the pointer hovers / presses it the row's own hover / pressed overlay is **suppressed** so the large row never reads as active alongside the small control. The decorative `nav: true` chevron is exempt โ€” it is the row's drill-in affordance, so hovering it still lights the whole row.
16
18
  - **Focus indicator.** Composition: Inward (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)) โ€” inset shadows entirely inside the row's box. Trigger: `:focus-visible`.
17
19
  - **Accessibility.** Every non-Radio sub (Standard / Entry / Accordion) exposes `role="list"`; Radio exposes `role="radiogroup"` with each row `role="radio"` + `aria-checked`. A `nav: true` row's trailing chevron is decorative (`aria-hidden`) โ€” the whole row stays the single interactive target. Keyboard navigation (Arrow โ†‘/โ†“, Home/End) is handled by the container; Radio also commits on Space / Enter.
18
20
 
@@ -1,5 +1,7 @@
1
1
  # Radio
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/list/radio.md`](../../../i18n/ko/schema/components/list/radio.md)
4
+
3
5
  Single-select picker List sub-component. Each row carries a leading 24px (`sys.icon.lg`) radio indicator; clicking commits that row's value via `onChange(value)`. Exactly one row is selected at a time. Row geometry, typography, divider, state overlays, and inward focus ring all delegate to the [family-wide rules](./list.md); this sub documents the Radio-specific leading indicator and selection contract.
4
6
 
5
7
  **Reach for this when** the user picks exactly one value from a short, fully-visible set โ€” sort order, range filter, equity tier. **Skip when** multiple values may be selected (use [`Chip variant="filter"`](../chip/filter.md) or [`Button variant="check"`](../button/check.md)), the set is long enough to demand a sheet-driven picker ([Select](../form-field/select.md)), or the row *only* navigates without selecting (use a [Standard](./standard.md) row with `nav: true`). When a value both selects *and* opens a deeper screen โ€” a major category โ€” keep Radio and add `nav: true` (see [Major category with a second screen](#major-category-with-a-second-screen)).
@@ -142,6 +142,25 @@
142
142
  "opacity": "sys.state.pressed"
143
143
  }
144
144
  },
145
+ "focused": {
146
+ "overlay": {
147
+ "color": "label",
148
+ "opacity": "sys.state.focus"
149
+ },
150
+ "focusRing": {
151
+ "composition": "inward",
152
+ "layer": "::after/::before overlay โ€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
153
+ "innerCounterRing": {
154
+ "width": "sys.borderWidth.hairline",
155
+ "color": "sys.color.focusInset"
156
+ },
157
+ "outerRing": {
158
+ "width": "sys.borderWidth.thin",
159
+ "color": "sys.color.focus"
160
+ }
161
+ },
162
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the row is in; never via plain mouse click."
163
+ },
145
164
  "selected": {
146
165
  "leading": "Filled primary indicator; row foreground stays at onSurface. No fill change on the row itself."
147
166
  },
@@ -1,5 +1,7 @@
1
1
  # Standard
2
2
 
3
+ > ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: [`i18n/ko/schema/components/list/standard.md`](../../../i18n/ko/schema/components/list/standard.md)
4
+
3
5
  The default [List](./list.md) sub โ€” display or navigation rows over the shared List anatomy. The whole row is the click target; there is no selection model. A row is text-only by default, opting into a **leading image** (40px [Thumbnail](../thumbnail/thumbnail.md) via `thumbnail`) or a **leading icon** (`sys.icon.lg` glyph via `icon`). Geometry, typography, divider, state overlays, and inward focus ring delegate to the [family-wide rules](./list.md).
4
6
 
5
7
  **Layout inset.** `full-bleed` โ€” sits as a direct child of the page shell. Each row pays its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap the list in another `padding-inline` / `px-*` / `style={{ padding: โ€ฆ }}` div. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out โ€” see [`AGENTS.md` ยง Composition rules](../../../AGENTS.md#composition-rules).
@@ -216,6 +218,51 @@ import { List } from '@teamblind-chorus/ui';
216
218
  />
217
219
  ```
218
220
 
221
+ ### With an embedded Banner
222
+
223
+ Pass a `banner` on a row and a [Banner](../banner/banner.md) renders **below** the row's text group, `8px` (`sys.layout.stack.xs`) down, spanning the row's full content width (aligned to the same 16px inline inset as the label above it). The row flips from a single line to a vertical stack โ€” text group on top, Banner underneath. Reach for it when a row needs an in-row call-out tied to *that row's* subject (a follow-up prompt, a capability nudge, a single-line CTA), rather than a separate full-width Banner detached from the row.
224
+
225
+ The Banner keeps its full prop surface โ€” here `appearance="accent"` + `neutralBody` for the quiet-tint shape, a blue `CheckCircleFillIcon` leading the single-line body, and a `trailingAction` Text Button with a trailing chevron. The Banner is a nested-action region: its button never commits the row, and the row's hover / pressed overlay is suppressed over it. The rows lead with fill-type category glyphs (`BriefcaseFillIcon`, `FlagFillIcon`) that the leading slot tones to `onSurfaceVariant`.
226
+
227
+ ```preview
228
+ list/standard-embedded-banner
229
+ ---
230
+ import { Banner, Button, List } from '@teamblind-chorus/ui';
231
+ import { BriefcaseFillIcon, CheckCircleFillIcon, ChevronRightIcon, FlagFillIcon } from '@teamblind-chorus/ui/icons';
232
+
233
+ <List
234
+ aria-label="Career"
235
+ items={[
236
+ {
237
+ value: 'major',
238
+ label: 'My major: Computer Science',
239
+ strong: true,
240
+ icon: <BriefcaseFillIcon />,
241
+ banner: (
242
+ <Banner
243
+ appearance="accent"
244
+ neutralBody
245
+ icon={<CheckCircleFillIcon size={16} style={{ color: 'var(--sys-color-primary)' }} />}
246
+ trailingAction={(
247
+ <Button variant="text" appearance="accent" size="small" trailingIcon={<ChevronRightIcon />}>
248
+ Expert Q&A
249
+ </Button>
250
+ )}
251
+ >
252
+ Ask a professional in this field
253
+ </Banner>
254
+ ),
255
+ },
256
+ {
257
+ value: 'roadmap',
258
+ label: 'View career roadmap',
259
+ icon: <FlagFillIcon />,
260
+ nav: true,
261
+ },
262
+ ]}
263
+ />
264
+ ```
265
+
219
266
  ## Slots
220
267
 
221
268
  - **container** โ€” outer vertical stack (delegates to family).
@@ -229,6 +276,7 @@ import { List } from '@teamblind-chorus/ui';
229
276
  - **trailingIcon** *(optional, per-row)* โ€” consumer-supplied node at the trailing edge. Each row decides independently. Canonical fills: a 16px icon (e.g. an external-link mark), or `<Button variant="text" appearance="accent">` (Follow / Invite) on leading-image rows. A status badge does **not** go here โ€” it tiles next to the label via `count`. Its own hit target: a tap on this slot stops propagating before it reaches the row's `onClick`. Overrides the nav chevron on the same row.
230
277
  - **navChevron** *(optional, per-row)* โ€” auto-rendered 16px right-pointing chevron, painted when the row sets `nav: true`. `onSurfaceVariant`, decorative; never a separate hit target.
231
278
  - **divider** *(optional, per-row)* โ€” pass `divider: false` to suppress the row's bottom hairline. Use when a visual group ends mid-stack and the divider would visually fence off the next group from its label.
279
+ - **banner** *(optional, per-row)* โ€” an embedded [Banner](../banner/banner.md) below the row's text group, `8px` (`sys.layout.stack.xs`) down, spanning the row's full content width. The row stacks (text group over Banner). A nested-action region: its own controls never commit the row, and the row's hover / pressed overlay is suppressed over it. Canonical fill: `<Banner appearance="accent" neutralBody icon={โ€ฆ} trailingAction={โ€ฆ}>` โ€” an in-row call-out tied to the row's subject.
232
280
 
233
281
  ## States
234
282