@conduction/docusaurus-preset 3.31.0 → 3.33.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 +1 -1
- package/src/components/BuildMock/BuildMock.jsx +101 -0
- package/src/components/BuildMock/BuildMock.module.css +290 -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 +149 -0
- package/src/components/KanbanMock/KanbanMock.module.css +325 -0
- package/src/components/KanbanMock/__tests__/KanbanMock.render.test.js +114 -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 +346 -14
- package/src/components/WidgetShelf/WidgetShelf.module.css +276 -5
- package/src/components/WidgetShelf/__tests__/WidgetShelf.render.test.js +149 -0
- package/src/components/index.js +4 -0
- package/src/components/primitives/SectionHead.module.css +6 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <KanbanMock /> styles. Scoped under `.km` (display: contents), the
|
|
3
|
+
* AppMock `.am` convention, so specimen pages can reuse the file as a
|
|
4
|
+
* plain global stylesheet.
|
|
5
|
+
*
|
|
6
|
+
* One shared timeline (--km-dur, default 7s):
|
|
7
|
+
*
|
|
8
|
+
* 0–10% cursor travels to the top card in column 1
|
|
9
|
+
* 14% the card lifts (scale + deeper shadow)
|
|
10
|
+
* 16–50% card + cursor drag to column 2's top slot
|
|
11
|
+
* 50–54% the card settles into the slot
|
|
12
|
+
* 54–56% the column counts (3→2, 2→3) and the lane € KPIs
|
|
13
|
+
* (€ 48k→€ 30k, € 27k→€ 45k) crossfade — right at the drop
|
|
14
|
+
* 56–86% hold
|
|
15
|
+
* 86–100% card fades back to its origin, counts + KPIs revert, loop
|
|
16
|
+
*
|
|
17
|
+
* Geometry: the mover is absolutely positioned over the board, so its
|
|
18
|
+
* keyframe coordinates must equal the top card slot of columns 1 and
|
|
19
|
+
* 2. Those slots mix %-sized board chrome with px-sized column chrome,
|
|
20
|
+
* so the coordinates are calc() custom properties (--km-x1, --km-x2,
|
|
21
|
+
* --km-y, --km-card-w) derived below rather than by-eye percentages —
|
|
22
|
+
* that keeps the landing exact at every frame width. Derivation:
|
|
23
|
+
* board padding 6%/5% of frame width W; column gap 4% of the 90%
|
|
24
|
+
* content box = 3.6%W; columns (90% − 7.2%)/3 = 27.6%W;
|
|
25
|
+
* col chrome = 1px border + 10px padding = 11px;
|
|
26
|
+
* vertical: 6%W top pad = 9.6% of the 16/10 frame height, + KPI row
|
|
27
|
+
* 34px + 12px row gap + col chrome 11px + head 16px + head margin
|
|
28
|
+
* 2px + col gap 8px = 83px.
|
|
29
|
+
*
|
|
30
|
+
* prefers-reduced-motion (and .static from running={false}) renders
|
|
31
|
+
* the static board: no cursor, no animation, the card resting in its
|
|
32
|
+
* origin column, original counts and KPI values. Tokens only; the
|
|
33
|
+
* mover's status dot (mint) is the only non-neutral accent; no orange.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
.km { display: contents; }
|
|
37
|
+
|
|
38
|
+
.km .figure {
|
|
39
|
+
margin: 0;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--space-2);
|
|
43
|
+
width: 100%;
|
|
44
|
+
max-width: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.km .frame {
|
|
48
|
+
--km-dur: 7s;
|
|
49
|
+
/* Card-slot geometry (see the derivation in the header comment). */
|
|
50
|
+
--km-x1: calc(5% + 11px);
|
|
51
|
+
--km-x2: calc(36.2% + 11px);
|
|
52
|
+
--km-y: calc(9.6% + 83px);
|
|
53
|
+
--km-card-w: calc(27.6% - 22px);
|
|
54
|
+
width: 100%;
|
|
55
|
+
max-width: 720px;
|
|
56
|
+
aspect-ratio: 16 / 10;
|
|
57
|
+
background: white;
|
|
58
|
+
border: 1px solid var(--c-cobalt-100);
|
|
59
|
+
border-radius: var(--radius-lg);
|
|
60
|
+
box-shadow: var(--shadow-3);
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
position: relative;
|
|
63
|
+
font-family: var(--conduction-typography-font-family-body);
|
|
64
|
+
}
|
|
65
|
+
.km .size-sm { max-width: 480px; }
|
|
66
|
+
.km .size-md { max-width: 720px; }
|
|
67
|
+
|
|
68
|
+
.km .caption {
|
|
69
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
70
|
+
font-size: 11px;
|
|
71
|
+
letter-spacing: 0.06em;
|
|
72
|
+
color: var(--c-cobalt-400);
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ---- Board ---- */
|
|
78
|
+
|
|
79
|
+
.km .board {
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset: 0;
|
|
82
|
+
padding: 6% 5%;
|
|
83
|
+
display: grid;
|
|
84
|
+
grid-template-columns: repeat(3, 1fr);
|
|
85
|
+
grid-template-rows: auto 1fr;
|
|
86
|
+
/* Fixed 12px row gap (part of the mover's --km-y derivation);
|
|
87
|
+
percentage column gap so lanes track the frame width. */
|
|
88
|
+
gap: 12px 4%;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ---- KPI row above the lanes ---- */
|
|
92
|
+
|
|
93
|
+
.km .kpis {
|
|
94
|
+
grid-column: 1 / -1;
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: repeat(3, 1fr);
|
|
97
|
+
/* Same content box as the board, so 4% aligns tiles with lanes. */
|
|
98
|
+
gap: 4%;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.km .kpi {
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
height: 34px; /* fixed: part of the mover's --km-y derivation */
|
|
105
|
+
background: white;
|
|
106
|
+
border: 1px solid var(--c-cobalt-100);
|
|
107
|
+
border-radius: var(--radius-md);
|
|
108
|
+
box-shadow: var(--shadow-1);
|
|
109
|
+
padding: 0 10px;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
}
|
|
116
|
+
.km .kpiBar {
|
|
117
|
+
display: block;
|
|
118
|
+
width: 26%;
|
|
119
|
+
height: 4px;
|
|
120
|
+
border-radius: 1px;
|
|
121
|
+
background: var(--c-cobalt-300);
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
}
|
|
124
|
+
.km .kpiValue {
|
|
125
|
+
position: relative;
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: flex-end;
|
|
129
|
+
min-width: 38px;
|
|
130
|
+
height: 14px;
|
|
131
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: var(--c-cobalt-900);
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
/* The crossfading value spans reuse .countBefore/.countAfter (same
|
|
138
|
+
timeline, same static overrides) but sit right-aligned in the tile
|
|
139
|
+
instead of filling a pill. */
|
|
140
|
+
.km .kpiValue .countBefore,
|
|
141
|
+
.km .kpiValue .countAfter {
|
|
142
|
+
inset: auto;
|
|
143
|
+
right: 0;
|
|
144
|
+
top: 50%;
|
|
145
|
+
transform: translateY(-50%);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
/* A lane total that never changes (third lane). */
|
|
149
|
+
.km .kpiStill { display: inline-flex; align-items: center; }
|
|
150
|
+
|
|
151
|
+
.km .col {
|
|
152
|
+
background: var(--c-cobalt-50);
|
|
153
|
+
border: 1px solid var(--c-cobalt-100);
|
|
154
|
+
border-radius: var(--radius-md);
|
|
155
|
+
padding: 10px;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 8px;
|
|
159
|
+
min-width: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.km .colHead {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
margin-bottom: 2px;
|
|
168
|
+
}
|
|
169
|
+
.km .colBar {
|
|
170
|
+
display: block;
|
|
171
|
+
flex: 1;
|
|
172
|
+
max-width: 55%;
|
|
173
|
+
height: 5px;
|
|
174
|
+
border-radius: 1.5px;
|
|
175
|
+
background: var(--c-cobalt-700);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Count pill: two stacked digits crossfading on the timeline. */
|
|
179
|
+
.km .count,
|
|
180
|
+
.km .countStatic {
|
|
181
|
+
position: relative;
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
min-width: 20px;
|
|
186
|
+
height: 16px;
|
|
187
|
+
padding: 0 5px;
|
|
188
|
+
border-radius: var(--radius-pill);
|
|
189
|
+
background: var(--c-cobalt-100);
|
|
190
|
+
color: var(--c-cobalt-700);
|
|
191
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
192
|
+
font-size: 10px;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
.km .countBefore,
|
|
196
|
+
.km .countAfter {
|
|
197
|
+
position: absolute;
|
|
198
|
+
inset: 0;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
}
|
|
203
|
+
.km .countBefore { animation: kmCountBefore var(--km-dur) linear infinite; }
|
|
204
|
+
.km .countAfter { opacity: 0; animation: kmCountAfter var(--km-dur) linear infinite; }
|
|
205
|
+
|
|
206
|
+
/* ---- Cards ---- */
|
|
207
|
+
|
|
208
|
+
.km .card,
|
|
209
|
+
.km .mover {
|
|
210
|
+
/* Explicit border-box: --km-card-w describes the full card box, and
|
|
211
|
+
not every consumer of this file ships a global box-sizing reset. */
|
|
212
|
+
box-sizing: border-box;
|
|
213
|
+
background: white;
|
|
214
|
+
border: 1px solid var(--c-cobalt-100);
|
|
215
|
+
border-radius: var(--radius-sm);
|
|
216
|
+
box-shadow: var(--shadow-1);
|
|
217
|
+
padding: 8px 9px;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 5px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* The gap where the travelling card lives at rest. Exactly the
|
|
224
|
+
mover's height (2px border + 16px padding + 23px content) so the
|
|
225
|
+
card sits flush in the slot when it lands. */
|
|
226
|
+
.km .slotGap { height: 41px; flex-shrink: 0; }
|
|
227
|
+
|
|
228
|
+
.km .cardTop { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
229
|
+
.km .cardBar { display: block; height: 4px; border-radius: 1px; background: var(--c-cobalt-700); }
|
|
230
|
+
.km .cardLine { display: block; height: 3px; border-radius: 1px; background: var(--c-cobalt-200); width: 78%; }
|
|
231
|
+
.km .cardDot {
|
|
232
|
+
width: 7px;
|
|
233
|
+
height: 7px;
|
|
234
|
+
border-radius: 50%;
|
|
235
|
+
background: var(--c-mint-500);
|
|
236
|
+
flex-shrink: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ---- The travelling card ---- */
|
|
240
|
+
|
|
241
|
+
.km .mover {
|
|
242
|
+
position: absolute;
|
|
243
|
+
/* Origin: column 1's top slot. Exact calc() geometry — see the
|
|
244
|
+
header comment — so the card sits flush in the slot at rest and
|
|
245
|
+
when it lands, at every frame width. The lift is a transform so
|
|
246
|
+
left/top always mean "the slot". */
|
|
247
|
+
left: var(--km-x1);
|
|
248
|
+
top: var(--km-y);
|
|
249
|
+
width: var(--km-card-w);
|
|
250
|
+
animation: kmMove var(--km-dur) linear infinite;
|
|
251
|
+
z-index: 2;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@keyframes kmMove {
|
|
255
|
+
0%, 10% { left: var(--km-x1); top: var(--km-y); transform: translateY(0) scale(1); box-shadow: var(--shadow-1); opacity: 1; }
|
|
256
|
+
14% { left: var(--km-x1); top: var(--km-y); transform: translateY(-3px) scale(1.04); box-shadow: var(--shadow-3); opacity: 1; }
|
|
257
|
+
50% { left: var(--km-x2); top: var(--km-y); transform: translateY(-3px) scale(1.04); box-shadow: var(--shadow-3); opacity: 1; }
|
|
258
|
+
54% { left: var(--km-x2); top: var(--km-y); transform: translateY(0) scale(1); box-shadow: var(--shadow-1); opacity: 1; }
|
|
259
|
+
86% { left: var(--km-x2); top: var(--km-y); transform: translateY(0) scale(1); box-shadow: var(--shadow-1); opacity: 1; }
|
|
260
|
+
92% { left: var(--km-x2); top: var(--km-y); opacity: 0; }
|
|
261
|
+
96% { left: var(--km-x1); top: var(--km-y); opacity: 0; }
|
|
262
|
+
100% { left: var(--km-x1); top: var(--km-y); transform: translateY(0) scale(1); box-shadow: var(--shadow-1); opacity: 1; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ---- Cursor ---- */
|
|
266
|
+
|
|
267
|
+
.km .cursor {
|
|
268
|
+
position: absolute;
|
|
269
|
+
width: 15px;
|
|
270
|
+
height: 15px;
|
|
271
|
+
left: 60%;
|
|
272
|
+
top: 60%;
|
|
273
|
+
fill: var(--c-cobalt-900);
|
|
274
|
+
stroke: white;
|
|
275
|
+
stroke-width: 1;
|
|
276
|
+
filter: drop-shadow(0 1px 2px rgba(10, 23, 47, 0.35));
|
|
277
|
+
animation: kmCursor var(--km-dur) linear infinite;
|
|
278
|
+
z-index: 3;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@keyframes kmCursor {
|
|
282
|
+
0% { left: 60%; top: 60%; }
|
|
283
|
+
10% { left: calc(var(--km-x1) + 10%); top: calc(var(--km-y) + 18px); } /* over the card */
|
|
284
|
+
14% { left: calc(var(--km-x1) + 10%); top: calc(var(--km-y) + 15px); }
|
|
285
|
+
50% { left: calc(var(--km-x2) + 10%); top: calc(var(--km-y) + 15px); } /* drag with the card */
|
|
286
|
+
54% { left: calc(var(--km-x2) + 10%); top: calc(var(--km-y) + 18px); }
|
|
287
|
+
64% { left: 60%; top: 60%; } /* release, drift away */
|
|
288
|
+
100% { left: 60%; top: 60%; }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ---- Count crossfade ---- */
|
|
292
|
+
|
|
293
|
+
/* Counts and KPI values swap the moment the card settles: the drop
|
|
294
|
+
ends at 54%, the crossfade runs 54–56%. They revert while the card
|
|
295
|
+
fades back to its origin (86–96%). */
|
|
296
|
+
@keyframes kmCountBefore {
|
|
297
|
+
0%, 54% { opacity: 1; }
|
|
298
|
+
56%, 88% { opacity: 0; }
|
|
299
|
+
93%, 100% { opacity: 1; }
|
|
300
|
+
}
|
|
301
|
+
@keyframes kmCountAfter {
|
|
302
|
+
0%, 54% { opacity: 0; }
|
|
303
|
+
56%, 88% { opacity: 1; }
|
|
304
|
+
93%, 100% { opacity: 0; }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ---- Static board (running={false} and reduced motion) ---- */
|
|
308
|
+
|
|
309
|
+
.km .static .mover,
|
|
310
|
+
.km .static .cursor,
|
|
311
|
+
.km .static .countBefore,
|
|
312
|
+
.km .static .countAfter { animation: none; }
|
|
313
|
+
.km .static .cursor { opacity: 0; visibility: hidden; }
|
|
314
|
+
.km .static .countBefore { opacity: 1; }
|
|
315
|
+
.km .static .countAfter { opacity: 0; }
|
|
316
|
+
|
|
317
|
+
@media (prefers-reduced-motion: reduce) {
|
|
318
|
+
.km .mover,
|
|
319
|
+
.km .cursor,
|
|
320
|
+
.km .countBefore,
|
|
321
|
+
.km .countAfter { animation: none; }
|
|
322
|
+
.km .cursor { opacity: 0; visibility: hidden; }
|
|
323
|
+
.km .countBefore { opacity: 1; }
|
|
324
|
+
.km .countAfter { opacity: 0; }
|
|
325
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KanbanMock.render.test.js — renders the real <KanbanMock> JSX to
|
|
3
|
+
* static markup (the SSR path Docusaurus takes at build time) and
|
|
4
|
+
* asserts on the output: the KPI row above the lanes with its
|
|
5
|
+
* before/after € values, the column counts, and the static mode.
|
|
6
|
+
*
|
|
7
|
+
* Uses the same esbuild-bundle-then-renderToStaticMarkup technique as
|
|
8
|
+
* AiDisclosure.render.test.js / scripts/build-kit.mjs. CSS modules
|
|
9
|
+
* are stubbed with an identity proxy (styles.foo → 'foo'), so class
|
|
10
|
+
* assertions read as the source class names.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const test = require('node:test');
|
|
16
|
+
const {before, after} = test;
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const fs = require('node:fs/promises');
|
|
20
|
+
const {build} = require('esbuild');
|
|
21
|
+
const React = require('react');
|
|
22
|
+
const {renderToStaticMarkup} = require('react-dom/server');
|
|
23
|
+
|
|
24
|
+
const COMPONENT = path.resolve(__dirname, '..', 'KanbanMock.jsx');
|
|
25
|
+
const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
26
|
+
const SCRATCH = path.join(PRESET_ROOT, '.tmp-kanban-mock-test');
|
|
27
|
+
|
|
28
|
+
const cssModuleStub = {
|
|
29
|
+
name: 'css-module-stub',
|
|
30
|
+
setup(b) {
|
|
31
|
+
b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
|
|
32
|
+
b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
|
|
33
|
+
contents: 'export default new Proxy({}, {get: (_, p) => p});',
|
|
34
|
+
loader: 'js',
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let KanbanMock;
|
|
40
|
+
|
|
41
|
+
before(async () => {
|
|
42
|
+
await fs.mkdir(SCRATCH, {recursive: true});
|
|
43
|
+
const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
|
|
44
|
+
const outFile = path.join(tmpDir, 'bundle.cjs');
|
|
45
|
+
await build({
|
|
46
|
+
entryPoints: [COMPONENT],
|
|
47
|
+
outfile: outFile,
|
|
48
|
+
bundle: true,
|
|
49
|
+
format: 'cjs',
|
|
50
|
+
jsx: 'automatic',
|
|
51
|
+
jsxImportSource: 'react',
|
|
52
|
+
tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
|
|
53
|
+
platform: 'node',
|
|
54
|
+
external: ['react'],
|
|
55
|
+
plugins: [cssModuleStub],
|
|
56
|
+
logLevel: 'warning',
|
|
57
|
+
});
|
|
58
|
+
delete require.cache[require.resolve(outFile)];
|
|
59
|
+
KanbanMock = require(outFile).default;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
after(async () => {
|
|
63
|
+
await fs.rm(SCRATCH, {recursive: true, force: true});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function render(props) {
|
|
67
|
+
return renderToStaticMarkup(React.createElement(KanbanMock, props));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('SSR smoke: renders the board with three lanes and the travelling card', () => {
|
|
71
|
+
const html = render({});
|
|
72
|
+
assert.equal((html.match(/class="col"/g) || []).length, 3);
|
|
73
|
+
assert.match(html, /class="mover"/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('KPI row: one € tile per lane, before/after values on the two lanes the deal moves between', () => {
|
|
77
|
+
const html = render({});
|
|
78
|
+
assert.match(html, /class="kpis"/);
|
|
79
|
+
assert.equal((html.match(/class="kpi"/g) || []).length, 3);
|
|
80
|
+
// Source lane loses the €18k deal, target lane gains it.
|
|
81
|
+
assert.match(html, /class="countBefore">€ 48k</);
|
|
82
|
+
assert.match(html, /class="countAfter">€ 30k</);
|
|
83
|
+
assert.match(html, /class="countBefore">€ 27k</);
|
|
84
|
+
assert.match(html, /class="countAfter">€ 45k</);
|
|
85
|
+
// Third lane never changes: a still value, no crossfade spans.
|
|
86
|
+
assert.match(html, /class="kpiStill">€ 64k</);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('KPI arithmetic: the moved deal value leaving lane 1 equals the value entering lane 2', () => {
|
|
90
|
+
const html = render({});
|
|
91
|
+
const nums = [...html.matchAll(/class="count(Before|After)">€ (\d+)k</g)].map((m) => Number(m[2]));
|
|
92
|
+
assert.equal(nums.length, 4, 'expected two crossfading € tiles (2 values each)');
|
|
93
|
+
// Order in markup: lane1 before, lane1 after, lane2 before, lane2 after.
|
|
94
|
+
const [l1From, l1To, l2From, l2To] = nums;
|
|
95
|
+
assert.equal(l1From - l1To, l2To - l2From, 'the deal value must shift lanes, not appear or vanish');
|
|
96
|
+
assert.ok(l1From - l1To > 0, 'source lane must decrement');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('column counts: source lane 3→2, target lane 2→3, third lane static 4', () => {
|
|
100
|
+
const html = render({});
|
|
101
|
+
assert.match(html, /class="count"><span class="countBefore">3<\/span><span class="countAfter">2<\/span>/);
|
|
102
|
+
assert.match(html, /class="count"><span class="countBefore">2<\/span><span class="countAfter">3<\/span>/);
|
|
103
|
+
assert.match(html, /class="countStatic">4</);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('aria-label names the counts and lane totals updating at the drop', () => {
|
|
107
|
+
const html = render({});
|
|
108
|
+
assert.match(html, /aria-label="[^"]*column counts and the lane value totals update at the drop[^"]*"/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('running={false} renders the static frame class', () => {
|
|
112
|
+
const html = render({running: false});
|
|
113
|
+
assert.match(html, /class="frame size-md static"/);
|
|
114
|
+
});
|
|
@@ -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
|
+
}
|