@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.
- package/MISSING_COMPONENTS.md +6 -2
- package/package.json +4 -4
- package/src/components/AppMock/AppMock.jsx +16 -0
- package/src/components/AppMock/variants/AppVersionsMock.jsx +83 -0
- package/src/components/AppMock/variants/DoriathMock.jsx +79 -0
- package/src/components/AppMock/variants/HermiqMock.jsx +71 -0
- package/src/components/AppMock/variants/HrmqMock.jsx +77 -0
- package/src/components/AppMock/variants/PlanixMock.jsx +80 -0
- package/src/components/AppMock/variants/PortaliqMock.jsx +63 -0
- package/src/components/AppMock/variants/ScholiqMock.jsx +74 -0
- package/src/components/AppMock/variants/ShillinqMock.jsx +90 -0
- package/src/components/BuildMock/BuildMock.jsx +101 -0
- package/src/components/BuildMock/BuildMock.module.css +290 -0
- package/src/components/CookieCli/CookieCli.jsx +197 -24
- package/src/components/CookieCli/CookieCli.module.css +139 -1
- package/src/components/CookieCli/shell.js +161 -0
- package/src/components/CookieCli/spaceInvaders.js +359 -0
- package/src/components/CookieCli/spaceInvaders.test.js +112 -0
- package/src/components/DetailHero/DetailHero.jsx +68 -16
- package/src/components/DetailHero/DetailHero.module.css +31 -1
- package/src/components/FlowMock/FlowMock.jsx +151 -0
- package/src/components/FlowMock/FlowMock.module.css +164 -0
- package/src/components/KanbanMock/KanbanMock.jsx +119 -0
- package/src/components/KanbanMock/KanbanMock.module.css +233 -0
- package/src/components/LeafMock/LeafMock.jsx +173 -0
- package/src/components/LeafMock/LeafMock.module.css +237 -0
- package/src/components/RotatingCards/RotatingCards.module.css +5 -2
- package/src/components/Showcase/Showcase.module.css +5 -2
- package/src/components/WidgetShelf/WidgetShelf.jsx +52 -13
- package/src/components/WidgetShelf/WidgetShelf.module.css +84 -5
- package/src/components/index.js +4 -0
- package/src/components/primitives/SectionHead.module.css +6 -0
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
57
|
-
{
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 +
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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);
|
package/src/components/index.js
CHANGED
|
@@ -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
|
|