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,55 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, AVATAR, BADGE, ICON, APP_CSS, CARD, CARD_HEAD } from "./_app.mjs";
2
+ const AGENDA = [['Thursday, 10 September', [['9:00', 'Design sync', ['MC', 'SA', 'NB'], 'accent'], ['15:00', 'Customer call — Catalog', ['MC', 'DC'], 'success']]], ['Friday, 11 September', [['10:00', 'Billing demo', ['DC', 'AK'], 'warning'], ['16:00', 'Retro', ['MC', 'DC', 'SA', 'LF'], 'accent']]]];
3
+ const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
4
+ const EVENTS = { 2: [['9:00', 'Design sync', 'accent']], 3: [['10:30', 'Roadmap review', 'info'], ['15:00', 'Customer call', 'success']], 8: [['11:00', 'Sprint planning', 'accent']], 10: [['14:00', 'Billing demo', 'warning']], 14: [['9:30', 'Hiring loop', 'info'], ['13:00', 'Lunch & learn', 'success'], ['16:00', 'Retro', 'accent']], 17: [['10:00', 'All hands', 'accent']], 22: [['9:00', 'Design sync', 'accent']], 25: [['15:00', 'Launch go/no-go', 'danger']] };
5
+ const MONTH = `<div class="cn-cal"><div class="cn-cal__weekdays">${DAYS.map((d) => `<span>${d}</span>`).join('')}</div><div class="cn-cal__grid">${Array.from({ length: 35 }, (_, i) => { const day = i; const inMonth = day >= 1 && day <= 30; const n = inMonth ? day : day < 1 ? 31 + day : day - 30; return `<div class="cn-cal__cell"${inMonth ? '' : ' data-outside'}${day === 11 ? ' data-today' : ''}><span class="cn-cal__daynum">${n}</span>${inMonth ? (EVENTS[day] ?? []).map(([t, e, tone]) => `<span class="cn-cal__event" data-tone="${tone}"><span class="cn-cal__time">${t}</span>${e}</span>`).join('') : ''}</div>`; }).join('')}</div></div>`;
6
+ const HOURS = ['8:00', '9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
7
+ const WEEK = `<div class="cn-cal cn-cal--week" role="region" tabindex="0" aria-label="Week calendar, scroll horizontally to see all days"><div class="cn-cal__weekhead"><span></span>${DAYS.slice(0, 5).map((d, i) => `<span class="cn-cal__dayhead"${i === 4 ? ' data-today' : ''}><span class="cn-text-label-sm">${d}</span><span class="cn-cal__daybig">${7 + i}</span></span>`).join('')}</div><div class="cn-cal__weekgrid">${HOURS.map((h, row) => `<span class="cn-cal__hour" style="grid-column:1;grid-row:${row + 1}">${h}</span>${DAYS.slice(0, 5).map((_, col) => `<span class="cn-cal__slot" style="grid-column:${col + 2};grid-row:${row + 1}"></span>`).join('')}`).join('')}<span class="cn-cal__block" data-tone="accent" style="grid-column:2;grid-row:2 / span 2"><span class="cn-cal__time">9:00 – 10:00</span>Design sync</span><span class="cn-cal__block" data-tone="info" style="grid-column:3;grid-row:3 / span 3"><span class="cn-cal__time">10:00 – 11:30</span>Roadmap review</span><span class="cn-cal__block" data-tone="success" style="grid-column:5;grid-row:7 / span 2"><span class="cn-cal__time">14:00 – 15:00</span>Customer call</span><span class="cn-cal__block" data-tone="warning" style="grid-column:6;grid-row:4 / span 2"><span class="cn-cal__time">11:00 – 12:00</span>Billing demo</span></div></div>`;
8
+ export const calendarPage = {
9
+ name: 'Calendar and scheduling',
10
+ slug: 'calendar-page',
11
+ category: 'app-page',
12
+ description: 'Month grid with event chips, week view with time blocks, and an agenda list with an event detail card.',
13
+ rules: [
14
+ 'Header: month/year title with prev/next icon buttons and a "Today" outline button; view switch (Month / Week / Day) as a segmented control; "New event" is the primary action.',
15
+ 'Month grid: 7 columns, cells ≥ 120px tall, day number top-left (label-sm), events as small chips (body-xs, 4px radius, tinted by category); outside-month days muted; today has a brand-filled day number.',
16
+ 'Week view: time column + 5–7 day columns, 1-hour rows (64px), events as blocks with a tinted background and left border; overlapping events share the column.',
17
+ 'Agenda: list grouped by day; each item time (code-sm) + title + attendees avatars; clicking opens the detail card (title, time, location, attendees, actions).',
18
+ ],
19
+ html: `<div class="cn-app">${SIDEBAR('Dashboard')}<main class="cn-app__main">${PAGE_HEADER('September 2026', '', `<div class="cn-segmented-control" data-size="md" data-variant="default" role="radiogroup" aria-label="View"><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="true">Month</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Week</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Day</button></div>${BTN('outline', 'Today', 'md')}<div class="cn-button-group" data-variant="attached" data-orientation="horizontal"><button type="button" class="cn-button" data-variant="outline" data-size="md" data-icon="only" aria-label="Previous month">${ICON.chevronRight.replace('cn-icon', 'cn-button__icon').replace('<svg', '<svg style="transform:rotate(180deg)"')}</button><button type="button" class="cn-button" data-variant="outline" data-size="md" data-icon="only" aria-label="Next month">${ICON.chevronRight.replace('cn-icon', 'cn-button__icon')}</button></div>${BTN('primary', 'New event', 'md', ICON.plus)}`).replace('<p class="cn-page-header__description"></p>', '')}${MONTH}</main></div>`,
20
+ variants: [
21
+ { title: 'Week view', html: `<div class="cn-app">${SIDEBAR('Dashboard', 'slim')}<main class="cn-app__main">${PAGE_HEADER('7 – 11 September 2026', '', `<div class="cn-segmented-control" data-size="md" data-variant="default" role="radiogroup" aria-label="View"><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Month</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="true">Week</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Day</button></div>${BTN('outline', 'Today', 'md')}${BTN('primary', 'New event', 'md', ICON.plus)}`).replace('<p class="cn-page-header__description"></p>', '')}${WEEK}</main></div>` },
22
+ { title: 'Agenda with event detail', html: `<div class="cn-app">${SIDEBAR('Dashboard')}<main class="cn-app__main">${PAGE_HEADER('Agenda', 'Your next 7 days.', BTN('primary', 'New event', 'md', ICON.plus))}<div class="cn-grid" data-cols="3" data-gap="6"><div class="cn-app__span2 cn-stack" data-gap="6">${AGENDA.map(([day, items]) => `<section class="cn-stack" data-gap="2"><h2 class="cn-text-label-sm cn-app__muted">${day}</h2><div class="cn-card" data-variant="default" data-padding="none"><ul class="cn-list" data-density="default" data-variant="plain">${items.map(([t, title, ppl, tone], i) => `<li class="cn-list__item"${i === 0 && day.startsWith('Thu') ? ' data-selected' : ''}><span class="cn-list__leading"><span class="cn-cal__agenda-time" data-tone="${tone}">${t}</span></span><span class="cn-list__content"><span class="cn-list__title">${title}</span><span class="cn-list__description">45 min · Google Meet</span></span><span class="cn-list__trailing"><span class="cn-avatar-group" data-size="sm">${ppl.map((p) => AVATAR(p, p, 'sm', tone).replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')).join('')}</span></span></li>`).join('')}</ul></div></section>`).join('')}</div>${CARD(CARD_HEAD('Design sync', 'Thursday, 10 September · 9:00 – 10:00', '', BADGE('Recurring', 'accent')), `<div class="cn-stack" data-gap="5"><dl class="cn-app__dl"><div><dt>Where</dt><dd>Google Meet · Room “Aurora”</dd></div><div><dt>Attendees</dt><dd><div class="cn-stack" data-gap="2">${[['MC', 'Maya Chen', 'Organizer', 'accent'], ['SA', 'Sofia Almeida', 'Accepted', 'warning'], ['NB', 'Noah Berg', 'Tentative', 'success']].map(([i, n, s, tone]) => `<div class="cn-row" data-gap="2">${AVATAR(i, n, 'xs', tone)}<span class="cn-text-body-sm">${n}</span><span class="cn-text-body-xs cn-app__muted">${s}</span></div>`).join('')}</div></dd></div><div><dt>Notes</dt><dd>Review the settings page redesign and decide on the drawer vs. dialog for member edits.</dd></div></dl><div class="cn-row" data-gap="2">${BTN('outline', 'Edit', 'sm')}${BTN('primary', 'Join call', 'sm')}</div></div>`)}</div></main></div>` },
23
+ ],
24
+ css: `${APP_CSS}
25
+ .cn-app__span2 { grid-column: span 2; min-width: 0; }
26
+ .cn-cal { border: {border.width.thin} solid {color.border-default}; border-radius: {radius.card}; background-color: {color.bg-surface}; overflow: hidden; }
27
+ .cn-cal__weekdays { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); border-bottom: {border.width.thin} solid {color.border-default}; }
28
+ .cn-cal__weekdays span { padding: {space.2} {space.3}; font-size: {font.size.xs}; font-weight: {font.weight.medium}; color: {color.fg-muted}; }
29
+ .cn-cal__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
30
+ .cn-cal__cell { min-width: 0; min-height: 120px; padding: {space.2}; border-inline-end: {border.width.thin} solid {color.border-subtle}; border-bottom: {border.width.thin} solid {color.border-subtle}; display: flex; flex-direction: column; gap: {space.1}; }
31
+ .cn-cal__cell:nth-child(7n) { border-inline-end: 0; }
32
+ .cn-cal__cell[data-outside] { background-color: {color.bg-subtle}; }
33
+ .cn-cal__cell[data-outside] .cn-cal__daynum { color: {color.fg-subtle}; }
34
+ .cn-cal__daynum { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: {radius.full}; font-size: {font.size.sm}; font-weight: {font.weight.medium}; color: {color.fg-muted}; }
35
+ .cn-cal__cell[data-today] .cn-cal__daynum { background-color: {color.bg-action}; color: {color.fg-on-action}; }
36
+ .cn-cal__event { display: flex; gap: {space.1}; align-items: baseline; padding: {space.0.5} {space.1.5}; border-radius: {radius.sm}; font-size: {font.size.xs}; font-weight: {font.weight.medium}; line-height: 1.4; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; background-color: {color.bg-subtle}; color: {color.fg-default}; border-inline-start: 2px solid {color.border-strong}; }
37
+ .cn-cal__time { color: {color.fg-subtle}; font-weight: {font.weight.regular}; }
38
+ .cn-cal__event[data-tone="accent"], .cn-cal__block[data-tone="accent"], .cn-cal__agenda-time[data-tone="accent"] { background-color: {color.bg-action-subtle}; color: {color.fg-action}; border-color: {color.bg-action}; }
39
+ .cn-cal__event[data-tone="info"], .cn-cal__block[data-tone="info"], .cn-cal__agenda-time[data-tone="info"] { background-color: {color.bg-info-subtle}; color: {color.fg-info}; border-color: {color.bg-info}; }
40
+ .cn-cal__event[data-tone="success"], .cn-cal__block[data-tone="success"], .cn-cal__agenda-time[data-tone="success"] { background-color: {color.bg-success-subtle}; color: {color.fg-success}; border-color: {color.bg-success}; }
41
+ .cn-cal__event[data-tone="warning"], .cn-cal__block[data-tone="warning"], .cn-cal__agenda-time[data-tone="warning"] { background-color: {color.bg-warning-subtle}; color: {color.fg-warning}; border-color: {color.bg-warning}; }
42
+ .cn-cal__event[data-tone="danger"], .cn-cal__block[data-tone="danger"], .cn-cal__agenda-time[data-tone="danger"] { background-color: {color.bg-danger-subtle}; color: {color.fg-danger}; border-color: {color.bg-danger}; }
43
+ .cn-cal--week { overflow-x: auto; }
44
+ .cn-cal__weekhead { display: grid; min-width: {size.container.sm}; grid-template-columns: 64px repeat(5, minmax(0, 1fr)); border-bottom: {border.width.thin} solid {color.border-default}; }
45
+ .cn-cal__dayhead { display: flex; flex-direction: column; align-items: center; gap: {space.1}; padding: {space.3}; color: {color.fg-muted}; border-inline-start: {border.width.thin} solid {color.border-subtle}; }
46
+ .cn-cal__daybig { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: {radius.full}; font-size: {font.size.lg}; font-weight: {font.weight.semibold}; color: {color.fg-default}; }
47
+ .cn-cal__dayhead[data-today] .cn-cal__daybig { background-color: {color.bg-action}; color: {color.fg-on-action}; }
48
+ .cn-cal__weekgrid { display: grid; min-width: {size.container.sm}; grid-template-columns: 64px repeat(5, minmax(0, 1fr)); grid-auto-rows: 64px; position: relative; }
49
+ .cn-cal__hour { padding: {space.2}; font-size: {font.size.xs}; color: {color.fg-subtle}; text-align: right; border-bottom: {border.width.thin} solid {color.border-subtle}; }
50
+ .cn-cal__slot { border-inline-start: {border.width.thin} solid {color.border-subtle}; border-bottom: {border.width.thin} solid {color.border-subtle}; }
51
+ .cn-cal__block { margin: 2px 4px; padding: {space.1.5} {space.2}; border-radius: {radius.md}; border-inline-start: 3px solid; font-size: {font.size.xs}; font-weight: {font.weight.medium}; display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
52
+ .cn-cal__agenda-time { display: inline-flex; align-items: center; height: 28px; padding-inline: {space.2}; border-radius: {radius.md}; font-family: {font.family.mono}; font-size: {font.size.xs}; border-inline-start: 3px solid; }
53
+ @media (max-width: {breakpoint.md}) { .cn-cal__cell { min-height: 64px; padding: {space.1}; } .cn-cal__weekdays span { padding-inline: {space.1}; text-align: center; } .cn-cal__event { display: block; font-size: {font.size.xs}; padding-inline: {space.1}; } .cn-cal__event .cn-cal__time { display: none; } .cn-app__span2 { grid-column: auto; } }`,
54
+ viewport: 'desktop',
55
+ };
@@ -0,0 +1,28 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, STAT, CHART, BARS, CARD, CARD_HEAD, TEAM_TABLE, PAGINATION, TABS, AVATAR, BADGE, ICON, APP_CSS } from "./_app.mjs";
2
+ const PROJECTS = [['Website redesign', 'Marketing', 68, 'On track', 'success'], ['Mobile app 2.0', 'Product', 32, 'At risk', 'warning'], ['Billing platform', 'Engineering', 91, 'On track', 'success'], ['Brand refresh', 'Design', 12, 'Planning', 'neutral'], ['Data warehouse', 'Engineering', 47, 'Blocked', 'danger'], ['Onboarding flow', 'Product', 80, 'On track', 'success']];
3
+ const METRICS = `<div class="cn-grid" data-cols="4" data-gap="6" data-keep>${STAT('Total customers', '2,420', '+40%')}${STAT('Members', '1,210', '+10%')}${STAT('Active now', '316', '−2%', 'down')}${STAT('Conversion', '4.2%', '+0.4%')}</div>`;
4
+ const CHART_CARD = CARD(CARD_HEAD('Revenue', 'Monthly recurring revenue, last 12 months', `${TABS(['12 months', '30 days', '7 days'], 0, 'pill')}`), `<div class="cn-stack" data-gap="4"><div class="cn-row" data-justify="between"><span class="cn-text-heading-lg">$128,430</span><span class="cn-app__legend"><span>This year</span><span>Last year</span></span></div>${CHART(260)}</div>`);
5
+ const ACTIVITY = CARD(CARD_HEAD('Recent activity', '', BTN('link-color', 'View all', 'sm')), `<ul class="cn-list" data-density="default" data-variant="plain">${[['MC', 'Maya Chen', 'commented on Q3 roadmap', '2m', 'accent'], ['DC', 'Daniel Costa', 'merged “Billing refactor”', '14m', 'success'], ['SA', 'Sofia Almeida', 'uploaded 3 files to Brand', '1h', 'warning'], ['LF', 'Lucas Ferreira', 'invited Elena Rossi', '3h', 'info']].map(([i, n, a, t, tone]) => `<li class="cn-list__item"><span class="cn-list__leading">${AVATAR(i, n, 'sm', tone)}</span><span class="cn-list__content"><span class="cn-list__title">${n}</span><span class="cn-list__description">${a}</span></span><span class="cn-list__trailing">${t}</span></li>`).join('')}</ul>`);
6
+ const TABLE_CARD = `<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Team members', 'Manage your team members and their account permissions here.', `${BTN('outline', 'Download all', 'md', ICON.copy)}${BTN('primary', 'Add member', 'md', ICON.plus)}`, BADGE('100 users', 'accent'))}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table">${TEAM_TABLE()}</div><footer class="cn-card__footer">${PAGINATION}</footer></section>`;
7
+ export const dashboardPage = {
8
+ name: 'Dashboard',
9
+ slug: 'dashboard-page',
10
+ category: 'app-page',
11
+ description: 'Application home: sidebar, page header with the primary action, a metrics row, a chart card with an activity list, and a data table card. Three layouts.',
12
+ rules: [
13
+ 'Structure: .cn-app (flex) → Sidebar + .cn-app__main (32px padding, 32px gap between blocks). Use the Sidebar component’s current width and navigation density.',
14
+ 'Order: page header → metrics (4 stat cards, 24px gap) → chart + activity (2:1 grid) → table card with its own header and pagination footer.',
15
+ 'Exactly one primary button on the page (in the header); card headers use outline/link buttons.',
16
+ 'Charts live inside a card with a section header (title, description, period tabs) and a legend; no 3D, no gradients, brand color for the main series and brand-300 for the secondary.',
17
+ 'Below 1024px the sidebar hides (use the MobileHeader) and grids collapse: metrics 2×2, chart and activity stacked.',
18
+ ],
19
+ html: `<div class="cn-app">${SIDEBAR('Dashboard')}<main class="cn-app__main">${PAGE_HEADER('Welcome back, Maya', 'Track, manage and forecast your customers and orders.', `${BTN('outline', 'Export', 'md', ICON.external)}${BTN('primary', 'New report', 'md', ICON.plus)}`)}${METRICS}<div class="cn-grid" data-cols="3" data-gap="6"><div class="cn-app__span2">${CHART_CARD}</div>${ACTIVITY}</div>${TABLE_CARD}</main></div>`,
20
+ variants: [
21
+ { title: 'Analytics with tabs and bar chart', html: `<div class="cn-app">${SIDEBAR('Reporting', 'slim')}<main class="cn-app__main">${PAGE_HEADER('Reporting', 'Performance across every channel.', `${BTN('outline', 'Share', 'md')}${BTN('primary', 'Create report', 'md')}`, 'with-tabs', '')}<div class="cn-page-header__tabs" >${TABS(['Overview', 'Traffic', 'Sales', 'Retention', 'Cohorts'], 0)}</div>${METRICS}${CARD(CARD_HEAD('Sessions by channel', 'Unique sessions, last 12 months', BTN('outline', 'Filters', 'sm')), `<div class="cn-stack" data-gap="4"><span class="cn-app__legend"><span>Organic</span><span>Paid</span></span>${BARS(280)}</div>`)}<div class="cn-grid" data-cols="2" data-gap="6">${CARD(CARD_HEAD('Top pages', '', BTN('link-color', 'View all', 'sm')), `<ul class="cn-list" data-density="compact" data-variant="plain">${[['/pricing', '12,430', '+8%'], ['/blog/design-systems', '9,214', '+24%'], ['/', '8,102', '−3%'], ['/docs', '6,320', '+2%']].map(([p, v, d]) => `<li class="cn-list__item"><span class="cn-list__content"><span class="cn-list__title">${p}</span></span><span class="cn-list__trailing"><span class="cn-text-label-sm">${v}</span>${BADGE(d, d.startsWith('−') ? 'danger' : 'success')}</span></li>`).join('')}</ul>`)}${CARD(CARD_HEAD('Conversion funnel', '', BTN('link-color', 'View report', 'sm')), `<div class="cn-stack" data-gap="4">${[['Visited', 100, '24,020'], ['Signed up', 42, '10,088'], ['Activated', 28, '6,725'], ['Paid', 9, '2,161']].map(([l, w, n]) => `<div class="cn-progress" data-size="md" data-tone="action" role="progressbar" aria-valuenow="${w}" aria-valuemin="0" aria-valuemax="100" aria-label="${l}"><div class="cn-progress__header"><span class="cn-progress__label">${l}</span><span class="cn-progress__value">${n}</span></div><div class="cn-progress__track"><div class="cn-progress__indicator" style="width:${w}%"></div></div></div>`).join('')}</div>`)}</div></main></div>` },
22
+ { title: 'Dark sidebar, project overview', html: `<div class="cn-app">${SIDEBAR('Projects', 'simple', 'dark')}<main class="cn-app__main">${PAGE_HEADER('Projects', 'Every project your team is working on this quarter.', `${BTN('outline', 'Import', 'md')}${BTN('primary', 'New project', 'md', ICON.plus)}`)}<div class="cn-grid" data-cols="3" data-gap="6">${PROJECTS.map(([t, team, p, st, tone]) => `<section class="cn-card" data-variant="default" data-padding="md"><div class="cn-card__body cn-stack" data-gap="4"><div class="cn-row" data-justify="between"><span class="cn-text-heading-xs">${t}</span>${BADGE(st, tone, 'soft', true)}</div><span class="cn-text-body-sm cn-app__muted">${team} · 4 members</span><div class="cn-progress" data-size="sm" data-tone="action" role="progressbar" aria-valuenow="${p}" aria-valuemin="0" aria-valuemax="100" aria-label="Progress"><div class="cn-progress__header"><span class="cn-progress__label">Progress</span><span class="cn-progress__value">${p}%</span></div><div class="cn-progress__track"><div class="cn-progress__indicator" style="width:${p}%"></div></div></div><div class="cn-row" data-justify="between"><div class="cn-avatar-group" data-size="sm">${AVATAR('MC', 'Maya Chen', 'sm', 'accent').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}${AVATAR('DC', 'Daniel Costa', 'sm', 'success').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}${AVATAR('SA', 'Sofia Almeida', 'sm', 'warning').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}</div>${BTN('link-color', 'Open', 'sm')}</div></div></section>`).join('')}</div></main></div>` },
23
+ ],
24
+ css: `${APP_CSS}
25
+ .cn-app__span2 { grid-column: span 2; min-width: 0; }
26
+ @media (max-width: {breakpoint.md}) { .cn-app__span2 { grid-column: auto; } }`,
27
+ viewport: 'desktop',
28
+ };
@@ -0,0 +1,30 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, TABS, AVATAR, BADGE, ICON, APP_CSS, CHECKBOX, PAGINATION, IMG } from "./_app.mjs";
2
+ const FILES = [['Brand guidelines v3.pdf', 'PDF', '4.2 MB', 'Sofia Almeida', 'SA', 'warning', 'Today'], ['Q3 roadmap.fig', 'Figma', '18.6 MB', 'Maya Chen', 'MC', 'accent', 'Yesterday'], ['Billing migration plan.docx', 'Document', '212 KB', 'Daniel Costa', 'DC', 'success', 'Mon'], ['Onboarding screens.png', 'Image', '1.8 MB', 'Noah Berg', 'NB', 'success', 'Mon'], ['Customer interviews.csv', 'Spreadsheet', '96 KB', 'Aisha Khan', 'AK', 'accent', 'Last week'], ['Launch checklist.md', 'Markdown', '12 KB', 'Lucas Ferreira', 'LF', 'info', 'Last week']];
3
+ const fileIcon = `<span class="cn-files__icon" aria-hidden="true">${ICON.copy}</span>`;
4
+ const CRUMBS = `<nav class="cn-breadcrumb" data-size="md" aria-label="Breadcrumb"><ol class="cn-breadcrumb__list"><li class="cn-breadcrumb__item"><a class="cn-breadcrumb__link" href="#">Files</a></li><li class="cn-breadcrumb__separator" aria-hidden="true">${ICON.chevronRight}</li><li class="cn-breadcrumb__item"><a class="cn-breadcrumb__link" href="#">Product</a></li><li class="cn-breadcrumb__separator" aria-hidden="true">${ICON.chevronRight}</li><li class="cn-breadcrumb__item"><span class="cn-breadcrumb__current" aria-current="page">Q3 launch</span></li></ol></nav>`;
5
+ const TOOLBAR = `<div class="cn-row" data-justify="between" data-gap="3" data-wrap><div class="cn-row" data-gap="3"><div class="cn-input" data-variant="default" data-size="md" style="width:320px">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search files" aria-label="Search files"></div>${BTN('outline', 'Type', 'md', ICON.chevronDown)}${BTN('outline', 'Owner', 'md', ICON.chevronDown)}</div><div class="cn-segmented-control" data-size="md" data-variant="default" role="radiogroup" aria-label="View"><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="true">List</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Grid</button></div></div>`;
6
+ const TABLE = `<table class="cn-table" data-density="default" data-variant="default" data-interactive><thead class="cn-table__head"><tr class="cn-table__row"><th class="cn-table__header-cell" scope="col" style="width:48px">${CHECKBOX()}</th><th class="cn-table__header-cell" scope="col">Name</th><th class="cn-table__header-cell" scope="col">Type</th><th class="cn-table__header-cell" scope="col" data-align="right">Size</th><th class="cn-table__header-cell" scope="col">Owner</th><th class="cn-table__header-cell" scope="col">Modified</th><th class="cn-table__header-cell" scope="col"><span class="cn-sr-only">Actions</span></th></tr></thead><tbody class="cn-table__body">${FILES.map(([n, t, s, o, i, tone, d], k) => `<tr class="cn-table__row"${k === 1 ? ' data-selected' : ''}><td class="cn-table__cell">${CHECKBOX('', k === 1)}</td><td class="cn-table__cell"><div class="cn-row" data-gap="3">${fileIcon}<span class="cn-text-label-sm">${n}</span></div></td><td class="cn-table__cell">${BADGE(t)}</td><td class="cn-table__cell" data-align="right">${s}</td><td class="cn-table__cell"><div class="cn-row" data-gap="2">${AVATAR(i, o, 'xs', tone)}<span>${o}</span></div></td><td class="cn-table__cell">${d}</td><td class="cn-table__cell cn-table__actions"><div class="cn-row" data-gap="1" data-justify="end"><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="Download">${ICON.external.replace('cn-icon', 'cn-icon-button__icon')}</button><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="More">${ICON.dots.replace('cn-icon', 'cn-icon-button__icon')}</button></div></td></tr>`).join('')}</tbody></table>`;
7
+ export const filesPage = {
8
+ name: 'Files and documents',
9
+ slug: 'files-page',
10
+ category: 'app-page',
11
+ description: 'A file manager: breadcrumbs, toolbar (search, filters, list/grid toggle), a selectable table of files with owner and size, or a grid of file cards with previews; an upload dropzone and a details drawer.',
12
+ rules: [
13
+ 'Breadcrumbs above the page title show the folder path (max 3 levels shown, the rest collapsed).',
14
+ 'Toolbar: search (320) + filter buttons on the left, list/grid segmented control on the right.',
15
+ 'List: table with checkbox, icon + name (label-sm), type badge, size right-aligned, owner (avatar xs + name), modified, and ghost icon actions. Selected rows get the action-subtle fill.',
16
+ 'Grid: cards with a 16:10 preview, name, type badge and size; 4 per row, 24px gap.',
17
+ 'Upload: a dropzone card at the top when the folder is empty or on hover of the page in real apps; never a floating "+" button.',
18
+ ],
19
+ html: `<div class="cn-app">${SIDEBAR('Projects')}<main class="cn-app__main">${PAGE_HEADER('Q3 launch', '6 files · 25.1 MB · shared with 8 people', `${BTN('outline', 'Share', 'md', ICON.user)}${BTN('primary', 'Upload', 'md', ICON.plus)}`, 'simple', `<div class="cn-page-header__crumbs">${CRUMBS}</div>`)}${TOOLBAR}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table">${TABLE}</div><footer class="cn-card__footer">${PAGINATION}</footer></section></main></div>`,
20
+ variants: [
21
+ { title: 'Grid with upload dropzone', html: `<div class="cn-app">${SIDEBAR('Projects')}<main class="cn-app__main">${PAGE_HEADER('Q3 launch', '6 files · 25.1 MB · shared with 8 people', `${BTN('outline', 'Share', 'md', ICON.user)}${BTN('primary', 'Upload', 'md', ICON.plus)}`, 'simple', `<div class="cn-page-header__crumbs">${CRUMBS}</div>`)}<div class="cn-file-dropzone" data-size="md"><input class="cn-file-dropzone__input" type="file" multiple aria-label="Upload files"><span class="cn-file-dropzone__icon" aria-hidden="true">${ICON.copy}</span><span class="cn-file-dropzone__title"><strong>Click to upload</strong> or drag and drop</span><span class="cn-file-dropzone__hint">Any file up to 50 MB</span></div>${TOOLBAR.replace('aria-checked="true">List', 'aria-checked="false">List').replace('aria-checked="false">Grid', 'aria-checked="true">Grid')}<div class="cn-grid" data-cols="4" data-gap="6">${FILES.map(([n, t, s]) => `<article class="cn-card" data-variant="default" data-padding="none"><div class="cn-files__preview">${IMG('16 / 10', 'data-radius="none"')}</div><div class="cn-card__body cn-app__card-body cn-stack" data-gap="2" style="padding:var(--cn-space-4)"><div class="cn-row" data-justify="between" data-gap="2"><span class="cn-text-label-sm cn-truncate">${n}</span><button type="button" class="cn-icon-button" data-variant="ghost" data-size="xs" data-shape="square" aria-label="More">${ICON.dots.replace('cn-icon', 'cn-icon-button__icon')}</button></div><div class="cn-row" data-gap="2">${BADGE(t)}<span class="cn-text-body-sm cn-app__muted">${s}</span></div></div></article>`).join('')}</div></main></div>` },
22
+ { title: 'With details drawer', html: `<div class="cn-app">${SIDEBAR('Projects', 'slim')}<main class="cn-app__main">${PAGE_HEADER('Q3 launch', '6 files', BTN('primary', 'Upload', 'md', ICON.plus), 'simple', `<div class="cn-page-header__crumbs">${CRUMBS}</div>`)}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table">${TABLE}</div></section></main><aside class="cn-app__drawer" aria-label="File details"><div class="cn-app__drawer-head"><div class="cn-row" data-gap="3">${fileIcon}<div class="cn-stack" data-gap="0"><span class="cn-text-heading-xs">Q3 roadmap.fig</span><span class="cn-text-body-sm cn-app__muted">Figma · 18.6 MB</span></div></div><button type="button" class="cn-close-button" data-size="sm" data-theme="light" aria-label="Close">${ICON.x.replace('cn-icon', 'cn-close-button__icon')}</button></div><div class="cn-app__drawer-body">${IMG('16 / 10')}${TABS(['Details', 'Activity', 'Versions'], 0)}<dl class="cn-app__dl"><div><dt>Owner</dt><dd><div class="cn-row" data-gap="2">${AVATAR('MC', 'Maya Chen', 'xs', 'accent')}<span>Maya Chen</span></div></dd></div><div><dt>Created</dt><dd>2 September 2026</dd></div><div><dt>Modified</dt><dd>Yesterday, 18:40</dd></div><div><dt>Shared with</dt><dd><div class="cn-avatar-group" data-size="sm">${AVATAR('DC', 'Daniel Costa', 'sm', 'success').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}${AVATAR('SA', 'Sofia Almeida', 'sm', 'warning').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}${AVATAR('LF', 'Lucas Ferreira', 'sm', 'info').replace('class="cn-avatar"', 'class="cn-avatar cn-avatar-group__item"')}<span class="cn-avatar-group__overflow">+5</span></div></dd></div><div><dt>Tags</dt><dd><div class="cn-row" data-gap="1">${BADGE('Design', 'accent')}${BADGE('Q3')}</div></dd></div></dl></div><div class="cn-app__drawer-foot">${BTN('outline', 'Move', 'md')}${BTN('primary', 'Download', 'md')}</div></aside></div>` },
23
+ ],
24
+ css: `${APP_CSS}
25
+ .cn-files__icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: {radius.lg}; background-color: {color.bg-subtle}; color: {color.fg-muted}; flex-shrink: 0; }
26
+ .cn-files__icon .cn-icon { width: 20px; height: 20px; }
27
+ .cn-files__preview { border-bottom: {border.width.thin} solid {color.border-default}; }
28
+ .cn-files__preview .cn-placeholder { border-radius: 0; }`,
29
+ viewport: 'desktop',
30
+ };
@@ -0,0 +1,31 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, ICON, APP_CSS, CARD, CARD_HEAD } from "./_app.mjs";
2
+ const STEP = (done, title, desc, action) => `<li class="cn-first-run__step" data-done="${done}"><span class="cn-first-run__check" aria-hidden="true">${done ? ICON.check : ''}</span><span class="cn-stack cn-flex-1" data-gap="0"><span class="cn-text-label-sm${done ? ' cn-first-run__done' : ''}">${title}</span><span class="cn-text-body-sm cn-app__muted">${desc}</span></span>${done ? '' : BTN('outline', action, 'sm')}</li>`;
3
+ const CHECKLIST = `<section class="cn-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Get started with Lumen', '2 of 5 steps done · about 8 minutes left', BTN('ghost', 'Dismiss', 'sm'))}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush"><div class="cn-progress" data-size="sm" data-tone="action" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" aria-label="Setup progress" style="padding:var(--cn-space-4) var(--cn-space-6) 0"><div class="cn-progress__track"><div class="cn-progress__indicator" style="width:40%"></div></div></div><ol class="cn-first-run__list">${STEP(true, 'Create your workspace', 'Done on 11 September', '')}${STEP(true, 'Invite your team', '2 people joined', '')}${STEP(false, 'Import your customers', 'CSV or connect your CRM in one click.', 'Import')}${STEP(false, 'Connect Slack', 'Get notified where your team already talks.', 'Connect')}${STEP(false, 'Create your first report', 'Pick a template to see your data in minutes.', 'Create')}</ol></div></section>`;
4
+ const EMPTY = (icon, title, text, actions) => `<div class="cn-empty-state" data-size="md" data-variant="default"><svg class="cn-empty-state__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">${icon}</svg><h3 class="cn-empty-state__title">${title}</h3><p class="cn-empty-state__description">${text}</p><div class="cn-empty-state__actions">${actions}</div></div>`;
5
+ export const firstRun = {
6
+ name: 'First run and empty states',
7
+ slug: 'first-run',
8
+ category: 'app-page',
9
+ description: 'What the app looks like before there is data: a dashboard with a setup checklist and empty widgets, an empty table with an import call to action, a no-results state and an error state.',
10
+ rules: [
11
+ 'A setup checklist card replaces the metrics on the first visit: title, progress ("2 of 5 steps"), a progress bar and steps with a check circle, title, one-line description and an outline action; done steps are struck through and lose their button.',
12
+ 'Empty widgets keep their card and header so the layout does not jump when data arrives; the body shows a small EmptyState (icon, one line, one link button).',
13
+ 'Empty table: keep the page header, filters and the card; the body shows an EmptyState with the primary action ("Import customers") and a secondary ("Add manually").',
14
+ 'No results: keep the search term visible in the description and offer "Clear search" (outline) before any primary action.',
15
+ 'Error state: featured icon error outline, plain language ("We couldn’t load your customers"), a retry button and a support link; never a stack trace.',
16
+ ],
17
+ html: `<div class="cn-app">${SIDEBAR('Dashboard')}<main class="cn-app__main">${PAGE_HEADER('Welcome to Lumen, Maya', 'Let’s get your workspace ready. You can come back to this any time.', BTN('primary', 'Import customers', 'md', ICON.plus))}${CHECKLIST}<div class="cn-grid" data-cols="3" data-gap="6">${CARD(CARD_HEAD('Revenue'), EMPTY('<path d="M2.5 13.5h11M4.5 11V7M8 11V4M11.5 11V8.5"/>', 'No revenue yet', 'Connect your billing provider to see revenue here.', BTN('link-color', 'Connect billing', 'sm')))}${CARD(CARD_HEAD('Customers'), EMPTY('<circle cx="8" cy="5.5" r="2.75"/><path d="M2.75 13.5c.6-2.6 2.6-4 5.25-4s4.65 1.4 5.25 4"/>', 'No customers yet', 'Import a CSV or add your first customer.', BTN('link-color', 'Import', 'sm')))}${CARD(CARD_HEAD('Activity'), EMPTY('<rect x="2.5" y="3.5" width="11" height="10" rx="1.5"/><path d="M2.5 7h11M5.5 2v3M10.5 2v3"/>', 'Nothing happened yet', 'Activity from your team will show up here.', ''))}</div></main></div>`,
18
+ variants: [
19
+ { title: 'Empty table with import CTA', html: `<div class="cn-app">${SIDEBAR('Users')}<main class="cn-app__main">${PAGE_HEADER('Customers', 'Everyone who bought from you, in one place.', `${BTN('outline', 'Add manually', 'md')}${BTN('primary', 'Import customers', 'md', ICON.plus)}`)}<div class="cn-row" data-gap="3"><div class="cn-input" data-variant="default" data-size="md" style="width:320px">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search" aria-label="Search customers" disabled></div>${BTN('outline', 'Filters', 'md', ICON.menu).replace('class="cn-button"', 'class="cn-button" disabled')}</div><section class="cn-card" data-variant="default" data-padding="none"><div class="cn-card__body" style="padding:var(--cn-space-16) var(--cn-space-6)"><div class="cn-empty-state" data-size="lg" data-variant="default"><span class="cn-featured-icon" data-theme="modern" data-tone="gray" data-size="lg" data-shape="square">${ICON.user.replace('cn-icon', 'cn-featured-icon__icon')}</span><h3 class="cn-empty-state__title">No customers yet</h3><p class="cn-empty-state__description">Import a CSV from your previous tool or connect your CRM. We’ll match fields automatically and you can review before anything is saved.</p><div class="cn-empty-state__actions">${BTN('outline', 'Add manually', 'md')}${BTN('primary', 'Import customers', 'md', ICON.plus)}</div></div></div></section></main></div>` },
20
+ { title: 'No results for a search', html: `<div class="cn-app">${SIDEBAR('Users')}<main class="cn-app__main">${PAGE_HEADER('Customers', '2,420 customers', BTN('primary', 'Add customer', 'md', ICON.plus))}<div class="cn-row" data-gap="3"><div class="cn-input" data-variant="default" data-size="md" style="width:320px">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" value="acme corporation" aria-label="Search customers"></div>${BTN('outline', 'Filters', 'md', ICON.menu)}<span class="cn-text-body-sm cn-app__muted">0 results</span></div><section class="cn-card" data-variant="default" data-padding="none"><div class="cn-card__body" style="padding:var(--cn-space-12) var(--cn-space-6)"><div class="cn-empty-state" data-size="md" data-variant="default"><span class="cn-featured-icon" data-theme="modern" data-tone="gray" data-size="lg" data-shape="square">${ICON.search.replace('cn-icon', 'cn-featured-icon__icon')}</span><h3 class="cn-empty-state__title">No results for “acme corporation”</h3><p class="cn-empty-state__description">Check the spelling or try a shorter term. You can also search by email, company or tag.</p><div class="cn-empty-state__actions">${BTN('outline', 'Clear search', 'md')}${BTN('primary', 'Add customer', 'md')}</div></div></div></section></main></div>` },
21
+ { title: 'Error state', html: `<div class="cn-app">${SIDEBAR('Users')}<main class="cn-app__main">${PAGE_HEADER('Customers', '', BTN('primary', 'Add customer', 'md', ICON.plus)).replace('<p class="cn-page-header__description"></p>', '')}<section class="cn-card" data-variant="default" data-padding="none"><div class="cn-card__body" style="padding:var(--cn-space-16) var(--cn-space-6)"><div class="cn-empty-state" data-size="md" data-variant="default"><span class="cn-featured-icon" data-theme="outline" data-tone="error" data-size="lg" data-shape="circle">${ICON.warning.replace('cn-icon', 'cn-featured-icon__icon')}</span><h3 class="cn-empty-state__title">We couldn’t load your customers</h3><p class="cn-empty-state__description">Something went wrong on our side. Your data is safe. Try again in a moment, or let us know if it keeps happening.</p><div class="cn-empty-state__actions">${BTN('outline', 'Contact support', 'md')}${BTN('primary', 'Try again', 'md')}</div></div></div></section></main></div>` },
22
+ ],
23
+ css: `${APP_CSS}
24
+ .cn-first-run__list { list-style: none; margin: 0; padding: {space.2} 0 0; display: flex; flex-direction: column; }
25
+ .cn-first-run__step { display: flex; align-items: center; gap: {space.4}; padding: {space.4} {space.6}; border-top: {border.width.thin} solid {color.border-subtle}; }
26
+ .cn-first-run__check { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: {radius.full}; border: {border.width.thin} solid {color.border-strong}; color: {color.fg-on-action}; flex-shrink: 0; }
27
+ .cn-first-run__step[data-done="true"] .cn-first-run__check { background-color: {color.bg-success}; border-color: {color.bg-success}; }
28
+ .cn-first-run__check .cn-icon { width: 14px; height: 14px; }
29
+ .cn-first-run__done { text-decoration: line-through; color: {color.fg-muted}; }`,
30
+ viewport: 'desktop',
31
+ };
@@ -0,0 +1,24 @@
1
+ export const formLayout = {
2
+ name: 'Form layout',
3
+ slug: 'form-layout',
4
+ category: 'app-section',
5
+ description: 'Vertical form: one column, labels above fields, 16px between fields, sections separated by a divider with a heading, actions at the end aligned right.',
6
+ rules: [
7
+ 'One column. Two columns only for short related pairs (city/state, first/last name) using .cn-form__row.',
8
+ 'Field order: label → control → helper or error. Helper and error never show at the same time.',
9
+ 'Required is the default; mark optional fields with "(optional)" in the label instead of asterisks.',
10
+ 'Gap between fields 16px (space.4); between sections 32px (space.8) with a section title (heading-sm) and optional description.',
11
+ 'Actions: right-aligned, ghost "Cancel" then primary. In a dialog they live in the dialog footer; in a page they are sticky at the bottom only when the form is long.',
12
+ 'Max width of a form column: 640px (size.container.sm).',
13
+ ],
14
+ html: `<form class="cn-form" style="max-width:520px"><section class="cn-form__section"><h3 class="cn-form__title">Empresa</h3><p class="cn-form__description">Como a Vera deve se apresentar.</p><div class="cn-field"><label class="cn-field__label" for="f1">Nome da empresa</label><div class="cn-input" data-variant="default" data-size="md"><input id="f1" class="cn-input__field" type="text" placeholder="Vero Finance"></div><p class="cn-field__helper">Como aparece na Receita Federal.</p></div><div class="cn-form__row"><div class="cn-field"><label class="cn-field__label" for="f2">Cidade</label><div class="cn-input" data-variant="default" data-size="md"><input id="f2" class="cn-input__field" type="text"></div></div><div class="cn-field"><label class="cn-field__label" for="f3">UF</label><div class="cn-input" data-variant="default" data-size="md"><input id="f3" class="cn-input__field" type="text" maxlength="2"></div></div></div></section><div class="cn-form__actions"><button type="button" class="cn-button" data-variant="ghost" data-size="md"><span class="cn-button__label">Cancelar</span></button><button type="submit" class="cn-button" data-variant="primary" data-size="md"><span class="cn-button__label">Salvar</span></button></div></form>`,
15
+ css: `
16
+ .cn-form { display: flex; flex-direction: column; gap: {space.8}; }
17
+ .cn-form__section { display: flex; flex-direction: column; gap: {space.4}; }
18
+ .cn-form__section + .cn-form__section { padding-top: {space.8}; border-top: {border.width.thin} solid {color.border-default}; }
19
+ .cn-form__title { font-family: {type.heading-sm.family}; font-size: {type.heading-sm.size}; font-weight: {type.heading-sm.weight}; line-height: {type.heading-sm.lineHeight}; letter-spacing: {type.heading-sm.letterSpacing}; }
20
+ .cn-form__description { font-size: {font.size.sm}; color: {color.fg-muted}; margin-top: calc({space.2} * -1); }
21
+ .cn-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: {space.4}; }
22
+ .cn-form__actions { display: flex; justify-content: flex-end; gap: {space.2}; padding-top: {space.2}; }
23
+ @media (max-width: {breakpoint.sm}) { .cn-form__row { grid-template-columns: 1fr; } }`,
24
+ };
@@ -0,0 +1,58 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, TABS, AVATAR, BADGE, ICON, APP_CSS } from "./_app.mjs";
2
+ const THREADS = [['MC', 'Maya Chen', 'Q3 roadmap review', 'Can we move the review to Thursday? I want to include the billing numbers…', '9:41', true, 'accent'], ['DC', 'Daniel Costa', 'Billing refactor merged', 'PR #482 is in. Migrations run on deploy; nothing to do on your side.', '8:20', false, 'success'], ['SA', 'Sofia Almeida', 'Brand assets v3', 'Uploaded the new logo set and the updated icon grid to the Brand folder.', 'Yesterday', false, 'warning'], ['LF', 'Lucas Ferreira', 'Drawer focus trap', 'Found the bug: focus escapes the drawer when tabbing from the last button…', 'Yesterday', true, 'info'], ['AK', 'Aisha Khan', 'Webhook retries', 'Retries now back off exponentially; alert threshold set to 5 failures.', 'Mon', false, 'accent'], ['NB', 'Noah Berg', 'Onboarding copy', 'Draft of the three onboarding screens attached. Tone check?', 'Mon', false, 'success']];
3
+ const thread = ([i, n, s, p, t, unread, tone], active = false) => `<a href="#" class="cn-inbox__thread${active ? ' cn-inbox__thread--active' : ''}"${unread ? ' data-unread' : ''}>${AVATAR(i, n, 'md', tone, 'online')}<span class="cn-inbox__thread-body"><span class="cn-row" data-justify="between" data-gap="2"><span class="cn-text-label-sm cn-inbox__name">${n}</span><span class="cn-text-body-xs cn-app__muted">${t}</span></span><span class="cn-text-label-sm cn-inbox__subject">${s}</span><span class="cn-text-body-sm cn-app__muted cn-inbox__preview">${p}</span></span></a>`;
4
+ const MSG = (i, n, tone, time, body) => `<article class="cn-inbox__msg"><header class="cn-row" data-gap="3">${AVATAR(i, n, 'md', tone)}<div class="cn-stack" data-gap="0"><span class="cn-text-label-sm">${n}</span><span class="cn-text-body-xs cn-app__muted">${time}</span></div></header><div class="cn-inbox__msg-body cn-text-body-md">${body}</div></article>`;
5
+ const READING = `<section class="cn-inbox__reading"><header class="cn-inbox__reading-head"><div class="cn-stack" data-gap="1"><h2 class="cn-text-heading-sm">Q3 roadmap review</h2><div class="cn-row" data-gap="2">${BADGE('Product', 'accent')}${BADGE('Needs reply', 'warning', 'soft', true)}</div></div><div class="cn-row" data-gap="1"><button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="Archive">${ICON.inbox.replace('cn-icon', 'cn-icon-button__icon')}</button><button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="Delete">${ICON.trash.replace('cn-icon', 'cn-icon-button__icon')}</button><button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="More">${ICON.dots.replace('cn-icon', 'cn-icon-button__icon')}</button></div></header><div class="cn-inbox__messages">${MSG('MC', 'Maya Chen', 'accent', 'Today, 9:41', '<p>Hi Daniel,</p><p>Can we move the roadmap review to Thursday? I want to include the billing numbers from the new dashboard and they only close on Wednesday night.</p><p>If Thursday doesn’t work, Friday morning is also fine for me.</p><p>Thanks,<br>Maya</p>')}${MSG('DC', 'Daniel Costa', 'success', 'Today, 10:02', '<p>Thursday works. I’ll update the invite and add Sofia so the brand assets are covered in the same session.</p>')}</div><footer class="cn-inbox__composer"><div class="cn-textarea" data-size="md" data-resize="none"><textarea class="cn-textarea__field" rows="3" placeholder="Reply to Maya…" aria-label="Reply"></textarea></div><div class="cn-row" data-justify="between"><div class="cn-row" data-gap="1"><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="Attach file">${ICON.copy.replace('cn-icon', 'cn-icon-button__icon')}</button><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="Insert link">${ICON.external.replace('cn-icon', 'cn-icon-button__icon')}</button></div><div class="cn-row" data-gap="2">${BTN('ghost', 'Save draft', 'sm')}${BTN('primary', 'Send', 'sm')}</div></div></footer></section>`;
6
+ const LIST = (active = 0) => `<section class="cn-inbox__list"><div class="cn-inbox__list-head"><div class="cn-row" data-justify="between"><h2 class="cn-text-heading-xs">Inbox <span class="cn-text-body-sm cn-app__muted">24</span></h2><button type="button" class="cn-icon-button" data-variant="ghost" data-size="sm" data-shape="square" aria-label="Compose">${ICON.plus.replace('cn-icon', 'cn-icon-button__icon')}</button></div><div class="cn-input" data-variant="default" data-size="sm">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search messages" aria-label="Search messages"></div>${TABS(['All', 'Unread', 'Flagged'], 0, 'pill')}</div><div class="cn-inbox__threads">${THREADS.map((t, i) => thread(t, i === active)).join('')}</div></section>`;
7
+ export const inboxPage = {
8
+ name: 'Inbox and messaging',
9
+ slug: 'inbox-page',
10
+ category: 'app-page',
11
+ description: 'Three-pane inbox (sidebar, thread list, reading pane with composer), a chat conversation layout, and a notification center. The list stays visible while reading.',
12
+ rules: [
13
+ 'Three panes: navigation sidebar (296), thread list (360, own scroll, search + filter tabs at the top), reading pane (flex 1) with a header (subject, badges, actions), the messages and a sticky composer.',
14
+ 'Thread row: avatar 40 with presence, name label-sm + time body-xs on one line, subject label-sm, one-line preview body-sm muted; unread rows show a brand dot and semibold name; the open thread has the subtle fill.',
15
+ 'Messages are separated by 24px; each has the sender row (avatar 40, name, time) and the body in body-md with 16px paragraph spacing.',
16
+ 'Composer: textarea (3 rows, no resize) + toolbar of ghost icon buttons on the left and Save draft / Send on the right; Send is the only primary button on the page.',
17
+ 'Chat variant: bubbles max 70% wide, incoming on the left in bg-subtle, outgoing on the right in the action color; a date divider chip between days.',
18
+ 'Notification center: a list grouped by day, each item with avatar, actor + action text, time, and an unread dot; "Mark all as read" as a link button in the header.',
19
+ ],
20
+ html: `<div class="cn-app">${SIDEBAR('Home')}${LIST(0)}${READING}</div>`,
21
+ variants: [
22
+ { title: 'Chat conversation', html: `<div class="cn-app">${SIDEBAR('Home', 'slim')}${LIST(3)}<section class="cn-inbox__reading"><header class="cn-inbox__reading-head"><div class="cn-row" data-gap="3">${AVATAR('LF', 'Lucas Ferreira', 'md', 'info', 'online')}<div class="cn-stack" data-gap="0"><span class="cn-text-heading-xs">Lucas Ferreira</span><span class="cn-text-body-sm cn-app__muted">Online · Frontend</span></div></div><div class="cn-row" data-gap="2">${BTN('outline', 'Call', 'sm')}${BTN('outline', 'Profile', 'sm')}</div></header><div class="cn-inbox__chat"><div class="cn-inbox__day"><span>Today</span></div>${[['in', 'Found the bug: focus escapes the drawer when tabbing from the last button. It jumps back to the page behind.', '10:12'], ['out', 'Nice catch. Is it only the users drawer or all of them?', '10:14'], ['in', 'All of them — the trap is on the wrapper but the footer buttons render outside it.', '10:15'], ['out', 'I’ll move the footer inside the trap and add a test. Give me 20 minutes.', '10:16'], ['in', 'Perfect. I’ll re-test on Safari too.', '10:16']].map(([d, t, time]) => `<div class="cn-inbox__bubble-row" data-direction="${d}">${d === 'in' ? AVATAR('LF', 'Lucas Ferreira', 'sm', 'info') : ''}<div class="cn-stack" data-gap="1" data-align="${d === 'in' ? 'start' : 'end'}"><div class="cn-inbox__bubble">${t}</div><span class="cn-text-body-xs cn-app__muted">${time}</span></div></div>`).join('')}<div class="cn-inbox__bubble-row" data-direction="in">${AVATAR('LF', 'Lucas Ferreira', 'sm', 'info')}<div class="cn-inbox__bubble cn-inbox__typing" aria-label="Lucas is typing"><span></span><span></span><span></span></div></div></div><footer class="cn-inbox__composer"><div class="cn-row" data-gap="2"><div class="cn-input cn-flex-1" data-variant="default" data-size="md"><input class="cn-input__field" type="text" placeholder="Message Lucas…" aria-label="Message"></div>${BTN('primary', 'Send', 'md')}</div></footer></section></div>` },
23
+ { title: 'Notification center', html: `<div class="cn-app">${SIDEBAR('Home')}<main class="cn-app__main">${PAGE_HEADER('Notifications', 'Everything that needs your attention, newest first.', `${BTN('link-color', 'Mark all as read', 'md')}${BTN('outline', 'Settings', 'md', ICON.settings)}`, 'simple')}${TABS(['All', 'Mentions', 'Assigned to you', 'Following'], 0)}<div class="cn-stack" data-gap="6" style="max-width:800px">${[['Today', [['MC', 'Maya Chen', 'mentioned you in', 'Q3 roadmap review', '9:41', true, 'accent'], ['DC', 'Daniel Costa', 'assigned you', 'Billing webhook retries', '8:20', true, 'success'], ['SA', 'Sofia Almeida', 'shared', 'Brand assets v3', '7:55', false, 'warning']]], ['Yesterday', [['LF', 'Lucas Ferreira', 'commented on', 'Drawer focus trap', '18:02', false, 'info'], ['AK', 'Aisha Khan', 'resolved', 'Webhook alert threshold', '16:40', false, 'accent'], ['NB', 'Noah Berg', 'requested review on', 'Onboarding copy', '11:15', false, 'success']]]].map(([day, items]) => `<section class="cn-stack" data-gap="2"><h2 class="cn-text-label-sm cn-app__muted">${day}</h2><div class="cn-card" data-variant="default" data-padding="none"><ul class="cn-list" data-density="default" data-variant="plain">${items.map(([i, n, a, tgt, t, unread, tone]) => `<li class="cn-list__item"${unread ? ' data-selected' : ''}><span class="cn-list__leading">${AVATAR(i, n, 'md', tone)}</span><span class="cn-list__content"><span class="cn-list__title"><strong>${n}</strong> ${a} <strong>${tgt}</strong></span><span class="cn-list__description">${t}</span></span><span class="cn-list__trailing">${unread ? '<span class="cn-inbox__dot" aria-label="Unread"></span>' : ''}${BTN('ghost', 'View', 'sm')}</span></li>`).join('')}</ul></div></section>`).join('')}</div></main></div>` },
24
+ ],
25
+ css: `${APP_CSS}
26
+ .cn-inbox__list { width: 360px; flex-shrink: 0; display: flex; flex-direction: column; border-inline-end: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; min-height: 720px; }
27
+ .cn-inbox__list-head { display: flex; flex-direction: column; gap: {space.3}; padding: {space.5} {space.4} {space.3}; border-bottom: {border.width.thin} solid {color.border-default}; }
28
+ .cn-inbox__threads { display: flex; flex-direction: column; overflow: auto; }
29
+ .cn-inbox__thread { display: flex; gap: {space.3}; padding: {space.4}; border-bottom: {border.width.thin} solid {color.border-subtle}; text-decoration: none; color: inherit; }
30
+ .cn-inbox__thread:hover { background-color: {color.bg-subtle}; }
31
+ .cn-inbox__thread--active { background-color: {color.bg-subtle}; box-shadow: inset 2px 0 0 {color.bg-action}; }
32
+ .cn-inbox__thread[data-unread] .cn-inbox__name { font-weight: {font.weight.semibold}; }
33
+ .cn-inbox__thread[data-unread] .cn-inbox__name::after { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: {radius.full}; background-color: {color.bg-action}; margin-inline-start: {space.2}; vertical-align: middle; }
34
+ .cn-inbox__thread-body { display: flex; flex-direction: column; gap: {space.0.5}; min-width: 0; flex: 1 1 auto; }
35
+ .cn-inbox__subject { color: {color.fg-default}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
36
+ .cn-inbox__preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
+ .cn-inbox__reading { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; background-color: {color.bg-surface}; }
38
+ .cn-inbox__reading-head { display: flex; justify-content: space-between; align-items: flex-start; gap: {space.4}; padding: {space.5} {space.6}; border-bottom: {border.width.thin} solid {color.border-default}; }
39
+ .cn-inbox__messages { display: flex; flex-direction: column; gap: {space.6}; padding: {space.6}; flex: 1 1 auto; overflow: auto; }
40
+ .cn-inbox__msg { display: flex; flex-direction: column; gap: {space.3}; }
41
+ .cn-inbox__msg-body { padding-inline-start: 52px; color: {color.fg-muted}; display: flex; flex-direction: column; gap: {space.4}; }
42
+ .cn-inbox__msg-body p { margin: 0; }
43
+ .cn-inbox__composer { display: flex; flex-direction: column; gap: {space.3}; padding: {space.4} {space.6}; border-top: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; }
44
+ .cn-inbox__chat { display: flex; flex-direction: column; gap: {space.4}; padding: {space.6}; flex: 1 1 auto; }
45
+ .cn-inbox__day { display: flex; justify-content: center; }
46
+ .cn-inbox__day span { font-size: {font.size.xs}; font-weight: {font.weight.medium}; color: {color.fg-subtle}; background-color: {color.bg-subtle}; border-radius: {radius.full}; padding: {space.1} {space.3}; }
47
+ .cn-inbox__bubble-row { display: flex; gap: {space.3}; align-items: flex-end; }
48
+ .cn-inbox__bubble-row[data-direction="out"] { justify-content: flex-end; }
49
+ .cn-inbox__bubble { max-width: 70%; padding: {space.2.5} {space.3.5}; border-radius: {radius.lg}; background-color: {color.bg-subtle}; color: {color.fg-default}; font-size: {font.size.md}; line-height: {font.lineHeight.normal}; border-start-start-radius: {radius.xs}; }
50
+ .cn-inbox__bubble-row[data-direction="out"] .cn-inbox__bubble { background-color: {color.bg-action}; color: {color.fg-on-action}; border-start-start-radius: {radius.lg}; border-start-end-radius: {radius.xs}; }
51
+ .cn-inbox__typing { display: inline-flex; gap: 4px; padding: {space.3} {space.3.5}; }
52
+ .cn-inbox__typing span { width: 6px; height: 6px; border-radius: {radius.full}; background-color: {color.fg-subtle}; animation: cn-inbox-typing 1.2s infinite ease-in-out; }
53
+ .cn-inbox__typing span:nth-child(2) { animation-delay: .2s; } .cn-inbox__typing span:nth-child(3) { animation-delay: .4s; }
54
+ @keyframes cn-inbox-typing { 0%, 60%, 100% { transform: translateY(0); opacity: .5; } 30% { transform: translateY(-3px); opacity: 1; } }
55
+ .cn-inbox__dot { display: inline-block; width: 8px; height: 8px; border-radius: {radius.full}; background-color: {color.bg-action}; margin-inline-end: {space.2}; }
56
+ @media (max-width: {breakpoint.lg}) { .cn-inbox__list { width: 100%; border: 0; } .cn-inbox__reading { display: none; } }`,
57
+ viewport: 'desktop',
58
+ };
@@ -0,0 +1,29 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { dirname, extname, join } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ export const PATTERN_ORDER = ['app-layout', 'app-section', 'app-page', 'marketing-section', 'marketing-page', 'shared-page', 'email'];
5
+ let cache = null;
6
+ /** Discover source or compiled modules exporting Pattern objects. */
7
+ export async function loadPatterns() {
8
+ if (cache)
9
+ return cache;
10
+ const dir = dirname(fileURLToPath(import.meta.url));
11
+ const extension = extname(fileURLToPath(import.meta.url));
12
+ const files = readdirSync(dir).filter((f) => f.endsWith(extension) && !f.startsWith('_') && f !== `index${extension}`).sort();
13
+ const out = [];
14
+ for (const f of files) {
15
+ const mod = await import(pathToFileURL(join(dir, f)).href);
16
+ for (const v of Object.values(mod))
17
+ if (v && typeof v === 'object' && 'slug' in v && 'html' in v)
18
+ out.push(v);
19
+ }
20
+ out.sort(comparePatterns);
21
+ cache = out;
22
+ return out;
23
+ }
24
+ export function comparePatterns(a, b) {
25
+ const ca = PATTERN_ORDER.indexOf(a.category), cb = PATTERN_ORDER.indexOf(b.category);
26
+ if (ca !== cb)
27
+ return ca - cb;
28
+ return a.slug.localeCompare(b.slug);
29
+ }
@@ -0,0 +1,24 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, TEAM_TABLE, PAGINATION, BADGE, CARD_HEAD, ICON, APP_CSS, AVATAR, TABS, PEOPLE } from "./_app.mjs";
2
+ const FILTERS = `<div class="cn-row" data-justify="between" data-gap="3" data-wrap><div class="cn-row" data-gap="3">${TABS(['View all', 'Active', 'Invited', 'Offline'], 0, 'pill')}</div><div class="cn-row" data-gap="3"><div class="cn-input" data-variant="default" data-size="md" style="width:320px">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search" aria-label="Search members"></div>${BTN('outline', 'Filters', 'md', ICON.menu)}</div></div>`;
3
+ const DRAWER = `<aside class="cn-app__drawer" aria-label="Member details"><div class="cn-app__drawer-head"><div class="cn-row" data-gap="4">${AVATAR('DC', 'Daniel Costa', 'xl', 'success', 'online')}<div class="cn-stack" data-gap="1"><span class="cn-text-heading-sm">Daniel Costa</span><span class="cn-text-body-md cn-app__muted">daniel@lumen.co</span><div class="cn-row" data-gap="1">${BADGE('Active', 'success', 'soft', true)}${BADGE('Engineering', 'info')}</div></div></div><button type="button" class="cn-close-button" data-size="sm" data-theme="light" aria-label="Close">${ICON.x.replace('cn-icon', 'cn-close-button__icon')}</button></div><div class="cn-app__drawer-body">${TABS(['Details', 'Activity', 'Permissions'], 0)}<dl class="cn-app__dl"><div><dt>Role</dt><dd>Engineering Manager</dd></div><div><dt>Teams</dt><dd>Platform, Billing, Design systems</dd></div><div><dt>Location</dt><dd>Lisbon, Portugal · GMT+1</dd></div><div><dt>Joined</dt><dd>12 March 2024</dd></div><div><dt>Last active</dt><dd>2 minutes ago</dd></div></dl><div class="cn-alert" data-tone="warning" data-variant="soft" data-layout="inline" role="status"><span class="cn-alert__icon" aria-hidden="true">${ICON.warning}</span><div class="cn-alert__content"><div class="cn-alert__title">Two-factor authentication is off</div><p class="cn-alert__description">Ask Daniel to enable it in Settings → Security.</p></div></div></div><div class="cn-app__drawer-foot">${BTN('outline', 'Remove from team', 'md')}${BTN('primary', 'Edit member', 'md')}</div></aside>`;
4
+ export const listDetailPage = {
5
+ name: 'List with detail drawer',
6
+ slug: 'list-detail-page',
7
+ category: 'app-page',
8
+ description: 'The workhorse app page: a filterable table of records with a detail drawer that opens over the list instead of navigating away. Also as a card list and as a kanban board.',
9
+ rules: [
10
+ 'Filters row above the table: view tabs (pill) on the left, search (320px) and a Filters button on the right.',
11
+ 'The table lives in a card with its header (title + count badge + actions) and a pagination footer.',
12
+ 'Selecting a row opens the 440px drawer on the right: header with avatar/title/status and a close button, tabs, a definition list, contextual alerts, and footer actions. The list stays visible and the selected row keeps its highlight.',
13
+ 'Never navigate to a separate page for one record; only deep edits open a dialog or a full page.',
14
+ 'Empty results show the EmptyState inside the table card, keeping the filters visible.',
15
+ ],
16
+ html: `<div class="cn-app">${SIDEBAR('Users')}<main class="cn-app__main">${PAGE_HEADER('Users', 'Manage the people in your workspace and their permissions.', `${BTN('outline', 'Import', 'md', ICON.copy)}${BTN('primary', 'Invite user', 'md', ICON.plus)}`)}${FILTERS}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('All users', '', '', BDG())}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table">${TEAM_TABLE()}</div><footer class="cn-card__footer">${PAGINATION}</footer></section></main>${DRAWER}</div>`,
17
+ variants: [
18
+ { title: 'Card list with empty state', html: `<div class="cn-app">${SIDEBAR('Projects')}<main class="cn-app__main">${PAGE_HEADER('Integrations', 'Connect the tools your team already uses.', BTN('primary', 'Request integration', 'md'))}${FILTERS}<div class="cn-grid" data-cols="3" data-gap="6">${[['Slack', 'Send notifications to channels.', true], ['GitHub', 'Link pull requests to tasks.', true], ['Figma', 'Embed live design files.', false], ['Linear', 'Sync issues both ways.', false], ['Notion', 'Attach docs to projects.', true], ['Zapier', 'Automate anything else.', false]].map(([n, d, on]) => `<section class="cn-card" data-variant="default" data-padding="md"><div class="cn-card__body cn-stack" data-gap="4"><div class="cn-row" data-justify="between" data-align="start"><span class="cn-featured-icon" data-theme="modern" data-tone="gray" data-size="md" data-shape="square">${ICON.copy.replace('cn-icon', 'cn-featured-icon__icon')}</span><label class="cn-switch" data-size="sm"><input class="cn-switch__input" type="checkbox" role="switch"${on ? ' checked' : ''} aria-label="Enable ${n}"><span class="cn-switch__track" aria-hidden="true"><span class="cn-switch__thumb"></span></span></label></div><div class="cn-stack" data-gap="1"><span class="cn-text-heading-xs">${n}</span><span class="cn-text-body-sm cn-app__muted">${d}</span></div></div><footer class="cn-card__footer">${BTN('link-color', 'View integration', 'sm')}</footer></section>`).join('')}</div><section class="cn-card" data-variant="default" data-padding="none"><div class="cn-card__body"><div class="cn-empty-state" data-size="md" data-variant="default"><svg class="cn-empty-state__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg><h3 class="cn-empty-state__title">No integrations found</h3><p class="cn-empty-state__description">Your search “calendar” did not match any integration. Try another term or request one.</p><div class="cn-empty-state__actions">${BTN('outline', 'Clear search', 'md')}${BTN('primary', 'Request integration', 'md')}</div></div></div></section></main></div>` },
19
+ { title: 'Kanban board', html: `<div class="cn-app">${SIDEBAR('Tasks')}<main class="cn-app__main">${PAGE_HEADER('Tasks', 'What the team is working on this sprint.', `${BTN('outline', 'Filters', 'md', ICON.menu)}${BTN('primary', 'New task', 'md', ICON.plus)}`)}<div class="cn-kanban" data-density="default">${[['To do', 'you', 4], ['In progress', 'agent', 3], ['In review', 'action', 2], ['Done', 'you', 5]].map(([col, owner, n], ci) => `<div class="cn-kanban__column" data-owner="${owner}"><div class="cn-kanban__column-header"><span class="cn-kanban__column-title">${col}</span><span class="cn-kanban__column-count">${n}</span></div><div class="cn-kanban__column-body">${Array.from({ length: Math.min(3, n) }, (_, i) => `<article class="cn-kanban__card"><span class="cn-kanban__card-title">${['Design settings page', 'Fix billing webhook', 'Write onboarding copy', 'QA mobile drawer', 'Ship dark mode'][(ci + i) % 5]}</span><div class="cn-kanban__card-meta">${BADGE(['Design', 'Bug', 'Content', 'QA', 'Feature'][(ci + i) % 5], ['accent', 'danger', 'info', 'warning', 'success'][(ci + i) % 5])}<span>Sep ${12 + i}</span></div><div class="cn-kanban__card-footer">${AVATAR(PEOPLE[(ci + i) % 6][0], PEOPLE[(ci + i) % 6][1], 'xs', PEOPLE[(ci + i) % 6][4])}<span class="cn-text-body-xs cn-app__muted">${2 + i} comments</span></div></article>`).join('')}</div></div>`).join('')}</div></main></div>` },
20
+ ],
21
+ css: APP_CSS,
22
+ viewport: 'desktop',
23
+ };
24
+ function BDG() { return BADGE('240 users', 'accent'); }
@@ -0,0 +1,46 @@
1
+ import { BTN, AVATAR, BADGE, ICON, APP_CSS, STAT, FIELD, INPUT } from "./_app.mjs";
2
+ const PHONE = (title, body, tab = 0, back = false, action = '') => `<div class="cn-phone"><header class="cn-phone__bar">${back ? `<button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="Back">${ICON.chevronRight.replace('cn-icon', 'cn-icon-button__icon').replace('<svg', '<svg style="transform:rotate(180deg)"')}</button>` : `<span class="cn-phone__logo" aria-hidden="true"></span>`}<span class="cn-text-label-md cn-phone__title">${title}</span>${action || `<button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="Notifications">${ICON.inbox.replace('cn-icon', 'cn-icon-button__icon')}</button>`}</header><main class="cn-phone__body">${body}</main><nav class="cn-phone__tabs" aria-label="Main">${[['Home', ICON.home], ['Tasks', ICON.check], ['Inbox', ICON.inbox], ['Profile', ICON.user]].map(([l, i], k) => `<a href="#" class="cn-phone__tab"${k === tab ? ' aria-current="page"' : ''}>${i}<span>${l}</span></a>`).join('')}</nav></div>`;
3
+ const ITEM = (i, n, tone, t, d, badge = '') => `<li class="cn-list__item"><span class="cn-list__leading">${AVATAR(i, n, 'md', tone)}</span><span class="cn-list__content"><span class="cn-list__title">${t}</span><span class="cn-list__description">${d}</span></span><span class="cn-list__trailing">${badge}${ICON.chevronRight.replace('cn-icon', 'cn-list__chevron')}</span></li>`;
4
+ export const mobileApp = {
5
+ name: 'Phone app screens',
6
+ slug: 'mobile-app',
7
+ category: 'app-page',
8
+ description: 'The application at 375px: top bar + content + bottom tab bar. Home with stats and a list, a searchable list with filter chips, a detail screen with a sticky action, a form, and a bottom sheet.',
9
+ rules: [
10
+ 'Shell: 56px top bar (back or logo, centered title, one icon action), scrolling content with 16px side padding, 64px bottom tab bar with 4–5 items (icon 24 + label 11px); the current tab is brand-colored.',
11
+ 'Controls are lg (44px) on phones; primary actions sit in a sticky footer above the tab bar, full width.',
12
+ 'Lists are edge-to-edge rows (56px) with avatar, title, one-line description and a chevron; cards only for grouped content.',
13
+ 'Filters are horizontal scrolling chips under the search field; never a sidebar or a dropdown filter panel.',
14
+ 'Detail screens open as pushed pages (back arrow), quick edits as bottom sheets (drawer side=bottom) with a grab handle.',
15
+ 'Everything here is the same components as desktop with data-size="lg"; no separate mobile component set.',
16
+ ],
17
+ html: PHONE('Home', `<div class="cn-stack" data-gap="5" style="padding:var(--cn-space-4)"><div class="cn-stack" data-gap="1"><span class="cn-text-body-sm cn-app__muted">Good morning</span><h1 class="cn-text-heading-md">Maya Chen</h1></div><div class="cn-grid" data-cols="2" data-gap="3" data-keep>${STAT('Open tasks', '9', '−2', 'down', 'This week')}${STAT('Waiting on you', '3', '+1', 'up', 'Reviews')}</div><div class="cn-row" data-justify="between"><h2 class="cn-text-heading-xs">Needs your attention</h2>${BTN('link-color', 'See all', 'sm')}</div></div><ul class="cn-list" data-density="default" data-variant="plain">${ITEM('DC', 'Daniel Costa', 'success', 'Approve billing refactor', 'PR #482 · waiting 2h', BADGE('Review', 'warning'))}${ITEM('SA', 'Sofia Almeida', 'warning', 'Brand assets v3', 'Shared 3 files', BADGE('New', 'info'))}${ITEM('LF', 'Lucas Ferreira', 'info', 'Drawer focus trap', 'Assigned to you', BADGE('Bug', 'danger'))}${ITEM('NB', 'Noah Berg', 'success', 'Onboarding copy', 'Comment · 1h', '')}</ul>`, 0),
18
+ variants: [
19
+ { title: 'Searchable list with filter chips', html: PHONE('Users', `<div class="cn-stack" data-gap="3" style="padding:var(--cn-space-4) var(--cn-space-4) 0"><div class="cn-input" data-variant="default" data-size="lg">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search people" aria-label="Search people"></div><div class="cn-phone__chips">${['All', 'Active', 'Admins', 'Invited', 'Offline'].map((c, i) => `<button type="button" class="cn-tag" data-tone="${i === 1 ? 'accent' : 'neutral'}" data-variant="outline" data-size="md" aria-pressed="${i === 1}"><span class="cn-tag__label">${c}</span></button>`).join('')}</div><span class="cn-text-body-sm cn-app__muted">198 active</span></div><ul class="cn-list" data-density="default" data-variant="plain">${[['MC', 'Maya Chen', 'accent', 'Product Manager'], ['DC', 'Daniel Costa', 'success', 'Engineering Manager'], ['SA', 'Sofia Almeida', 'warning', 'Design'], ['LF', 'Lucas Ferreira', 'info', 'Frontend'], ['AK', 'Aisha Khan', 'accent', 'Backend'], ['NB', 'Noah Berg', 'success', 'Product Design'], ['ER', 'Elena Rossi', 'warning', 'UX Design']].map(([i, n, tone, r]) => ITEM(i, n, tone, n, r, BADGE('Active', 'success', 'soft', true))).join('')}</ul>`, 3) },
20
+ { title: 'Detail with sticky action', html: PHONE('Task', `<div class="cn-stack" data-gap="5" style="padding:var(--cn-space-4)"><div class="cn-row" data-gap="2">${BADGE('Bug', 'danger')}${BADGE('In progress', 'accent', 'soft', true)}</div><h1 class="cn-text-heading-md">Drawer focus trap escapes on last button</h1><p class="cn-text-body-md cn-app__muted">Focus leaves the drawer when tabbing from the footer buttons because they render outside the trap wrapper. Reproduces on Safari and Chrome.</p><dl class="cn-app__dl"><div><dt>Assignee</dt><dd><div class="cn-row" data-gap="2">${AVATAR('LF', 'Lucas Ferreira', 'xs', 'info')}<span>Lucas Ferreira</span></div></dd></div><div><dt>Due</dt><dd>Fri, 12 Sep</dd></div><div><dt>Project</dt><dd>Design system</dd></div></dl><div class="cn-stack" data-gap="2"><h2 class="cn-text-heading-xs">Activity</h2><ul class="cn-list" data-density="compact" data-variant="plain">${[['LF', 'Lucas Ferreira', 'info', 'moved to In progress', '10:16'], ['MC', 'Maya Chen', 'accent', 'commented: “Nice catch.”', '10:14']].map(([i, n, tone, a, t]) => `<li class="cn-list__item"><span class="cn-list__leading">${AVATAR(i, n, 'sm', tone)}</span><span class="cn-list__content"><span class="cn-list__title">${n}</span><span class="cn-list__description">${a}</span></span><span class="cn-list__trailing">${t}</span></li>`).join('')}</ul></div></div><footer class="cn-phone__sticky">${BTN('outline', 'Comment', 'lg')}${BTN('primary', 'Mark done', 'lg')}</footer>`, 1, true, `<button type="button" class="cn-icon-button" data-variant="ghost" data-size="md" data-shape="square" aria-label="More">${ICON.dots.replace('cn-icon', 'cn-icon-button__icon')}</button>`) },
21
+ { title: 'Form', html: PHONE('New task', `<form class="cn-stack" data-gap="5" style="padding:var(--cn-space-4)">${FIELD('m-title', 'Title', INPUT('m-title', '', 'text', 'What needs to be done?').replace('data-size="md"', 'data-size="lg"'))}${FIELD('m-desc', 'Description', `<div class="cn-textarea" data-size="lg" data-resize="none"><textarea class="cn-textarea__field" id="m-desc" rows="4" placeholder="Add details…"></textarea></div>`)}${FIELD('m-proj', 'Project', `<div class="cn-select" data-variant="default" data-size="lg"><select class="cn-select__field" id="m-proj" aria-label="Project"><option>Design system</option><option>Billing platform</option><option>Website redesign</option></select><svg class="cn-select__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></div>`)}${FIELD('m-due', 'Due date', INPUT('m-due', '', 'date').replace('data-size="md"', 'data-size="lg"'))}<label class="cn-switch" data-size="md"><input class="cn-switch__input" type="checkbox" role="switch" checked><span class="cn-switch__track" aria-hidden="true"><span class="cn-switch__thumb"></span></span><span class="cn-switch__label">Notify assignee</span></label></form><footer class="cn-phone__sticky">${BTN('primary', 'Create task', 'lg')}</footer>`, 1, true, '<span style="width:44px"></span>') },
22
+ { title: 'Bottom sheet over a list', html: `<div class="cn-phone cn-phone--sheet">${PHONE('Users', `<ul class="cn-list" data-density="default" data-variant="plain">${[['MC', 'Maya Chen', 'accent', 'Product Manager'], ['DC', 'Daniel Costa', 'success', 'Engineering Manager'], ['SA', 'Sofia Almeida', 'warning', 'Design'], ['LF', 'Lucas Ferreira', 'info', 'Frontend'], ['AK', 'Aisha Khan', 'accent', 'Backend']].map(([i, n, tone, r]) => ITEM(i, n, tone, n, r)).join('')}</ul>`, 3).replace('class="cn-phone"', 'class="cn-phone__inner"')}<div class="cn-phone__scrim"></div><div class="cn-phone__sheet" role="dialog" aria-modal="true" aria-label="Daniel Costa"><span class="cn-phone__handle" aria-hidden="true"></span><div class="cn-row" data-gap="3" style="padding:var(--cn-space-2) var(--cn-space-4) var(--cn-space-4)">${AVATAR('DC', 'Daniel Costa', 'lg', 'success', 'online')}<div class="cn-stack" data-gap="0"><span class="cn-text-heading-xs">Daniel Costa</span><span class="cn-text-body-sm cn-app__muted">Engineering Manager · Online</span></div></div><ul class="cn-list" data-density="default" data-variant="plain">${[['Message', ICON.inbox], ['Call', ICON.user], ['View profile', ICON.external], ['Remove from team', ICON.trash]].map(([l, i], k) => `<li class="cn-list__item${k === 3 ? ' cn-phone__danger' : ''}"><span class="cn-list__leading">${i}</span><span class="cn-list__content"><span class="cn-list__title">${l}</span></span></li>`).join('')}</ul><div style="padding:var(--cn-space-4)">${BTN('outline', 'Cancel', 'lg').replace('class="cn-button"', 'class="cn-button" style="width:100%"')}</div></div></div>` },
23
+ ],
24
+ css: `${APP_CSS}
25
+ .cn-phone { width: 375px; max-width: 100%; min-height: 720px; margin-inline: auto; background-color: {color.bg-surface}; display: flex; flex-direction: column; position: relative; overflow: hidden; border-inline: {border.width.thin} solid {color.border-default}; }
26
+ .cn-phone__bar { display: flex; align-items: center; gap: {space.2}; height: 56px; padding-inline: {space.3}; border-bottom: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; position: sticky; top: 0; z-index: {z.sticky}; }
27
+ .cn-phone__title { flex: 1 1 auto; text-align: center; }
28
+ .cn-phone__logo { width: 28px; height: 28px; border-radius: {radius.md}; background-color: {color.bg-action}; margin-inline: {space.2}; }
29
+ .cn-phone__body { flex: 1 1 auto; overflow: auto; display: flex; flex-direction: column; background-color: {color.bg-canvas}; }
30
+ .cn-phone__body .cn-list { background-color: {color.bg-surface}; }
31
+ .cn-phone__tabs { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; height: 64px; border-top: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; }
32
+ .cn-phone__tab { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; font-size: 11px; font-weight: {font.weight.medium}; color: {color.fg-subtle}; text-decoration: none; }
33
+ .cn-phone__tab .cn-icon { width: 24px; height: 24px; }
34
+ .cn-phone__tab[aria-current="page"] { color: {color.fg-action}; }
35
+ .cn-phone__chips { display: flex; gap: {space.2}; overflow-x: auto; padding-bottom: {space.1}; scrollbar-width: none; }
36
+ .cn-phone__sticky { position: sticky; bottom: 0; display: flex; gap: {space.3}; padding: {space.3} {space.4}; background-color: {color.bg-surface}; border-top: {border.width.thin} solid {color.border-default}; margin-top: auto; }
37
+ .cn-phone__sticky .cn-button { flex: 1 1 0; }
38
+ .cn-phone--sheet { background: none; border: 0; }
39
+ .cn-phone__inner { width: 100%; min-height: 720px; background-color: {color.bg-surface}; display: flex; flex-direction: column; border-inline: {border.width.thin} solid {color.border-default}; }
40
+ .cn-phone__scrim { position: absolute; inset: 0; background-color: {color.bg-overlay}; }
41
+ .cn-phone__sheet { position: absolute; left: 0; right: 0; bottom: 0; background-color: {color.bg-surface-raised}; border-start-start-radius: {radius.2xl}; border-start-end-radius: {radius.2xl}; box-shadow: {shadow.xl}; padding-top: {space.2}; }
42
+ .cn-phone__handle { display: block; width: 40px; height: 4px; border-radius: {radius.full}; background-color: {color.bg-muted}; margin: 0 auto {space.2}; }
43
+ .cn-phone__danger .cn-list__title, .cn-phone__danger .cn-list__leading { color: {color.fg-danger}; }
44
+ .cn-phone .cn-list__leading .cn-icon { width: 20px; height: 20px; color: {color.fg-muted}; }`,
45
+ viewport: 'mobile',
46
+ };
@@ -0,0 +1,32 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, ICON, APP_CSS, FIELD, INPUT, TEAM_TABLE, CARD_HEAD, BADGE, AVATAR } from "./_app.mjs";
2
+ const PAGE = `<div class="cn-app cn-modal-flow__page" aria-hidden="true">${SIDEBAR('Users')}<main class="cn-app__main">${PAGE_HEADER('Users', 'Manage the people in your workspace.', BTN('primary', 'Invite user', 'md', ICON.plus))}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('All users', '', '', BADGE('240 users', 'accent'))}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table">${TEAM_TABLE()}</div></section></main></div>`;
3
+ const OVERLAY = (panel, align = 'center') => `<div class="cn-modal-flow">${PAGE}<div class="cn-modal-flow__backdrop" data-align="${align}">${panel}</div></div>`;
4
+ const DIALOG = (size, variant, icon, tone, title, desc, body, footer) => `<div class="cn-dialog" data-size="${size}" data-variant="${variant}" data-state="open" role="dialog" aria-modal="true" aria-labelledby="mf-title"><div class="cn-dialog__header"><div class="cn-stack" data-gap="4" data-align="start"><span class="cn-featured-icon" data-theme="outline" data-tone="${tone}" data-size="md" data-shape="circle">${icon.replace('cn-icon', 'cn-featured-icon__icon')}</span><div class="cn-stack" data-gap="1"><h2 class="cn-dialog__title" id="mf-title">${title}</h2><p class="cn-dialog__description">${desc}</p></div></div><button type="button" class="cn-dialog__close cn-close-button" data-size="md" data-theme="light" aria-label="Close">${ICON.x.replace('cn-icon', 'cn-close-button__icon')}</button></div>${body ? `<div class="cn-dialog__body">${body}</div>` : ''}<div class="cn-dialog__footer">${footer}</div></div>`;
5
+ export const modalFlows = {
6
+ name: 'Dialog and drawer flows',
7
+ slug: 'modal-flows',
8
+ category: 'app-page',
9
+ description: 'Overlays in context: a destructive confirmation, a form dialog, a two-step dialog and an edit drawer, each rendered over the users page with the scrim.',
10
+ rules: [
11
+ 'Confirmation dialogs (sm, 400px): outline featured icon (error for destructive), title heading-md, one sentence, footer with ghost Cancel + primary (danger for destructive); the destructive button names the action ("Delete user"), never "OK".',
12
+ 'Form dialogs (md, 480px): title + description, 1–4 fields (20px apart), footer Cancel + primary; longer forms use a drawer instead.',
13
+ 'Multi-step dialogs show a stepper or "Step 1 of 2" in the header and keep the same size across steps.',
14
+ 'Drawers (md, 480px) slide from the right for editing a record; the list stays visible behind the scrim.',
15
+ 'Focus is trapped, Escape closes, focus returns to the trigger; the page behind is aria-hidden.',
16
+ ],
17
+ html: OVERLAY(DIALOG('sm', 'destructive', ICON.trash, 'error', 'Delete user', 'Are you sure you want to delete Daniel Costa? This action cannot be undone.', '', `${BTN('outline', 'Cancel', 'lg')}${BTN('danger', 'Delete user', 'lg')}`)),
18
+ variants: [
19
+ { title: 'Form dialog (invite)', html: OVERLAY(DIALOG('md', 'default', ICON.user, 'brand', 'Invite team member', 'They’ll receive an email with a link to join Lumen.', `<form class="cn-stack" data-gap="5">${FIELD('mf-email', 'Email', INPUT('mf-email', '', 'email', 'name@company.com', ICON.inbox))}${FIELD('mf-role', 'Role', `<div class="cn-select" data-variant="default" data-size="md"><select class="cn-select__field" id="mf-role" aria-label="Role"><option>Member</option><option selected>Admin</option><option>Viewer</option></select><svg class="cn-select__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></div>`, 'Admins can manage billing and members.')}<label class="cn-checkbox" data-size="md"><input class="cn-checkbox__input" type="checkbox" checked><span class="cn-checkbox__control" aria-hidden="true"><svg class="cn-checkbox__indicator" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8.5l3 3 7-7"/></svg></span><span class="cn-checkbox__label">Send a welcome message</span></label></form>`, `${BTN('outline', 'Cancel', 'lg')}${BTN('primary', 'Send invite', 'lg')}`)) },
20
+ { title: 'Two-step dialog (export)', html: OVERLAY(DIALOG('md', 'default', ICON.external, 'gray', 'Export users', 'Step 1 of 2 · Choose what to include.', `<div class="cn-stack" data-gap="3">${[['All users', '240 people, every column', true], ['Active users only', '198 people', false], ['Current selection', '1 person', false]].map(([t, d, on]) => `<label class="cn-radio cn-modal-flow__option" data-size="md"><input class="cn-radio__input" type="radio" name="mf-scope"${on ? ' checked' : ''}><span class="cn-radio__control" aria-hidden="true"><span class="cn-radio__indicator"></span></span><span class="cn-stack" data-gap="0"><span class="cn-radio__label">${t}</span><span class="cn-radio__description">${d}</span></span></label>`).join('')}</div>`, `${BTN('outline', 'Cancel', 'lg')}${BTN('primary', 'Continue', 'lg')}`)) },
21
+ { title: 'Edit drawer over the list', html: OVERLAY(`<aside class="cn-drawer" data-side="right" data-size="md" data-state="open" role="dialog" aria-modal="true" aria-labelledby="mf-drawer-title"><div class="cn-drawer__header"><div class="cn-row" data-gap="3">${AVATAR('DC', 'Daniel Costa', 'lg', 'success')}<div class="cn-stack" data-gap="1"><h2 class="cn-drawer__title" id="mf-drawer-title">Edit member</h2><p class="cn-drawer__description">daniel@lumen.co</p></div></div><button type="button" class="cn-drawer__close cn-close-button" data-size="md" data-theme="light" aria-label="Close">${ICON.x.replace('cn-icon', 'cn-close-button__icon')}</button></div><div class="cn-drawer__body"><form class="cn-stack" data-gap="5">${FIELD('mf-name', 'Full name', INPUT('mf-name', 'Daniel Costa'))}${FIELD('mf-title', 'Title', INPUT('mf-title', 'Engineering Manager'))}${FIELD('mf-team', 'Teams', `<div class="cn-row" data-gap="2" data-wrap>${['Platform', 'Billing', 'Design systems'].map((t) => `<button type="button" class="cn-tag" data-tone="neutral" data-variant="outline" data-size="md" aria-pressed="true"><span class="cn-tag__label">${t}</span><span class="cn-tag__remove" role="button" aria-label="Remove ${t}">${ICON.x}</span></button>`).join('')}</div>`)}${FIELD('mf-access', 'Access', `<div class="cn-stack" data-gap="3">${['Can manage billing', 'Can invite members', 'Can delete projects'].map((l, i) => `<label class="cn-switch" data-size="sm"><input class="cn-switch__input" type="checkbox" role="switch"${i < 2 ? ' checked' : ''}><span class="cn-switch__track" aria-hidden="true"><span class="cn-switch__thumb"></span></span><span class="cn-switch__label">${l}</span></label>`).join('')}</div>`)}</form></div><div class="cn-drawer__footer">${BTN('danger-ghost', 'Remove member', 'md')}<span class="cn-flex-1"></span>${BTN('outline', 'Cancel', 'md')}${BTN('primary', 'Save changes', 'md')}</div></aside>`, 'end') },
22
+ ],
23
+ css: `${APP_CSS}
24
+ .cn-modal-flow { position: relative; min-height: 720px; overflow: hidden; }
25
+ .cn-modal-flow__page { pointer-events: none; }
26
+ .cn-modal-flow__backdrop { position: absolute; inset: 0; background-color: {color.bg-overlay}; display: flex; align-items: center; justify-content: center; padding: {space.6}; z-index: {z.modal}; }
27
+ .cn-modal-flow__backdrop[data-align="end"] { justify-content: flex-end; padding: 0; }
28
+ .cn-modal-flow__backdrop .cn-drawer { height: 100%; }
29
+ .cn-modal-flow__option { padding: {space.4}; border: {border.width.thin} solid {color.border-default}; border-radius: {radius.card}; }
30
+ .cn-modal-flow__option:has(:checked) { border-color: {color.border-action}; background-color: {color.bg-action-subtle}; }`,
31
+ viewport: 'desktop',
32
+ };