@conduction/docusaurus-preset 3.28.0 → 3.32.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 (33) hide show
  1. package/MISSING_COMPONENTS.md +6 -2
  2. package/package.json +4 -4
  3. package/src/components/AppMock/AppMock.jsx +16 -0
  4. package/src/components/AppMock/variants/AppVersionsMock.jsx +83 -0
  5. package/src/components/AppMock/variants/DoriathMock.jsx +79 -0
  6. package/src/components/AppMock/variants/HermiqMock.jsx +71 -0
  7. package/src/components/AppMock/variants/HrmqMock.jsx +77 -0
  8. package/src/components/AppMock/variants/PlanixMock.jsx +80 -0
  9. package/src/components/AppMock/variants/PortaliqMock.jsx +63 -0
  10. package/src/components/AppMock/variants/ScholiqMock.jsx +74 -0
  11. package/src/components/AppMock/variants/ShillinqMock.jsx +90 -0
  12. package/src/components/BuildMock/BuildMock.jsx +101 -0
  13. package/src/components/BuildMock/BuildMock.module.css +290 -0
  14. package/src/components/CookieCli/CookieCli.jsx +197 -24
  15. package/src/components/CookieCli/CookieCli.module.css +139 -1
  16. package/src/components/CookieCli/shell.js +161 -0
  17. package/src/components/CookieCli/spaceInvaders.js +359 -0
  18. package/src/components/CookieCli/spaceInvaders.test.js +112 -0
  19. package/src/components/DetailHero/DetailHero.jsx +68 -16
  20. package/src/components/DetailHero/DetailHero.module.css +31 -1
  21. package/src/components/FlowMock/FlowMock.jsx +151 -0
  22. package/src/components/FlowMock/FlowMock.module.css +164 -0
  23. package/src/components/KanbanMock/KanbanMock.jsx +119 -0
  24. package/src/components/KanbanMock/KanbanMock.module.css +233 -0
  25. package/src/components/LeafMock/LeafMock.jsx +173 -0
  26. package/src/components/LeafMock/LeafMock.module.css +237 -0
  27. package/src/components/RotatingCards/RotatingCards.module.css +5 -2
  28. package/src/components/Showcase/Showcase.module.css +5 -2
  29. package/src/components/WidgetShelf/WidgetShelf.jsx +52 -13
  30. package/src/components/WidgetShelf/WidgetShelf.module.css +84 -5
  31. package/src/components/index.js +4 -0
  32. package/src/components/primitives/SectionHead.module.css +6 -0
  33. package/src/data/app-glyphs.json +4 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * <LeafMock />
