canon-ds 0.1.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/THIRD_PARTY_NOTICES.md +55 -0
  4. package/bin/canon.js +6 -0
  5. package/lib/build-manifest.mjs +54 -0
  6. package/lib/build.mjs +69 -0
  7. package/lib/cli.mjs +273 -0
  8. package/lib/color.js +160 -0
  9. package/lib/components/_shared.mjs +93 -0
  10. package/lib/components/accordion.mjs +136 -0
  11. package/lib/components/account-card.mjs +166 -0
  12. package/lib/components/activity-feed.mjs +176 -0
  13. package/lib/components/activity-gauge.mjs +98 -0
  14. package/lib/components/agent-presence.mjs +156 -0
  15. package/lib/components/alert.mjs +154 -0
  16. package/lib/components/app-store-button.mjs +108 -0
  17. package/lib/components/avatar-group.mjs +128 -0
  18. package/lib/components/avatar.mjs +225 -0
  19. package/lib/components/badge-group.mjs +55 -0
  20. package/lib/components/badge.mjs +174 -0
  21. package/lib/components/banner.mjs +116 -0
  22. package/lib/components/breadcrumb.mjs +170 -0
  23. package/lib/components/button-group.mjs +114 -0
  24. package/lib/components/button.mjs +197 -0
  25. package/lib/components/card-header.mjs +115 -0
  26. package/lib/components/card.mjs +106 -0
  27. package/lib/components/carousel.mjs +182 -0
  28. package/lib/components/chart-frame.mjs +101 -0
  29. package/lib/components/checkbox.mjs +163 -0
  30. package/lib/components/close-button.mjs +49 -0
  31. package/lib/components/code.mjs +123 -0
  32. package/lib/components/color-picker.mjs +122 -0
  33. package/lib/components/combobox.mjs +210 -0
  34. package/lib/components/command-palette.mjs +226 -0
  35. package/lib/components/content-divider.mjs +104 -0
  36. package/lib/components/counter.mjs +86 -0
  37. package/lib/components/credit-card.mjs +126 -0
  38. package/lib/components/date-picker.mjs +202 -0
  39. package/lib/components/description-list.mjs +88 -0
  40. package/lib/components/dialog.mjs +140 -0
  41. package/lib/components/divider.mjs +115 -0
  42. package/lib/components/drawer.mjs +150 -0
  43. package/lib/components/empty-state.mjs +112 -0
  44. package/lib/components/featured-card.mjs +124 -0
  45. package/lib/components/featured-icon.mjs +67 -0
  46. package/lib/components/field.mjs +122 -0
  47. package/lib/components/file-dropzone.mjs +166 -0
  48. package/lib/components/filter-bar.mjs +179 -0
  49. package/lib/components/header-navigation.mjs +94 -0
  50. package/lib/components/icon-button.mjs +179 -0
  51. package/lib/components/index.mjs +52 -0
  52. package/lib/components/inline-cta.mjs +139 -0
  53. package/lib/components/input-group.mjs +164 -0
  54. package/lib/components/input.mjs +197 -0
  55. package/lib/components/kanban.mjs +179 -0
  56. package/lib/components/kbd.mjs +70 -0
  57. package/lib/components/kicker.mjs +79 -0
  58. package/lib/components/link.mjs +108 -0
  59. package/lib/components/list.mjs +117 -0
  60. package/lib/components/media-frame.mjs +119 -0
  61. package/lib/components/menu.mjs +190 -0
  62. package/lib/components/message.mjs +249 -0
  63. package/lib/components/metric-group.mjs +210 -0
  64. package/lib/components/mobile-header.mjs +173 -0
  65. package/lib/components/multi-select.mjs +218 -0
  66. package/lib/components/notification.mjs +175 -0
  67. package/lib/components/number-input.mjs +149 -0
  68. package/lib/components/page-header.mjs +68 -0
  69. package/lib/components/pagination.mjs +236 -0
  70. package/lib/components/pin-input.mjs +120 -0
  71. package/lib/components/popover.mjs +126 -0
  72. package/lib/components/progress-circle.mjs +115 -0
  73. package/lib/components/progress.mjs +93 -0
  74. package/lib/components/prose.mjs +96 -0
  75. package/lib/components/radio.mjs +160 -0
  76. package/lib/components/rating.mjs +93 -0
  77. package/lib/components/rich-text-editor.mjs +178 -0
  78. package/lib/components/section-header.mjs +62 -0
  79. package/lib/components/segmented-control.mjs +121 -0
  80. package/lib/components/select.mjs +163 -0
  81. package/lib/components/sidebar-nav.mjs +172 -0
  82. package/lib/components/sidebar.mjs +124 -0
  83. package/lib/components/skeleton.mjs +66 -0
  84. package/lib/components/slider.mjs +122 -0
  85. package/lib/components/social-button.mjs +119 -0
  86. package/lib/components/spinner.mjs +69 -0
  87. package/lib/components/stat.mjs +120 -0
  88. package/lib/components/stepper.mjs +228 -0
  89. package/lib/components/switch.mjs +159 -0
  90. package/lib/components/table.mjs +212 -0
  91. package/lib/components/tabs.mjs +207 -0
  92. package/lib/components/tag.mjs +196 -0
  93. package/lib/components/tags-input.mjs +146 -0
  94. package/lib/components/textarea.mjs +117 -0
  95. package/lib/components/timeline.mjs +135 -0
  96. package/lib/components/toast.mjs +115 -0
  97. package/lib/components/tooltip.mjs +86 -0
  98. package/lib/components/topbar.mjs +207 -0
  99. package/lib/components/tree-view.mjs +156 -0
  100. package/lib/components/video-player.mjs +189 -0
  101. package/lib/connect.mjs +110 -0
  102. package/lib/design-files.mjs +175 -0
  103. package/lib/distribution.mjs +109 -0
  104. package/lib/editor.js +940 -0
  105. package/lib/engine.js +339 -0
  106. package/lib/generators/agents.mjs +68 -0
  107. package/lib/generators/connection.md +58 -0
  108. package/lib/generators/css.mjs +2 -0
  109. package/lib/generators/designmd.mjs +324 -0
  110. package/lib/generators/documentation.css +82 -0
  111. package/lib/generators/documentation.html +81 -0
  112. package/lib/generators/documentation.js +96 -0
  113. package/lib/generators/documentation.mjs +35 -0
  114. package/lib/generators/dtcg.mjs +60 -0
  115. package/lib/generators/preview.css +279 -0
  116. package/lib/generators/preview.mjs +225 -0
  117. package/lib/generators/react.mjs +162 -0
  118. package/lib/generators/tailwind.mjs +114 -0
  119. package/lib/install.mjs +108 -0
  120. package/lib/lint.mjs +538 -0
  121. package/lib/mcp.mjs +268 -0
  122. package/lib/open.mjs +7 -0
  123. package/lib/patterns/_app.mjs +56 -0
  124. package/lib/patterns/app-shell.mjs +25 -0
  125. package/lib/patterns/audit-log.mjs +37 -0
  126. package/lib/patterns/auth-pages.mjs +44 -0
  127. package/lib/patterns/billing-page.mjs +39 -0
  128. package/lib/patterns/calendar-page.mjs +55 -0
  129. package/lib/patterns/dashboard-page.mjs +28 -0
  130. package/lib/patterns/files-page.mjs +30 -0
  131. package/lib/patterns/first-run.mjs +31 -0
  132. package/lib/patterns/form-layout.mjs +24 -0
  133. package/lib/patterns/inbox-page.mjs +58 -0
  134. package/lib/patterns/index.mjs +29 -0
  135. package/lib/patterns/list-detail-page.mjs +24 -0
  136. package/lib/patterns/mobile-app.mjs +46 -0
  137. package/lib/patterns/modal-flows.mjs +32 -0
  138. package/lib/patterns/onboarding-page.mjs +42 -0
  139. package/lib/patterns/permissions-page.mjs +38 -0
  140. package/lib/patterns/profile-page.mjs +32 -0
  141. package/lib/patterns/settings-page.mjs +24 -0
  142. package/lib/project.mjs +28 -0
  143. package/lib/serve.mjs +279 -0
  144. package/lib/system.mjs +210 -0
  145. package/lib/tokens/base.js +251 -0
  146. package/lib/tokens/canon-preset.mjs +67 -0
  147. package/lib/tokens/index.mjs +1 -0
  148. package/lib/tokens/legacy-canon.json +559 -0
  149. package/lib/tokens/presets.mjs +161 -0
  150. package/lib/tokens/resolve.mjs +1 -0
  151. package/lib/types.mjs +4 -0
  152. package/lib/version.mjs +2 -0
  153. package/package.json +54 -0