3
+ *
4
+ * Token-built abstract of one workspace integration leaf: the small
5
+ * panels that show *what an app plugs into* — contacts, the meetings
6
+ * calendar, mail, files. Used on Showcase items and app detail pages
7
+ * that previously reused a generic <AppMock> for every leaf.
8
+ *
9
+ * Same abstraction level as <AppMock> (honeycomb.io/technologies/*):
10
+ * greeked content, tokens only, no real text beyond short labels.
11
+ * Four variants:
12
+ *
13
+ * - contacts: address-book rows with pointy-top hex avatars and a
14
+ * mint sync arrow to a phone silhouette
15
+ * - calendar: mini month grid with one highlighted day and a
16
+ * linked-event chip (lavender bar)
17
+ * - mail: message list with one highlighted row carrying a
18
+ * mint "create from email" action chip
19
+ * - files: folder/file rows with a link-to-record chip (forest)
20
+ *
21
+ * Usage:
22
+ *
23
+ * <LeafMock leaf="contacts" />
24
+ * <LeafMock leaf="calendar" caption size="sm" />
25
+ *
26
+ * Props:
27
+ * - leaf: 'contacts' | 'calendar' | 'mail' | 'files' (required)
28
+ * - size: 'sm' | 'md' (default) — frame width
29
+ * - caption: string | true — small label below the frame;
30
+ * `true` uses the variant's default label
31
+ * - className: string
32
+ */
33
+
34
+ import React from 'react';
35
+ import styles from './LeafMock.module.css';
36
+
37
+ function Greek({w, tone = 'sub'}) {
38
+ return <span className={[styles.greek, styles[`greek-${tone}`]].join(' ')} style={{width: w}} />;
39
+ }
40
+
41
+ function ContactsLeaf() {
42
+ return (
43
+ <div className={styles.contacts}>
44
+ <div className={styles.contactList}>
45
+ {[0, 1, 2].map((i) => (
46
+ <div key={i} className={styles.contactRow}>
47
+ <span className={[styles.hexAvatar, styles[`hexAvatar-${i}`]].join(' ')} />
48
+ <span className={styles.contactLines}>
49
+ <Greek w="64%" tone="title" />
50
+ <Greek w="44%" />
51
+ </span>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ <svg className={styles.syncArrow} viewBox="0 0 24 24" aria-hidden="true">
56
+ <path d="M4 9h13m0 0l-3.5-3.5M17 9l-3.5 3.5M20 15H7m0 0l3.5-3.5M7 15l3.5 3.5" />
57
+ </svg>
58
+ <div className={styles.phone}>
59
+ <div className={styles.phoneScreen}>
60
+ <span className={styles.phoneRow} />
61
+ <span className={styles.phoneRow} />
62
+ <span className={styles.phoneRow} />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function CalendarLeaf() {
70
+ /* 7×4 mini month; day 10 (index 9) is the highlighted, linked day. */
71
+ const days = Array.from({length: 28}, (_, i) => i);
72
+ return (
73
+ <div className={styles.calendar}>
74
+ <div className={styles.monthGrid}>
75
+ {days.map((i) => (
76
+ <span key={i} className={[styles.day, i === 9 && styles.dayActive].filter(Boolean).join(' ')} />
77
+ ))}
78
+ </div>
79
+ <div className={styles.eventChip}>
80
+ <span className={styles.eventBar} />
81
+ <span className={styles.eventLines}>
82
+ <Greek w="70%" tone="title" />
83
+ <Greek w="46%" />
84
+ </span>
85
+ <svg className={styles.linkIcon} viewBox="0 0 24 24" aria-hidden="true">
86
+ <path d="M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6L12.5 17.5" />
87
+ </svg>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function MailLeaf() {
94
+ return (
95
+ <div className={styles.mail}>
96
+ {[0, 1, 2].map((i) => (
97
+ <div key={i} className={[styles.mailRow, i === 1 && styles.mailRowActive].filter(Boolean).join(' ')}>
98
+ <span className={styles.mailDot} />
99
+ <span className={styles.mailLines}>
100
+ <Greek w="58%" tone="title" />
101
+ <Greek w="82%" />
102
+ </span>
103
+ {i === 1 && (
104
+ <span className={styles.actionChip}>
105
+ <svg className={styles.plusIcon} viewBox="0 0 24 24" aria-hidden="true">
106
+ <path d="M12 5v14M5 12h14" />
107
+ </svg>
108
+ <Greek w="26px" tone="chip" />
109
+ </span>
110
+ )}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ function FilesLeaf() {
118
+ return (
119
+ <div className={styles.files}>
120
+ {[0, 1, 2].map((i) => (
121
+ <div key={i} className={styles.fileRow}>
122
+ <span className={[styles.fileIcon, i === 0 && styles.folderIcon].filter(Boolean).join(' ')} />
123
+ <span className={styles.fileLines}>
124
+ <Greek w={i === 1 ? '72%' : '54%'} tone="title" />
125
+ </span>
126
+ {i === 1 && (
127
+ <span className={[styles.actionChip, styles.recordChip].join(' ')}>
128
+ <svg className={styles.linkIcon} viewBox="0 0 24 24" aria-hidden="true">
129
+ <path d="M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6L12.5 17.5" />
130
+ </svg>
131
+ <Greek w="26px" tone="chip" />
132
+ </span>
133
+ )}
134
+ </div>
135
+ ))}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ const VARIANTS = {
141
+ contacts: {Component: ContactsLeaf, label: 'Contacts leaf'},
142
+ calendar: {Component: CalendarLeaf, label: 'Calendar leaf'},
143
+ mail: {Component: MailLeaf, label: 'Mail leaf'},
144
+ files: {Component: FilesLeaf, label: 'Files leaf'},
145
+ };
146
+
147
+ export default function LeafMock({leaf, size = 'md', caption, className}) {
148
+ const variant = VARIANTS[leaf];
149
+ if (!variant) {
150
+ return (
151
+ <div className={styles.lm}>
152
+ <div className={[styles.frame, styles[`size-${size}`], className].filter(Boolean).join(' ')}>
153
+ <div className={styles.empty}>Unknown leaf: {leaf}</div>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+ const {Component, label} = variant;
159
+ return (
160
+ <div className={styles.lm}>
161
+ <figure className={[styles.figure, className].filter(Boolean).join(' ')}>
162
+ <div className={[styles.frame, styles[`size-${size}`]].filter(Boolean).join(' ')}>
163
+ <Component />
164
+ </div>
165
+ {caption && (
166
+ <figcaption className={styles.caption}>
167
+ {caption === true ? label : caption}
168
+ </figcaption>
169
+ )}
170
+ </figure>
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * <LeafMock /> styles. Scoped under `.lm` (display: contents), the
3
+ * same convention as AppMock's `.am`, so the file also works as a
4
+ * plain global stylesheet for static specimen pages.
5
+ *
6
+ * Tokens only. Accent discipline per leaf: contacts sync = mint,
7
+ * calendar event bar = lavender, mail action chip = mint, files
8
+ * record chip = forest. No orange anywhere in this component.
9
+ */
10
+
11
+ .lm { display: contents; }
12
+
13
+ .lm .figure {
14
+ margin: 0;
15
+ display: inline-flex;
16
+ flex-direction: column;
17
+ gap: var(--space-2);
18
+ width: 100%;
19
+ max-width: 100%;
20
+ }
21
+
22
+ .lm .frame {
23
+ width: 100%;
24
+ max-width: 360px;
25
+ aspect-ratio: 4 / 3;
26
+ background: white;
27
+ border: 1px solid var(--c-cobalt-100);
28
+ border-radius: var(--radius-lg);
29
+ box-shadow: var(--shadow-3);
30
+ overflow: hidden;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ padding: var(--space-4);
35
+ font-family: var(--conduction-typography-font-family-body);
36
+ }
37
+ .lm .size-sm { max-width: 240px; }
38
+ .lm .size-md { max-width: 360px; }
39
+
40
+ .lm .caption {
41
+ font-family: var(--conduction-typography-font-family-code);
42
+ font-size: 11px;
43
+ letter-spacing: 0.06em;
44
+ color: var(--c-cobalt-400);
45
+ text-transform: uppercase;
46
+ text-align: center;
47
+ }
48
+
49
+ .lm .empty {
50
+ color: var(--c-cobalt-400);
51
+ font-size: 13px;
52
+ }
53
+
54
+ /* ---- Greeked lines ---- */
55
+
56
+ .lm .greek { display: block; height: 3px; border-radius: 1px; background: var(--c-cobalt-200); }
57
+ .lm .greek-title { height: 4px; background: var(--c-cobalt-700); }
58
+ .lm .greek-chip { height: 3px; background: currentColor; opacity: 0.8; }
59
+
60
+ /* ---- Contacts leaf ---- */
61
+
62
+ .lm .contacts {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: var(--space-3);
66
+ width: 100%;
67
+ }
68
+ .lm .contactList {
69
+ flex: 1;
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 10px;
73
+ min-width: 0;
74
+ }
75
+ .lm .contactRow { display: flex; align-items: center; gap: 8px; }
76
+ .lm .hexAvatar {
77
+ width: 20px;
78
+ height: 23px;
79
+ clip-path: var(--hex-pointy-top);
80
+ background: var(--c-cobalt-300);
81
+ flex-shrink: 0;
82
+ }
83
+ .lm .hexAvatar-1 { background: var(--c-lavender-300); }
84
+ .lm .hexAvatar-2 { background: var(--c-cobalt-200); }
85
+ .lm .contactLines { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
86
+
87
+ .lm .syncArrow {
88
+ width: 22px;
89
+ height: 22px;
90
+ flex-shrink: 0;
91
+ fill: none;
92
+ stroke: var(--c-mint-500);
93
+ stroke-width: 2;
94
+ stroke-linecap: round;
95
+ stroke-linejoin: round;
96
+ }
97
+
98
+ .lm .phone {
99
+ width: 44px;
100
+ height: 82px;
101
+ border: 2px solid var(--c-cobalt-300);
102
+ border-radius: 9px;
103
+ padding: 6px 5px;
104
+ flex-shrink: 0;
105
+ }
106
+ .lm .phoneScreen {
107
+ height: 100%;
108
+ border-radius: 4px;
109
+ background: var(--c-cobalt-50);
110
+ padding: 6px 4px;
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 5px;
114
+ }
115
+ .lm .phoneRow { display: block; height: 3px; border-radius: 1px; background: var(--c-cobalt-200); }
116
+
117
+ /* ---- Calendar leaf ---- */
118
+
119
+ .lm .calendar {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: var(--space-3);
123
+ width: 100%;
124
+ max-width: 220px;
125
+ }
126
+ .lm .monthGrid {
127
+ display: grid;
128
+ grid-template-columns: repeat(7, 1fr);
129
+ gap: 5px;
130
+ }
131
+ .lm .day {
132
+ aspect-ratio: 1;
133
+ border-radius: 3px;
134
+ background: var(--c-cobalt-50);
135
+ border: 1px solid var(--c-cobalt-100);
136
+ }
137
+ .lm .dayActive { background: var(--c-lavender-300); border-color: var(--c-lavender-500); }
138
+
139
+ .lm .eventChip {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 8px;
143
+ background: white;
144
+ border: 1px solid var(--c-cobalt-100);
145
+ border-radius: var(--radius-sm);
146
+ box-shadow: var(--shadow-1);
147
+ padding: 7px 9px;
148
+ }
149
+ .lm .eventBar {
150
+ width: 4px;
151
+ align-self: stretch;
152
+ border-radius: 2px;
153
+ background: var(--c-lavender-500);
154
+ flex-shrink: 0;
155
+ }
156
+ .lm .eventLines { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
157
+ .lm .linkIcon {
158
+ width: 13px;
159
+ height: 13px;
160
+ flex-shrink: 0;
161
+ fill: none;
162
+ stroke: currentColor;
163
+ stroke-width: 2;
164
+ stroke-linecap: round;
165
+ stroke-linejoin: round;
166
+ color: var(--c-cobalt-400);
167
+ }
168
+
169
+ /* ---- Mail leaf ---- */
170
+
171
+ .lm .mail {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 8px;
175
+ width: 100%;
176
+ }
177
+ .lm .mailRow {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 8px;
181
+ padding: 7px 9px;
182
+ border-radius: var(--radius-sm);
183
+ border: 1px solid transparent;
184
+ }
185
+ .lm .mailRowActive { background: var(--c-cobalt-50); border-color: var(--c-cobalt-100); }
186
+ .lm .mailDot {
187
+ width: 16px;
188
+ height: 16px;
189
+ border-radius: 50%;
190
+ background: var(--c-cobalt-200);
191
+ flex-shrink: 0;
192
+ }
193
+ .lm .mailLines { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
194
+
195
+ .lm .actionChip {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 5px;
199
+ padding: 4px 8px;
200
+ border-radius: var(--radius-pill);
201
+ background: var(--c-mint-500);
202
+ color: white;
203
+ flex-shrink: 0;
204
+ }
205
+ .lm .plusIcon {
206
+ width: 10px;
207
+ height: 10px;
208
+ fill: none;
209
+ stroke: currentColor;
210
+ stroke-width: 2.4;
211
+ stroke-linecap: round;
212
+ }
213
+ .lm .actionChip .linkIcon { color: white; width: 10px; height: 10px; }
214
+
215
+ /* ---- Files leaf ---- */
216
+
217
+ .lm .files {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: 10px;
221
+ width: 100%;
222
+ }
223
+ .lm .fileRow { display: flex; align-items: center; gap: 8px; }
224
+ .lm .fileIcon {
225
+ width: 16px;
226
+ height: 19px;
227
+ border-radius: 2px;
228
+ background: var(--c-cobalt-200);
229
+ flex-shrink: 0;
230
+ }
231
+ .lm .folderIcon {
232
+ height: 14px;
233
+ background: var(--c-cobalt-300);
234
+ border-radius: 2px 4px 2px 2px;
235
+ }
236
+ .lm .fileLines { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
237
+ .lm .recordChip { background: var(--c-forest-500); }
@@ -16,7 +16,9 @@
16
16
  }
17
17
 
18
18
  /* ---------- Header ---------- */
19
- .head { max-width: 70ch; margin: 0 0 var(--space-8); }
19
+ /* Full-width head: only the lede keeps the 70ch reading measure, so
20
+ the title fills the line instead of balance-wrapping early. */
21
+ .head { margin: 0 0 var(--space-8); }
20
22
  .eyebrow {
21
23
  display: inline-flex; align-items: center; gap: var(--space-2);
22
24
  font-family: var(--conduction-typography-font-family-code);
@@ -28,8 +30,9 @@
28
30
  .title {
29
31
  font-size: 40px; font-weight: 700; letter-spacing: -0.02em;
30
32
  line-height: 1.1; color: var(--c-cobalt-900); margin: 0 0 var(--space-3);
33
+ text-wrap: pretty; /* run full width; no artificial two-line balance */
31
34
  }
32
- .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; }
35
+ .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; max-width: 70ch; }
33
36
 
34
37
  /* ---------- The stack ---------- */
35
38
  .stack {
@@ -12,7 +12,9 @@
12
12
  }
13
13
 
14
14
  /* ---------- Header ---------- */
15
- .head { max-width: 70ch; margin: 0 0 var(--space-8); }
15
+ /* Full-width head: only the lede keeps the 70ch reading measure, so
16
+ the title fills the line instead of balance-wrapping early. */
17
+ .head { margin: 0 0 var(--space-8); }
16
18
  .eyebrow {
17
19
  display: inline-flex; align-items: center; gap: var(--space-2);
18
20
  font-family: var(--conduction-typography-font-family-code);
@@ -24,8 +26,9 @@
24
26
  .title {
25
27
  font-size: 40px; font-weight: 700; letter-spacing: -0.02em;
26
28
  line-height: 1.1; color: var(--c-cobalt-900); margin: 0 0 var(--space-3);
29
+ text-wrap: pretty; /* run full width; no artificial two-line balance */
27
30
  }
28
- .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; }
31
+ .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; max-width: 70ch; }
29
32
 
30
33
  /* ---------- Body ---------- */
31
34
  .body { display: grid; gap: var(--space-8); align-items: stretch; }
@@ -27,9 +27,19 @@
27
27
  * ]}
28
28
  * />
29
29
  *
30
- * Layout: 2 or 3 columns depending on widget count, responsive
31
- * to viewport. Each card has the panel (preview) at the top,
32
- * title + description below.
30
+ * Layout: by default a single-row carousel that auto-scrolls slowly
31
+ * (marquee-style, seamless loop via a duplicated track), pausing on
32
+ * hover and on keyboard focus. The duplicate copy is aria-hidden so
33
+ * assistive tech reads each widget once; the first copy keeps normal
34
+ * semantics. `prefers-reduced-motion: reduce` collapses the carousel
35
+ * to the static wrapping grid, which is also what `carousel={false}`
36
+ * renders: 2 or 3 columns depending on widget count, responsive to
37
+ * viewport. Each card has the panel (preview) at the top, title +
38
+ * description below.
39
+ *
40
+ * Props (beyond eyebrow/title/lede/widgets/columns/className):
41
+ * - carousel: boolean (default true) — false renders the static grid
42
+ * - speed: number — seconds per full carousel loop (default 45)
33
43
  */
34
44
 
35
45
  import React from 'react';
@@ -41,11 +51,20 @@ export default function WidgetShelf({
41
51
  lede,
42
52
  widgets = [],
43
53
  columns,
54
+ carousel = true,
55
+ speed = 45,
44
56
  className,
45
57
  }) {
46
58
  const cols = columns || (widgets.length >= 4 ? 3 : Math.min(widgets.length, 3));
59
+ const cards = widgets.map((w, i) => (
60
+ <article key={i} className={styles.card}>
61
+ <div className={styles.panel}>{w.panel}</div>
62
+ {w.title && <h3 className={styles.cardTitle}>{w.title}</h3>}
63
+ {w.desc && <p className={styles.cardDesc}>{w.desc}</p>}
64
+ </article>
65
+ ));
47
66
  return (
48
- <section className={[styles.shelf, className].filter(Boolean).join(' ')}>
67
+ <section className={[styles.shelf, carousel && styles.carousel, className].filter(Boolean).join(' ')}>
49
68
  {(eyebrow || title || lede) && (
50
69
  <header className={styles.head}>
51
70
  {eyebrow && <div className={styles.eyebrow}><span className={styles.h}></span>{eyebrow}</div>}
@@ -53,15 +72,35 @@ export default function WidgetShelf({
53
72
  {lede && <p className={styles.lede}>{lede}</p>}
54
73
  </header>
55
74
  )}
56
- <div className={[styles.grid, styles[`cols-${cols}`]].join(' ')}>
57
- {widgets.map((w, i) => (
58
- <article key={i} className={styles.card}>
59
- <div className={styles.panel}>{w.panel}</div>
60
- {w.title && <h3 className={styles.cardTitle}>{w.title}</h3>}
61
- {w.desc && <p className={styles.cardDesc}>{w.desc}</p>}
62
- </article>
63
- ))}
64
- </div>
75
+ {carousel ? (
76
+ <div className={styles.viewport}>
77
+ <div
78
+ className={styles.track}
79
+ style={{'--ws-speed': `${speed}s`}}
80
+ >
81
+ <div className={[styles.group, styles[`cols-${cols}`]].join(' ')}>
82
+ {cards}
83
+ </div>
84
+ {/* Seamless-loop duplicate. Hidden from assistive tech so
85
+ each widget is announced once; under reduced motion the
86
+ CSS removes it entirely and the first group becomes the
87
+ static grid. */}
88
+ <div className={[styles.group, styles.dup].join(' ')} aria-hidden="true">
89
+ {widgets.map((w, i) => (
90
+ <article key={i} className={styles.card}>
91
+ <div className={styles.panel}>{w.panel}</div>
92
+ {w.title && <h3 className={styles.cardTitle}>{w.title}</h3>}
93
+ {w.desc && <p className={styles.cardDesc}>{w.desc}</p>}
94
+ </article>
95
+ ))}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ ) : (
100
+ <div className={[styles.grid, styles[`cols-${cols}`]].join(' ')}>
101
+ {cards}
102
+ </div>
103
+ )}
65
104
  </section>
66
105
  );
67
106
  }
@@ -1,7 +1,12 @@
1
1
  /**
2
- * <WidgetShelf /> styles. Header + responsive grid of widget-preview
3
- * cards. Each card has a tinted panel containing the widget mock,
4
- * with title + description below.
2
+ * <WidgetShelf /> styles. Header + widget-preview cards. Each card
3
+ * has a tinted panel containing the widget mock, with title +
4
+ * description below.
5
+ *
6
+ * Two layouts: the default single-row carousel (slow marquee, track
7
+ * duplicated for a seamless loop, paused on hover/focus) and the
8
+ * static responsive grid (carousel={false}, and what
9
+ * prefers-reduced-motion collapses the carousel to).
5
10
  */
6
11
 
7
12
  .shelf {
@@ -11,7 +16,11 @@
11
16
  font-family: var(--conduction-typography-font-family-body);
12
17
  }
13
18
 
14
- .head { max-width: 70ch; margin: 0 0 var(--space-8); }
19
+ /* The head runs full width so the title can fill the line; only the
20
+ lede (body text) keeps the 70ch reading measure. Capping the whole
21
+ head at 70ch forced titles into an early wrap, which the base
22
+ h1-h6 `text-wrap: balance` then split into two even lines. */
23
+ .head { margin: 0 0 var(--space-8); }
15
24
  .eyebrow {
16
25
  display: inline-flex; align-items: center; gap: var(--space-2);
17
26
  font-family: var(--conduction-typography-font-family-code);
@@ -23,8 +32,9 @@
23
32
  .title {
24
33
  font-size: 36px; font-weight: 700; letter-spacing: -0.02em;
25
34
  line-height: 1.1; color: var(--c-cobalt-900); margin: 0 0 var(--space-3);
35
+ text-wrap: pretty; /* run full width; no artificial two-line balance */
26
36
  }
27
- .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; }
37
+ .lede { font-size: 17px; line-height: 1.5; color: var(--c-cobalt-700); margin: 0; max-width: 70ch; }
28
38
 
29
39
  .grid {
30
40
  display: grid;
@@ -34,6 +44,75 @@
34
44
  .cols-3 { grid-template-columns: repeat(3, 1fr); }
35
45
  @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
36
46
 
47
+ /* ---- Carousel (default) ----
48
+ *
49
+ * Marquee mechanics: the track holds two identical groups of cards
50
+ * and translates from 0 to -50% on a slow linear loop; because each
51
+ * group carries a trailing padding equal to the card gap, -50% of
52
+ * the track lands exactly one group later and the loop is seamless.
53
+ * The duplicate group is aria-hidden in the JSX so screen readers
54
+ * hear each widget once. Hover or keyboard focus anywhere in the
55
+ * viewport pauses the animation. */
56
+
57
+ .viewport {
58
+ overflow: hidden;
59
+ /* Soft fade at both edges so cards enter/exit gently. */
60
+ -webkit-mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
61
+ mask-image: linear-gradient(to right, transparent, black 48px, black calc(100% - 48px), transparent);
62
+ }
63
+
64
+ .track {
65
+ display: flex;
66
+ width: max-content;
67
+ animation: wsMarquee var(--ws-speed, 45s) linear infinite;
68
+ }
69
+
70
+ .viewport:hover .track,
71
+ .viewport:focus-within .track {
72
+ animation-play-state: paused;
73
+ }
74
+
75
+ .group {
76
+ display: flex;
77
+ gap: var(--space-5);
78
+ padding-right: var(--space-5);
79
+ }
80
+
81
+ .carousel .card {
82
+ width: 340px;
83
+ flex: 0 0 auto;
84
+ }
85
+ @media (max-width: 700px) { .carousel .card { width: 280px; } }
86
+
87
+ @keyframes wsMarquee {
88
+ from { transform: translateX(0); }
89
+ to { transform: translateX(-50%); }
90
+ }
91
+
92
+ /* Reduced motion: drop the duplicate copy and render the first group
93
+ as the static wrapping grid — identical to carousel={false}. The
94
+ .cols-2 / .cols-3 class on the group supplies the column template. */
95
+ @media (prefers-reduced-motion: reduce) {
96
+ .viewport {
97
+ overflow: visible;
98
+ -webkit-mask-image: none;
99
+ mask-image: none;
100
+ }
101
+ .track { animation: none; display: block; width: 100%; }
102
+ .group {
103
+ display: grid;
104
+ gap: var(--space-5);
105
+ padding-right: 0;
106
+ width: 100%;
107
+ }
108
+ /* After .group so its display wins the equal-specificity contest. */
109
+ .group.dup { display: none; }
110
+ .carousel .card { width: auto; }
111
+ }
112
+ @media (prefers-reduced-motion: reduce) and (max-width: 900px) {
113
+ .group { grid-template-columns: 1fr; }
114
+ }
115
+
37
116
  .card {
38
117
  background: white;
39
118
  border: 1px solid var(--c-cobalt-100);
@@ -77,6 +77,10 @@ export {default as ComposeBlock} from './ComposeBlock/ComposeBlock.jsx';
77
77
  export {default as AppsGrid} from './AppsGrid/AppsGrid.jsx';
78
78
  export {default as AppMock} from './AppMock/AppMock.jsx';
79
79
  export {default as WidgetMock} from './WidgetMock/WidgetMock.jsx';
80
+ export {default as FlowMock} from './FlowMock/FlowMock.jsx';
81
+ export {default as LeafMock} from './LeafMock/LeafMock.jsx';
82
+ export {default as BuildMock} from './BuildMock/BuildMock.jsx';
83
+ export {default as KanbanMock} from './KanbanMock/KanbanMock.jsx';
80
84
  export {default as SidebarMock} from './SidebarMock/SidebarMock.jsx';
81
85
  export {default as MockScene} from './MockScene/MockScene.jsx';
82
86
  export {default as IntegrationIcon} from './IntegrationIcon/IntegrationIcon.jsx';
@@ -27,6 +27,12 @@
27
27
  letter-spacing: -0.02em;
28
28
  color: var(--c-cobalt-900);
29
29
  margin: 0;
30
+ /* Section titles run the full line before wrapping. The kit's base
31
+ h1-h6 rule sets `text-wrap: balance`, which splits a title into
32
+ two near-equal lines even when it fits comfortably on one; the
33
+ PO decision is that section titles fill the available width.
34
+ `pretty` keeps orphan control without the artificial balance. */
35
+ text-wrap: pretty;
30
36
  }
31
37
  @media (max-width: 700px) { .title { font-size: 32px; } }
32
38