@@ -0,0 +1,179 @@
1
+ import { TRANSITION_ALL, ex, typeStyle } from "./_shared.mjs";
2
+ // a board in the reference's card language: transparent columns with a 14px semibold title, a
3
+ // 22px count badge and an owner badge; cards as small white cards (radius 12, gray-200 ring,
4
+ // shadow-xs, 16px padding) with a 14px medium title, a muted meta line and a status + avatar
5
+ // footer; gray-50 drop zones.
6
+ const HAIRLINE = '{border.width.thin} solid {color.border-default}';
7
+ const ring = (color) => `inset 0 0 0 1px ${color}`;
8
+ const badge = (text, tone, dot = false) => `<span class="cn-badge" data-tone="${tone}" data-variant="soft" data-size="sm">${dot ? '<span class="cn-badge__dot"></span>' : ''}${text}</span>`;
9
+ const avatar = (t, name) => `<span class="cn-avatar" data-size="xs" data-shape="circle" data-tone="neutral" data-status="none" role="img" aria-label="${name}"><span class="cn-avatar__fallback" aria-hidden="true">${t}</span></span>`;
10
+ const card = (title, meta, status, owner, extra = '') => `<article class="cn-kanban__card" tabindex="0"${extra}><h4 class="cn-kanban__card-title">${title}</h4><div class="cn-kanban__card-meta">${meta}</div><div class="cn-kanban__card-footer">${status}${avatar(owner[0], owner[1])}</div></article>`;
11
+ const column = (owner, title, ownerLabel, cards, bodyExtra = '') => `<section class="cn-kanban__column" data-owner="${owner}" aria-label="${title}, ${cards.length} tasks"><header class="cn-kanban__column-header"><h3 class="cn-kanban__column-title">${title}</h3><span class="cn-kanban__column-count">${cards.length}</span><span class="cn-kanban__column-owner">${ownerLabel}</span></header><div class="cn-kanban__column-body"${bodyExtra}>${cards.join('')}</div></section>`;
12
+ const MAYA = ['MC', 'Maya Chen'];
13
+ const DANIEL = ['DC', 'Daniel Costa'];
14
+ const SOFIA = ['SA', 'Sofia Almeida'];
15
+ const LUCAS = ['LF', 'Lucas Ferreira'];
16
+ const BOARD = (density, dragging = false) => `<div class="cn-kanban" data-density="${density}" role="list" aria-label="Q3 launch board">${column('agent', 'In progress', 'Engineering', [
17
+ card('Migrate billing to usage-based plans', 'LUM-241 · Due Sep 19', badge('In progress', 'accent', true), DANIEL),
18
+ card('Design the new onboarding checklist', 'LUM-233 · Due Sep 15', badge('In review', 'accent', true), SOFIA, dragging ? ' data-state="dragging"' : ''),
19
+ card('Rate-limit the public API', 'LUM-228 · Due Sep 22', badge('In progress', 'accent', true), LUCAS),
20
+ ])}${column('you', 'Waiting on you', 'Product', [
21
+ card('Approve pricing page copy', 'LUM-250 · Due today', badge('Needs approval', 'warning'), MAYA),
22
+ card('Review Q3 roadmap', 'LUM-247 · Due Sep 12', badge('Review', 'neutral'), MAYA),
23
+ ], dragging ? ' data-over' : '')}${column('action', 'Blocked', 'Needs action', [
24
+ card('SSO login fails for Okta tenants', 'LUM-252 · Reported 2h ago', badge('Blocked', 'danger'), DANIEL),
25
+ card('Invoice emails delayed', 'LUM-249 · Reported yesterday', badge('Overdue', 'warning'), LUCAS),
26
+ ])}</div>`;
27
+ export const kanban = {
28
+ name: 'Kanban',
29
+ slug: 'kanban',
30
+ category: 'data-display',
31
+ description: 'A board of columns with draggable cards. Columns are transparent with a header that names the stage, its count badge and who owns it; cards are small white cards (radius 12, gray-200 ring, shadow-xs) with a 14px medium title, a muted meta line and a footer of status Badge + Avatar.',
32
+ usage: 'Work that moves through stages and is owned by someone at each stage: tasks, deals, tickets. When the stage does not matter, use a List; when people compare many fields, use a Table.',
33
+ anatomy: [
34
+ { part: 'root', element: 'div', description: 'Horizontal row of columns, 24px apart, scrolls sideways, columns aligned to the top.' },
35
+ { part: 'column', element: 'section', description: 'One stage: 320px wide (288 compact), transparent. data-owner="agent|you|action" colors the owner badge.' },
36
+ { part: 'column-header', element: 'header', description: 'Row with title, count and owner badge, 8px apart, 36px tall.' },
37
+ { part: 'column-title', element: 'h3', description: 'Stage name, 14px semibold in ink.' },
38
+ { part: 'column-count', element: 'span', description: 'Number of cards in a 22px gray badge (12px medium, gray-50, gray-200 ring).' },
39
+ { part: 'column-owner', element: 'span', description: 'Owner badge pushed to the right (12px medium, radius 6): "Engineering", "Product", "Needs action". Color follows data-owner on the column.' },
40
+ { part: 'column-body', element: 'div', description: 'Drop zone holding the cards 12px apart, min height 128px, radius 12. data-over shows a dashed gray-300 outline on gray-50 while a card hovers over it.' },
41
+ { part: 'card', element: 'article', description: 'One item: white, gray-200 ring, shadow-xs, radius 12, 16px padding, grab cursor, focusable. data-state="dragging" lifts it.' },
42
+ { part: 'card-title', element: 'h4', description: 'The thing\'s name, 14px medium in ink, up to two lines.' },
43
+ { part: 'card-meta', element: 'div', description: 'One line of context, 14px fg-muted ("LUM-241 · Due Sep 19").' },
44
+ { part: 'card-footer', element: 'div', description: 'Status (Badge sm) on the left, owner (Avatar xs) on the right, 8px above.' },
45
+ ],
46
+ props: {
47
+ density: {
48
+ values: ['compact', 'default'],
49
+ default: 'default',
50
+ description: 'default = 320px columns, 16px card padding, 12px between cards (boards with up to ~8 cards per column); compact = 288px columns, 12px card padding, 8px between cards, for boards with many cards.',
51
+ },
52
+ },
53
+ states: {
54
+ cardHover: { selector: ' > * > * > article:hover', description: 'On a card, not the root: the ring darkens to gray-300.', markup: 'native :hover on the card' },
55
+ cardFocus: { selector: ' > * > * > article:focus-visible', description: 'On a card: the 4px brand focus ring (cards are focusable for keyboard moves).', markup: 'tabindex="0" on the card; native :focus-visible' },
56
+ cardDragging: { selector: ' > * > * > [data-state="dragging"]', description: 'On a card while it is being dragged: shadow-lg, 1° tilt, grabbing cursor.', markup: 'data-state="dragging" on the card' },
57
+ columnOver: { selector: ' > * > [data-over]', description: 'On a column body while a card is dragged over it: dashed gray-300 border on a gray-50 fill.', markup: 'data-over on the column body' },
58
+ },
59
+ base: {
60
+ root: {
61
+ display: 'flex',
62
+ 'align-items': 'flex-start',
63
+ gap: '{space.6}',
64
+ width: '100%',
65
+ 'min-width': '0',
66
+ 'overflow-x': 'auto',
67
+ 'padding-bottom': '{space.2}',
68
+ color: '{color.fg-default}',
69
+ },
70
+ column: {
71
+ display: 'flex',
72
+ 'flex-direction': 'column',
73
+ gap: '{space.3}',
74
+ flex: '0 0 calc({space.64} + {space.16})',
75
+ 'min-width': '0',
76
+ 'background-color': 'transparent',
77
+ },
78
+ 'column-header': {
79
+ display: 'flex',
80
+ 'align-items': 'center',
81
+ gap: '{space.2}',
82
+ padding: '0 {space.1}',
83
+ 'min-height': '{space.9}',
84
+ },
85
+ 'column-title': { ...typeStyle('label-md'), color: '{color.fg-default}', margin: '0' },
86
+ 'column-count': {
87
+ display: 'inline-flex',
88
+ 'align-items': 'center',
89
+ 'justify-content': 'center',
90
+ height: '22px',
91
+ 'min-width': '22px',
92
+ 'padding-inline': '{space.2}',
93
+ 'border-radius': '{radius.full}',
94
+ 'background-color': '{color.bg-subtle}',
95
+ 'box-shadow': ring('{color.border-default}'),
96
+ ...typeStyle('label-xs'),
97
+ color: '{color.fg-muted}',
98
+ 'font-variant-numeric': 'tabular-nums',
99
+ },
100
+ 'column-owner': {
101
+ ...typeStyle('label-xs'),
102
+ 'margin-inline-start': 'auto',
103
+ height: '22px',
104
+ display: 'inline-flex',
105
+ 'align-items': 'center',
106
+ padding: '0 {space.2}',
107
+ 'border-radius': '{radius.md}',
108
+ 'background-color': '{color.bg-subtle}',
109
+ 'box-shadow': ring('{color.border-default}'),
110
+ color: '{color.fg-muted}',
111
+ 'white-space': 'nowrap',
112
+ },
113
+ 'column-body': {
114
+ display: 'flex',
115
+ 'flex-direction': 'column',
116
+ gap: '{space.3}',
117
+ 'min-height': '{space.32}',
118
+ 'border-radius': '{radius.card}',
119
+ border: '{border.width.thin} dashed transparent',
120
+ ...TRANSITION_ALL,
121
+ },
122
+ card: {
123
+ display: 'flex',
124
+ 'flex-direction': 'column',
125
+ gap: '{space.1}',
126
+ padding: '{space.4}',
127
+ 'background-color': '{color.bg-surface}',
128
+ 'box-shadow': `${ring('{color.border-default}')}, {shadow.xs}`,
129
+ 'border-radius': '{radius.card}',
130
+ cursor: 'grab',
131
+ 'min-width': '0',
132
+ ...TRANSITION_ALL,
133
+ },
134
+ 'card-title': { ...typeStyle('label-sm'), color: '{color.fg-default}', margin: '0', 'overflow-wrap': 'anywhere' },
135
+ 'card-meta': { display: 'flex', 'align-items': 'center', 'flex-wrap': 'wrap', gap: '{space.2}', ...typeStyle('body-md'), color: '{color.fg-muted}' },
136
+ 'card-footer': { display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', gap: '{space.2}', 'margin-top': '{space.2}' },
137
+ },
138
+ variants: {
139
+ density: {
140
+ compact: {
141
+ column: { flex: '0 0 calc({space.64} + {space.8})', gap: '{space.2}' },
142
+ 'column-body': { gap: '{space.2}' },
143
+ card: { padding: '{space.3}', gap: '{space.0.5}' },
144
+ 'card-footer': { 'margin-top': '{space.1.5}' },
145
+ },
146
+ default: { card: { padding: '{space.4}' } },
147
+ },
148
+ },
149
+ extraCss: `
150
+ .cn-kanban__column[data-owner="agent"] .cn-kanban__column-owner { background-color: {brand.50}; box-shadow: ${ring('{brand.200}')}; color: {brand.700}; }
151
+ .cn-kanban__column[data-owner="you"] .cn-kanban__column-owner { background-color: {color.bg-subtle}; box-shadow: ${ring('{color.border-default}')}; color: {color.fg-default}; }
152
+ .cn-kanban__column[data-owner="action"] .cn-kanban__column-owner { background-color: {amber.50}; box-shadow: ${ring('{amber.200}')}; color: {amber.700}; }
153
+ .cn-kanban__column-body[data-over] { border-color: {color.border-control}; background-color: {color.bg-subtle}; }
154
+ .cn-kanban__card:hover { box-shadow: ${ring('{color.border-control}')}, {shadow.xs}; }
155
+ .cn-kanban__card:focus-visible { outline: none; box-shadow: ${ring('{color.border-control}')}, {shadow.focus}; }
156
+ .cn-kanban__card[data-state="dragging"] { box-shadow: ${ring('{color.border-control}')}, {shadow.lg}; transform: rotate(1deg); opacity: 0.9; cursor: grabbing; }
157
+ .cn-kanban__card:active { cursor: grabbing; }`,
158
+ examples: [
159
+ ex('Board: in progress, waiting on you, blocked', BOARD('default'), 'Each column names its owner in a badge and shows its count. Cards carry a status Badge and the person responsible.'),
160
+ ex('Compact, mid-drag', BOARD('compact', true), 'The second card is being dragged; the "Waiting on you" column shows the drop zone.'),
161
+ ],
162
+ rules: [
163
+ 'Every column names an owner with data-owner: agent (a team is working on it), you (waiting for the user) or action (something went wrong or is overdue). The owner badge is the reader\'s first question answered.',
164
+ 'Column titles are stages or owners, 1–3 words; the count badge is always shown, even when zero.',
165
+ 'Cards show exactly: title (14px medium), one meta line, one status Badge sm and one Avatar xs. No descriptions, no buttons, no menus on the card; open the record for details.',
166
+ 'Card height comes from content only; do not force equal heights or add images.',
167
+ 'Columns are 320px (288px compact) and never stretch; the board scrolls sideways. Keep it to 3–6 columns.',
168
+ 'Drop feedback is the dashed column body (data-over) and the lifted card (data-state="dragging"); do not add insertion lines or ghost cards.',
169
+ 'Empty columns keep their min-height so there is somewhere to drop; show "Nothing here" in fg-subtle 14px when a column is empty for a while.',
170
+ 'Status tone follows the owner: brand (with dot) while a team works, warning when the user must act, danger for failures, neutral otherwise.',
171
+ ],
172
+ a11y: [
173
+ 'The board is role="list" of columns; each column is a <section> with an aria-label that includes its title and count; each card is an <article> with a heading.',
174
+ 'Cards are focusable (tabindex="0"). Provide a keyboard way to move a card (a "Move to…" action in the record or a menu on Enter); drag and drop is never the only way.',
175
+ 'Announce moves with a polite live region ("Approve pricing page copy moved to In progress").',
176
+ 'Owner and status are text (the owner badge, the status badge), never only a color; the dot is reinforcement.',
177
+ ],
178
+ related: ['badge', 'avatar', 'card', 'list', 'counter'],
179
+ };
@@ -0,0 +1,70 @@
1
+ import { ex, typeStyle } from "./_shared.mjs";
2
+ // the reference shortcut chip: 12px medium sans text in gray-600 on white, a 1px gray-300 inset
3
+ // ring, radius 4, padding 1px 4px, at least 20×20. Flat — no keycap edge, no mono.
4
+ const ring = (color) => `inset 0 0 0 1px ${color}`;
5
+ const k = (key, size = 'md') => `<kbd class="cn-kbd" data-size="${size}">${key}</kbd>`;
6
+ export const kbd = {
7
+ name: 'Kbd',
8
+ slug: 'kbd',
9
+ category: 'typography',
10
+ description: 'A keyboard key drawn as the reference shortcut chip: 12px medium text on white with a 1px gray-300 ring, radius 4, 20px tall. Several in a row spell a shortcut (⌘ K).',
11
+ usage: 'Show keyboard shortcuts in menus, tooltips, the command palette, input trailers and onboarding hints. Not for code (Code) and never as a button: a Kbd is not clickable.',
12
+ anatomy: [
13
+ { part: 'root', element: 'kbd', description: 'One key: a symbol (⌘ ⇧ ⌥) or a short word (Esc, Enter, K). One Kbd per key; put them side by side for a chord.' },
14
+ ],
15
+ props: {
16
+ size: { values: ['sm', 'md'], default: 'md', description: 'md = 20px tall with 12px text, for menus, inputs, docs and the command palette (the reference chip); sm = 18px tall with 11px text inside tooltips and table cells.' },
17
+ },
18
+ states: {},
19
+ base: {
20
+ root: {
21
+ display: 'inline-flex',
22
+ 'align-items': 'center',
23
+ 'justify-content': 'center',
24
+ 'box-sizing': 'border-box',
25
+ 'min-width': '{space.5}',
26
+ height: '{space.5}',
27
+ padding: '{space.px} {space.1}',
28
+ ...typeStyle('label-xs'),
29
+ color: '{color.fg-muted}',
30
+ 'background-color': '{color.bg-surface}',
31
+ 'box-shadow': ring('{color.border-control}'),
32
+ 'border-radius': '{radius.sm}',
33
+ 'vertical-align': 'middle',
34
+ 'white-space': 'nowrap',
35
+ 'user-select': 'none',
36
+ },
37
+ },
38
+ variants: {
39
+ size: {
40
+ sm: { root: { height: '18px', 'min-width': '18px', padding: '{space.px} 3px', 'font-size': '{font.size.2xs}' } },
41
+ md: { root: {} },
42
+ },
43
+ },
44
+ extraCss: `
45
+ .cn-kbd + .cn-kbd { margin-inline-start: {space.1}; }`,
46
+ examples: [
47
+ ex('Single key', k('⌘'), '20×20 minimum, 12px medium gray-600, 1px gray-300 ring, radius 4.'),
48
+ ex('Chord', `${k('⌘')}${k('K')}`, 'Modifiers first, then the key, 4px between them.'),
49
+ ex('Named keys', `<span style="display:inline-flex;gap:var(--cn-space-4);align-items:center">${k('Esc')}<span>${k('⇧')}${k('⌘')}${k('P')}</span>${k('Enter')}${k('Tab')}</span>`, 'Words stretch the chip; the padding stays 1px 4px.'),
50
+ ex('In a sentence', `<p class="cn-text-body-md" style="max-width:560px">Press ${k('⌘')}${k('K')} to open the command palette, ${k('/')} to search projects, or ${k('Esc')} to close any panel.</p>`),
51
+ ex('Small in a hint', `<span class="cn-text-body-sm" style="color:var(--cn-color-fg-muted)">Save ${k('⌘', 'sm')}${k('S', 'sm')}</span>`, '18px tall with 11px text for tooltips and table cells.'),
52
+ ],
53
+ recipes: [
54
+ ex('Menu item with shortcut', `<div style="width:240px;display:flex;flex-direction:column;gap:var(--cn-space-0-5)">${[['Duplicate', '⌘', 'D'], ['Archive', '⌘', 'E'], ['Delete', '⌘', '⌫']].map(([label, m, key], i) => `<div class="cn-text-body-md" style="display:flex;align-items:center;justify-content:space-between;gap:var(--cn-space-6);height:36px;padding:0 var(--cn-space-2-5);border-radius:var(--cn-radius-md);${i === 0 ? 'background:var(--cn-color-bg-subtle)' : ''}"><span>${label}</span><span>${k(m, 'sm')}${k(key, 'sm')}</span></div>`).join('')}</div>`, 'Shortcut right-aligned, size sm; the label stays the primary text.'),
55
+ ],
56
+ rules: [
57
+ 'One key per Kbd. A shortcut is a row of Kbds 4px apart, in the order pressed: modifiers first (⌃ ⌥ ⇧ ⌘), then the key.',
58
+ 'Use platform symbols on macOS (⌘ ⌥ ⇧ ⌃ ⏎ ⌫ ⎋) and words on Windows/Linux (Ctrl, Alt, Shift, Enter). Detect the platform; never show both.',
59
+ 'Letters are uppercase (K, not k); named keys are capitalized words (Esc, Tab, Space, Enter), never abbreviations like "Ret".',
60
+ 'Kbd sits at the end of a menu item, tooltip, palette row or input, right-aligned. It never appears inside a button label.',
61
+ 'Never use Kbd for pointer input (clicks, gestures) or for code; that is Code.',
62
+ 'md by default; sm where the surrounding text is 12px (tooltips, table cells, dense menu rows).',
63
+ ],
64
+ a11y: [
65
+ 'Use the <kbd> element; screen readers read its text, so add aria-label on symbol keys ("Command", "Shift", "Escape").',
66
+ 'A shortcut hint is supplementary: the same action must be reachable by pointer and through the menu.',
67
+ 'Keep gray-600 on white; at 11–12px medium it meets AA. Do not lighten Kbd text to gray-500.',
68
+ ],
69
+ related: ['code', 'tooltip', 'menu', 'command-palette', 'input'],
70
+ };
@@ -0,0 +1,79 @@
1
+ import { ex, typeStyle } from "./_shared.mjs";
2
+ const MARK = {
3
+ neutral: '{color.fg-subtle}',
4
+ accent: '{color.bg-accent}',
5
+ success: '{color.bg-success}',
6
+ warning: '{color.bg-warning}',
7
+ danger: '{color.bg-danger}',
8
+ };
9
+ export const kicker = {
10
+ name: 'Kicker',
11
+ slug: 'kicker',
12
+ category: 'typography',
13
+ description: 'The signature eyebrow: mono, uppercase, letter-spaced, muted, with an optional 6×6 brand square before the text. It sits above a title and names the type of thing the title is.',
14
+ usage: 'Above a page title, a card title or a section title ("Prospect", "Step 2 of 4", "Agent report"). Not for status (Badge), not for counts (Counter), not for form labels (Field). Never clickable: if it filters it is a Tag, if it acts it is a Button.',
15
+ anatomy: [
16
+ { part: 'root', element: 'div', description: 'Inline-flex row carrying the kicker type style and the muted color. <div> above block titles, <span> when inline.' },
17
+ { part: 'mark', element: 'span', description: 'Optional 6×6px square before the text, radius 1.5px, filled with the tone color (brand accent by default). Empty, decorative.', optional: true },
18
+ ],
19
+ props: {
20
+ mark: { values: ['none', 'square', 'dot'], default: 'none', description: 'none = plain text (section subheading); square = 6px brand square before the text (Vero family signature); dot = round marker.' },
21
+ tone: {
22
+ values: ['neutral', 'accent', 'success', 'warning', 'danger'],
23
+ default: 'accent',
24
+ description: 'Color of the mark only; the text always stays fg-subtle. accent = the brand square (default: page and card titles); neutral = grey square for secondary sections; success/warning/danger = the section is about a state ("Verified", "Needs review", "Blocked").',
25
+ },
26
+ size: {
27
+ values: ['sm', 'md'],
28
+ default: 'sm',
29
+ description: 'sm = 11px, the canonical kicker above card titles and inside tables (equals the kicker type style, so markup without data-size renders sm); md = 12px for page-level eyebrows and marketing sections.',
30
+ },
31
+ },
32
+ states: {},
33
+ base: {
34
+ root: {
35
+ display: 'inline-flex',
36
+ 'align-items': 'center',
37
+ gap: '{space.2}',
38
+ ...typeStyle('kicker'),
39
+ 'text-transform': 'uppercase',
40
+ color: '{color.fg-subtle}',
41
+ 'max-width': '100%',
42
+ },
43
+ mark: { width: '6px', height: '6px', 'border-radius': '1.5px', 'background-color': '{color.bg-accent}', 'flex-shrink': '0' },
44
+ },
45
+ variants: {
46
+ mark: { none: { mark: { display: 'none' } }, square: { mark: { display: 'inline-block' } }, dot: { mark: { display: 'inline-block', 'border-radius': '{radius.full}' } } },
47
+ tone: Object.fromEntries(Object.entries(MARK).map(([t, c]) => [t, { mark: { 'background-color': c } }])),
48
+ size: {
49
+ sm: { root: { 'font-size': '{font.size.2xs}' } },
50
+ md: { root: { 'font-size': '{font.size.xs}', gap: '{space.2.5}' }, mark: { width: '7px', height: '7px' } },
51
+ },
52
+ },
53
+ examples: [
54
+ ex('With mark', `<div class="cn-kicker" data-tone="accent" data-size="sm"><span class="cn-kicker__mark"></span>Prospect</div>`),
55
+ ex('Text only', `<div class="cn-kicker" data-tone="accent" data-size="sm">Step 2 of 4 · Sourcing</div>`, 'No mark for secondary eyebrows and inline uses.'),
56
+ ex('Tones', `<div class="cn-kicker" data-tone="accent" data-size="sm"><span class="cn-kicker__mark"></span>Prospect</div> <div class="cn-kicker" data-tone="neutral" data-size="sm"><span class="cn-kicker__mark"></span>Archive</div> <div class="cn-kicker" data-tone="success" data-size="sm"><span class="cn-kicker__mark"></span>Verified</div> <div class="cn-kicker" data-tone="warning" data-size="sm"><span class="cn-kicker__mark"></span>Needs review</div> <div class="cn-kicker" data-tone="danger" data-size="sm"><span class="cn-kicker__mark"></span>Blocked</div>`, 'Only the mark changes color. The text is always muted.'),
57
+ ex('Page-level (md)', `<div class="cn-kicker" data-tone="accent" data-size="md"><span class="cn-kicker__mark"></span>Agent report</div>`),
58
+ ],
59
+ recipes: [
60
+ ex('Above a title', `<div><div class="cn-kicker" data-tone="accent" data-size="sm"><span class="cn-kicker__mark"></span>Prospect</div><h2 class="cn-text-heading-lg" style="margin-top:var(--cn-space-2)">Nakamura Trading Co.</h2><p class="cn-text-body-sm" style="color:var(--cn-color-fg-muted);margin-top:var(--cn-space-1)">Osaka, Japan · Importer of frozen seafood</p></div>`, 'Kicker names the type, the title names the thing. space.2 between them.'),
61
+ ],
62
+ rules: [
63
+ 'One kicker per title, directly above it, with space.2 between them. Never two kickers stacked.',
64
+ 'The kicker names the TYPE of thing; the title names the thing: "Prospect / Nakamura Trading Co.", never "Nakamura / Prospect".',
65
+ '1–3 words. Write the source in sentence case ("Needs review"); the CSS uppercases it.',
66
+ 'The text is always fg-subtle. Never color the text; tone colors only the mark.',
67
+ 'Never a badge: no background, no border, no pill. Never clickable.',
68
+ 'The mark goes only on the primary kicker of a surface (page title, card title). Inline and table uses go without the mark.',
69
+ 'Do not use the kicker as a form label; Field labels are label-md in sentence case.',
70
+ 'Column headers, chart legends and stat labels already speak in this voice through their own parts; do not nest a Kicker inside them.',
71
+ ],
72
+ a11y: [
73
+ 'The mark is an empty decorative span; screen readers skip it. Do not put text or an icon inside it.',
74
+ 'The kicker is plain text, not a heading: no heading role, no aria-level. It is associated with the title by reading order.',
75
+ 'Uppercasing is CSS, so assistive tech reads the source case ("Prospect", not letter by letter).',
76
+ 'fg-subtle at 11px medium mono meets AA on canvas and surface; do not place kickers on tinted or dark backgrounds.',
77
+ ],
78
+ related: ['badge', 'card', 'stat', 'table', 'divider'],
79
+ };
@@ -0,0 +1,108 @@
1
+ import { STATE, ex, ICON, typeStyle } from "./_shared.mjs";
2
+ // Links: the "link-color" style (brand-700 semibold, underline appears on
3
+ // hover in brand-500, icons brand-500) and the "link-gray" style (gray-600 → gray-700,
4
+ // underline gray-400 on hover), radius 4, 100ms linear.
5
+ const icon = (name) => ICON[name].replace('cn-icon', 'cn-link__icon');
6
+ export const link = {
7
+ name: 'Link',
8
+ slug: 'link',
9
+ category: 'typography',
10
+ description: 'An anchor in the brand link color, semibold, with an underline that appears on hover (the reference link-color style). Three voices: inline (inside running text), standalone ("View all →" at the end of a list, with a trailing icon) and muted (the gray link-gray style for footers, legal, meta).',
11
+ usage: 'Use for navigation: to a page, a section, a document, an external site. Use Button (or Button variant="link"/"link-color") for actions that do not change the URL. Inline links only live inside sentences; anything that stands on its own line is standalone. Navigation lists (topbar, sidebar) are Tabs and SidebarNav, not rows of links.',
12
+ anatomy: [
13
+ { part: 'root', element: 'a', description: 'The anchor with a real href. Text names the destination ("Billing settings"), never "here" or "click".' },
14
+ { part: 'icon', element: 'svg', description: 'Optional trailing 20px icon (16px on small text): arrow when it goes somewhere in the app, external when it opens a new tab. Brand-500 at rest, brand-600 on hover. Decorative (aria-hidden); the text carries the meaning.', optional: true },
15
+ ],
16
+ props: {
17
+ variant: {
18
+ values: ['inline', 'standalone', 'muted'],
19
+ default: 'inline',
20
+ description: 'inline = brand link color, semibold, inherits the surrounding size; underline appears on hover; use only inside running text (the reference link-color). standalone = the same as an inline-flex row with a trailing icon, 14px semibold; for "View all", "Open in…", "Learn more" that sit on their own line (the reference link-color button md). muted = gray text that darkens on hover with a gray underline; footers, legal links, timestamps that link (the reference link-gray).',
21
+ },
22
+ },
23
+ states: {
24
+ hover: STATE.hover(),
25
+ focus: STATE.focus(),
26
+ current: STATE.current(),
27
+ },
28
+ base: {
29
+ root: {
30
+ color: '{color.fg-link}',
31
+ 'font-weight': '{font.weight.semibold}',
32
+ 'text-decoration-line': 'underline',
33
+ 'text-decoration-thickness': '1px',
34
+ 'text-underline-offset': '3px',
35
+ 'text-decoration-color': 'transparent',
36
+ cursor: 'pointer',
37
+ 'border-radius': '{radius.sm}',
38
+ 'box-decoration-break': 'clone',
39
+ '-webkit-box-decoration-break': 'clone',
40
+ 'transition-property': 'color, text-decoration-color, box-shadow',
41
+ 'transition-duration': '{motion.duration.fast}',
42
+ 'transition-timing-function': '{motion.easing.linear}',
43
+ },
44
+ icon: {
45
+ width: '1.25em',
46
+ height: '1.25em',
47
+ 'flex-shrink': '0',
48
+ display: 'inline-block',
49
+ 'vertical-align': '-0.25em',
50
+ 'margin-inline-start': '{space.1}',
51
+ color: '{brand.500}',
52
+ 'transition-property': 'color',
53
+ 'transition-duration': '{motion.duration.fast}',
54
+ 'transition-timing-function': '{motion.easing.linear}',
55
+ },
56
+ '@states': {
57
+ hover: { root: { color: '{color.fg-link-hover}', 'text-decoration-color': '{brand.500}' }, icon: { color: '{brand.600}' } },
58
+ focus: { root: { outline: 'none', 'box-shadow': '{shadow.focus}' } },
59
+ current: { root: { color: '{color.fg-default}', 'text-decoration-line': 'none', cursor: 'default' } },
60
+ },
61
+ },
62
+ variants: {
63
+ variant: {
64
+ inline: { root: {} },
65
+ standalone: {
66
+ root: {
67
+ display: 'inline-flex',
68
+ 'align-items': 'center',
69
+ gap: '{space.1}',
70
+ ...typeStyle('label-md'),
71
+ 'text-decoration-line': 'underline',
72
+ 'text-decoration-color': 'transparent',
73
+ },
74
+ icon: { width: '{size.icon.md}', height: '{size.icon.md}', 'margin-inline-start': '0', 'vertical-align': 'baseline' },
75
+ },
76
+ muted: {
77
+ root: { color: '{color.fg-muted}' },
78
+ icon: { color: '{color.fg-subtle}' },
79
+ '@states': {
80
+ hover: { root: { color: '{color.fg-default}', 'text-decoration-color': '{color.fg-subtle}' }, icon: { color: '{color.fg-muted}' } },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ examples: [
86
+ ex('Inline, in running text', `<p class="cn-text-body-md" style="max-width:52ch">Interview notes are shared with the project by default. <a href="#" class="cn-link" data-variant="inline">Read how sharing works</a> before you change it, or <a href="#" class="cn-link" data-variant="inline">keep this study private</a>.</p>`, 'Brand-700 semibold; the brand-500 underline appears on hover.'),
87
+ ex('Standalone with arrow', `<a href="#" class="cn-link" data-variant="standalone">View all 24 studies${icon('arrow')}</a>`, '14px semibold with a 20px brand-500 trailing icon (the reference link-color button, size md).'),
88
+ ex('External (new tab)', `<a href="https://docs.lumen.app" target="_blank" rel="noopener noreferrer" class="cn-link" data-variant="standalone">Open the docs<span class="cn-sr-only"> (opens in a new tab)</span>${icon('external')}</a>`, 'The external icon replaces the arrow; the sr-only text announces the new tab.'),
89
+ ex('Muted footer row with current page', `<div class="cn-text-body-sm" style="display:flex;gap:var(--cn-space-4)"><a href="#" class="cn-link" data-variant="muted">Terms</a><a href="#" class="cn-link" data-variant="muted">Privacy</a><a href="#" class="cn-link" data-variant="muted" aria-current="page">Status</a><a href="#" class="cn-link" data-variant="muted">Contact</a></div>`, 'the reference link-gray: gray-600, gray-700 with a gray-400 underline on hover. aria-current="page" drops the underline and darkens the text.'),
90
+ ],
91
+ rules: [
92
+ 'Links are semibold in the brand link color; the underline is a hover affordance, so keep link text short and distinct from the sentence around it.',
93
+ 'Inline links live only inside sentences and inherit the surrounding size; anything on its own line is standalone: "View all →", "Open the docs ↗", "Learn more →". One standalone link per card or list region, placed last.',
94
+ 'Link text names the destination: "Billing settings", "the Q3 report". Never "here", "click here", "this page", "Learn more" without a subject.',
95
+ 'A link changes the URL. If it opens a dialog, submits, toggles or deletes, it is a Button (variant link or link-color).',
96
+ 'External links get target="_blank", rel="noopener noreferrer", the external icon and an sr-only "(opens in a new tab)".',
97
+ 'Trailing icon only: arrow (in-app), external (new tab), or none. Never a leading icon and never two icons.',
98
+ 'Do not underline navigation lists (Tabs, SidebarNav, Breadcrumb have their own styles). Muted is for footers, legal and meta text.',
99
+ ],
100
+ a11y: [
101
+ 'Always an <a> with a real href, so it works with middle-click, Enter, and the context menu; never a span with onClick.',
102
+ 'Focus ring is the 4px box-shadow ring on :focus-visible (cloned across line breaks); never outline: none without replacement.',
103
+ 'aria-current="page" on the link that points to the current page (footer, pagination-like lists).',
104
+ 'New-tab links announce it via sr-only text; the icon is aria-hidden.',
105
+ 'Links with the same text must go to the same place within a view; disambiguate with the subject ("Edit study", "Edit interview").',
106
+ ],
107
+ related: ['button', 'breadcrumb', 'tabs', 'prose'],
108
+ };
@@ -0,0 +1,117 @@
1
+ import { ex, ICON, typeStyle } from "./_shared.mjs";
2
+ // list rows in the reference's table rhythm: 16 × 24px padding (12 × 20 compact), 14px medium ink
3
+ // titles over 14px muted descriptions, gray-200 hairlines, gray-50 hover and current rows, and the
4
+ // card frame (radius 12, ring, shadow-xs) when the list stands alone.
5
+ const HAIRLINE = '{border.width.thin} solid {color.border-default}';
6
+ const ring = (color) => `inset 0 0 0 1px ${color}`;
7
+ const TRUNCATE = { overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' };
8
+ const SILHOUETTE = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23CDCED1'/%3E%3Ccircle cx='32' cy='25' r='11' fill='%23FAFAFA'/%3E%3Cpath d='M10 64c2-13 11-21 22-21s20 8 22 21z' fill='%23FAFAFA'/%3E%3C/svg%3E";
9
+ const person = (name, status = 'none') => `<span class="cn-avatar" data-size="md" data-shape="circle" data-tone="neutral" data-status="${status}" aria-hidden="true"><img class="cn-avatar__image" src="${SILHOUETTE}" alt="">${status !== 'none' ? '<span class="cn-avatar__status"></span>' : ''}</span>`;
10
+ const company = (t, name) => `<span class="cn-avatar" data-size="md" data-shape="square" data-tone="neutral" data-status="none" role="img" aria-label="${name}"><span class="cn-avatar__fallback" aria-hidden="true">${t}</span></span>`;
11
+ const badge = (text, tone, dot = false) => `<span class="cn-badge" data-tone="${tone}" data-variant="soft" data-size="sm">${dot ? '<span class="cn-badge__dot"></span>' : ''}${text}</span>`;
12
+ const item = (leading, title, description, trailing, extra = '') => `<li class="cn-list__item"${extra}><span class="cn-list__leading">${leading}</span><span class="cn-list__content"><span class="cn-list__title">${title}</span><span class="cn-list__description">${description}</span></span><span class="cn-list__trailing">${trailing}</span></li>`;
13
+ const link = (title, description, trailing, extra = '') => `<a href="#" class="cn-list__item"${extra}><span class="cn-list__content"><span class="cn-list__title">${title}</span>${description ? `<span class="cn-list__description">${description}</span>` : ''}</span><span class="cn-list__trailing">${trailing}</span>${ICON.chevronRight.replace('cn-icon', 'cn-list__chevron')}</a>`;
14
+ const TEAM = `${item(person('Maya Chen', 'online'), 'Maya Chen', 'maya@lumen.co · Product Manager', badge('Admin', 'accent'))}${item(person('Daniel Costa', 'online'), 'Daniel Costa', 'daniel@lumen.co · Engineering', badge('Editor', 'neutral'))}${item(person('Sofia Almeida', 'offline'), 'Sofia Almeida', 'sofia@lumen.co · Design', badge('Editor', 'neutral'))}${item(person('Lucas Ferreira'), 'Lucas Ferreira', 'lucas@lumen.co · Frontend', badge('Invited', 'warning', true))}`;
15
+ export const list = {
16
+ name: 'List',
17
+ slug: 'list',
18
+ category: 'data-display',
19
+ description: 'Stacked rows with a leading slot (avatar, icon, checkbox), a 14px medium ink title with an optional muted description, and a trailing slot (badge, meta, chevron). Gray-200 hairlines between rows, gray-50 hover and current rows; the same rhythm as a table row. Rows can be links or buttons.',
20
+ usage: 'Collections where each item is one thing with a name and a little context: team members, conversations, settings entries, files, integrations. When people compare several columns of values, use Table. For key/value pairs use DescriptionList.',
21
+ anatomy: [
22
+ { part: 'root', element: 'ul', description: 'Column of items. <ul> with <li> items for static lists, <nav>/<div> with <a>/<button> items for interactive lists. Items are direct children.' },
23
+ { part: 'item', element: 'li', description: 'One row: flex, centered, 16 × 24px padding (12 × 20 compact), min height 72px with an avatar (56 compact). Also an <a> or <button> when the whole row is one action.' },
24
+ { part: 'leading', element: 'span', description: 'Optional slot before the content: Avatar md (sm at compact), a 20px icon or a Checkbox. Does not shrink.', optional: true },
25
+ { part: 'content', element: 'span', description: 'Flexible middle: title over description, both truncating.' },
26
+ { part: 'title', element: 'span', description: '14px medium in ink, one line, truncates with an ellipsis.' },
27
+ { part: 'description', element: 'span', description: '14px fg-muted under the title, one line, truncates.', optional: true },
28
+ { part: 'trailing', element: 'span', description: 'Optional slot after the content: Badge sm, Counter, a timestamp, a Switch or a sm ghost Button. Does not shrink.', optional: true },
29
+ { part: 'chevron', element: 'svg', description: '20px chevron-right in fg-subtle for items that navigate. Last child of link items.', optional: true },
30
+ ],
31
+ props: {
32
+ density: {
33
+ values: ['compact', 'default'],
34
+ default: 'default',
35
+ description: 'default = 72px min rows with 16 × 24px padding (two-line items with a 40px avatar); compact = 56px min rows with 12 × 20px padding (single-line items, settings, sm avatars).',
36
+ },
37
+ variant: {
38
+ values: ['plain', 'card'],
39
+ default: 'plain',
40
+ description: 'plain = no frame, for lists inside a Card (padding none) or a drawer; card = the list draws its own frame (radius 12, gray-200 ring, shadow-xs) on a surface, for standalone lists on the canvas.',
41
+ },
42
+ },
43
+ states: {
44
+ hover: { selector: ' > a:hover, & > button:hover', description: 'On an interactive item (an <a> or <button> row), not the root: gray-50 fill.', markup: 'make the item an <a href> or <button>; native :hover' },
45
+ focus: { selector: ' > a:focus-visible, & > button:focus-visible', description: 'On an interactive item: inset 2px brand ring so it stays inside the frame.', markup: 'native :focus-visible on the item' },
46
+ current: { selector: ' > [aria-current="true"], & > [data-selected]', description: 'On an item: gray-50 fill for the current or selected row (the reference highlights like hover); the title stays ink.', markup: 'aria-current="true" (navigation) or data-selected (selection) on the item' },
47
+ disabled: { selector: ' > [aria-disabled="true"], & > button:disabled', description: 'On an item: 50% opacity, no pointer events.', markup: 'aria-disabled="true" on the item, or disabled on a button item' },
48
+ },
49
+ base: {
50
+ root: { display: 'flex', 'flex-direction': 'column', 'min-width': '0', color: '{color.fg-default}' },
51
+ item: {
52
+ display: 'flex',
53
+ 'align-items': 'center',
54
+ gap: '{space.3}',
55
+ width: '100%',
56
+ 'min-width': '0',
57
+ 'min-height': 'calc({space.16} + {space.2})',
58
+ padding: '{space.4} {space.6}',
59
+ color: '{color.fg-default}',
60
+ 'text-align': 'left',
61
+ 'transition-property': 'background-color',
62
+ 'transition-duration': '{motion.duration.fast}',
63
+ 'transition-timing-function': '{motion.easing.linear}',
64
+ },
65
+ leading: { display: 'flex', 'align-items': 'center', 'flex-shrink': '0', color: '{color.fg-subtle}' },
66
+ content: { display: 'flex', 'flex-direction': 'column', 'min-width': '0', flex: '1 1 auto' },
67
+ title: { ...typeStyle('label-sm'), color: '{color.fg-default}', ...TRUNCATE },
68
+ description: { ...typeStyle('body-md'), color: '{color.fg-muted}', ...TRUNCATE },
69
+ trailing: { display: 'flex', 'align-items': 'center', gap: '{space.2}', 'flex-shrink': '0', ...typeStyle('body-md'), color: '{color.fg-muted}' },
70
+ chevron: { width: '{size.icon.md}', height: '{size.icon.md}', 'flex-shrink': '0', color: '{color.fg-subtle}' },
71
+ },
72
+ variants: {
73
+ density: {
74
+ compact: { item: { 'min-height': '{space.14}', padding: '{space.3} {space.5}', gap: '{space.3}' } },
75
+ default: { item: { 'min-height': 'calc({space.16} + {space.2})', padding: '{space.4} {space.6}' } },
76
+ },
77
+ variant: {
78
+ plain: { root: {} },
79
+ card: { root: { 'background-color': '{color.bg-surface}', 'box-shadow': `${ring('{color.border-default}')}, {shadow.xs}`, 'border-radius': '{radius.card}', overflow: 'hidden' } },
80
+ },
81
+ },
82
+ extraCss: `
83
+ .cn-list > .cn-list__item + .cn-list__item { border-top: ${HAIRLINE}; }
84
+ button.cn-list__item { appearance: none; border: 0; margin: 0; background: none; font: inherit; }
85
+ a.cn-list__item, button.cn-list__item { cursor: pointer; }
86
+ a.cn-list__item:hover, button.cn-list__item:hover { background-color: {color.bg-subtle}; }
87
+ a.cn-list__item:focus-visible, button.cn-list__item:focus-visible { outline: none; box-shadow: inset 0 0 0 2px {brand.500}; }
88
+ .cn-list > .cn-list__item[aria-current="true"], .cn-list > .cn-list__item[data-selected] { background-color: {color.bg-subtle}; }
89
+ .cn-list > .cn-list__item[aria-disabled="true"], .cn-list > button.cn-list__item:disabled { opacity: {opacity.disabled}; pointer-events: none; }
90
+ a.cn-list__item:hover .cn-list__chevron { color: {color.fg-muted}; }
91
+ .cn-list__leading > .cn-icon { width: {size.icon.md}; height: {size.icon.md}; }`,
92
+ examples: [
93
+ ex('Team members with avatars and roles', `<ul class="cn-list" data-density="default" data-variant="card" style="width:100%;max-width:560px">${TEAM}</ul>`, '72px rows: Avatar md leading, ink medium name over the muted email, Badge sm trailing.'),
94
+ ex('Link items with chevron (compact)', `<nav class="cn-list" data-density="compact" data-variant="card" aria-label="Workspace settings" style="width:100%;max-width:560px">${link('General', '', 'Lumen · Lisbon', ' aria-current="true"')}${link('Members', '', badge('6', 'neutral'))}${link('Billing', '', 'Business plan')}${link('Integrations', '', badge('2 connected', 'success', true))}</nav>`, 'Whole row is the link; the current page is gray-50. Single-line 56px rows.'),
95
+ ex('Inside a Card with a header', `<section class="cn-card" data-variant="default" data-padding="none" style="width:100%;max-width:560px"><header class="cn-card-header" data-variant="default" data-size="md"><div class="cn-card-header__content"><h2 class="cn-card-header__title">Integrations</h2><p class="cn-card-header__description">Connect the tools your team already uses.</p></div></header><ul class="cn-list" data-density="default" data-variant="plain">${item(company('SL', 'Slack'), 'Slack', 'Notifications in #lumen-product', badge('Connected', 'success', true))}${item(company('GH', 'GitHub'), 'GitHub', 'Link pull requests to tasks', badge('Connected', 'success', true))}${item(company('FG', 'Figma'), 'Figma', 'Embed designs in projects', `<button type="button" class="cn-button" data-variant="outline" data-size="sm"><span class="cn-button__label">Connect</span></button>`)}</ul></section>`, 'plain variant: the Card supplies the frame and the CardHeader names the list.'),
96
+ ex('Selected and disabled items', `<ul class="cn-list" data-density="default" data-variant="card" style="width:100%;max-width:560px">${item(person('Aisha Khan', 'online'), 'Aisha Khan', 'aisha@lumen.co · Backend', badge('Owner', 'accent'), ' data-selected')}${item(person('Noah Berg'), 'Noah Berg', 'noah@lumen.co · Product design', '<span>2h ago</span>')}${item(person('Elena Rossi'), 'Elena Rossi', 'elena@lumen.co · Deactivated', badge('Deactivated', 'neutral'), ' aria-disabled="true"')}</ul>`),
97
+ ex('Button items (pick one)', `<div class="cn-list" data-density="compact" data-variant="card" role="listbox" aria-label="Choose a sender" style="width:100%;max-width:420px"><button type="button" class="cn-list__item" role="option" aria-selected="true" data-selected><span class="cn-list__content"><span class="cn-list__title">hello@lumen.co</span><span class="cn-list__description">Default · verified domain</span></span><span class="cn-list__trailing">${ICON.check}</span></button><button type="button" class="cn-list__item" role="option" aria-selected="false"><span class="cn-list__content"><span class="cn-list__title">maya@lumen.co</span><span class="cn-list__description">Personal</span></span></button></div>`),
98
+ ],
99
+ rules: [
100
+ 'Every item has a title (14px medium ink); the description is optional, 14px muted and never longer than one line. Longer text belongs in the record, not the list.',
101
+ 'One leading slot and one trailing slot per item at most. Leading = identity (Avatar md, a 20px icon, a Checkbox); trailing = state or meta (Badge sm, Counter, timestamp, Switch, one sm Button).',
102
+ 'If the whole row is one action, the item is an <a> (navigate) or <button> (act) and shows the chevron only when it navigates to a page. Do not put buttons inside link rows.',
103
+ 'Rows never change height on hover; hover, current and selected are the same gray-50 fill.',
104
+ 'Use variant="card" only when the list stands alone on the canvas; inside a Card (padding none) or a drawer use plain so the Card supplies the frame.',
105
+ 'compact (56px, sm avatars) for single-line items and settings; default (72px, md avatars) when items have two lines. One density per list.',
106
+ 'Order by what the user is looking for (recency, priority), not alphabetically by default.',
107
+ 'Long lists paginate or virtualize after ~50 items; do not stack hundreds of rows.',
108
+ ],
109
+ a11y: [
110
+ 'Static lists are <ul>/<li>; navigation lists are <nav> with <a href> items; pick-one lists use role="listbox" with role="option" and aria-selected on button items.',
111
+ 'aria-current="true" (or "page") marks the current navigation item; data-selected alone is only visual.',
112
+ 'Interactive rows are one focus stop each with the inset ring; the title text is the accessible name, so it must be meaningful on its own.',
113
+ 'Avatars in rows are decorative (aria-hidden or alt="") because the title names the person; presence must be in text when it matters.',
114
+ 'Truncated titles keep the full text in the DOM (ellipsis is CSS); add a title attribute only when the truncation hides essential information.',
115
+ ],
116
+ related: ['table', 'description-list', 'avatar', 'badge', 'card', 'card-header', 'sidebar-nav', 'empty-state'],
117
+ };