@usefidel/design-system 0.2.0 → 0.4.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.
@@ -10,6 +10,6 @@
10
10
  "webapp.css"
11
11
  ],
12
12
  "storybookUrl": null,
13
- "packageVersion": "0.2.0",
14
- "resolvedSha": "c78cdac9bf6848d4e37053d17dd1bf15ded0aa40"
13
+ "packageVersion": "0.4.0",
14
+ "resolvedSha": "2ffffb3224acff54fc34007bdbf831644f69fa22"
15
15
  }
package/dist/index.cjs CHANGED
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Button: () => Button
23
+ Button: () => Button,
24
+ DiffSection: () => DiffSection,
25
+ MismatchCard: () => MismatchCard
24
26
  });
25
27
  module.exports = __toCommonJS(index_exports);
26
28
 
@@ -48,7 +50,201 @@ function Button({
48
50
  rightIcon != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "fds-button__icon", "aria-hidden": "true", children: rightIcon })
49
51
  ] });
50
52
  }
53
+
54
+ // src/components/MismatchCard/MismatchCard.tsx
55
+ var import_react = require("react");
56
+ var import_jsx_runtime2 = require("react/jsx-runtime");
57
+ function MismatchCard({
58
+ title,
59
+ properties,
60
+ collapsedCount = 2,
61
+ defaultExpanded = false,
62
+ className,
63
+ ...rest
64
+ }) {
65
+ const [expanded, setExpanded] = (0, import_react.useState)(defaultExpanded);
66
+ const canToggle = properties.length > collapsedCount;
67
+ const visible = expanded || !canToggle ? properties : properties.slice(0, collapsedCount);
68
+ const classes = ["fds-mismatch-card", className].filter(Boolean).join(" ");
69
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: classes, ...rest, children: [
70
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "fds-mismatch-card__content", children: [
71
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "fds-mismatch-card__title", title, children: title }),
72
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fds-mismatch-card__rows", children: visible.map((prop, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "fds-mismatch-card__row", children: [
73
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fds-mismatch-card__label", children: prop.label }),
74
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "fds-mismatch-card__values", children: [
75
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fds-mismatch-card__live", children: prop.live }),
76
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fds-mismatch-card__design", children: prop.design })
77
+ ] })
78
+ ] }, `${prop.label}-${i}`)) })
79
+ ] }),
80
+ canToggle && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
81
+ "button",
82
+ {
83
+ type: "button",
84
+ className: "fds-mismatch-card__toggle",
85
+ "aria-expanded": expanded,
86
+ onClick: () => setExpanded((v) => !v),
87
+ children: [
88
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Chevron, { direction: expanded ? "up" : "down" }),
89
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "fds-mismatch-card__toggle-label", children: expanded ? "Show less" : "Show more" })
90
+ ]
91
+ }
92
+ )
93
+ ] });
94
+ }
95
+ function Chevron({ direction }) {
96
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
97
+ "svg",
98
+ {
99
+ className: `fds-mismatch-card__chevron fds-mismatch-card__chevron--${direction}`,
100
+ width: "20",
101
+ height: "20",
102
+ viewBox: "0 0 20 20",
103
+ fill: "none",
104
+ "aria-hidden": "true",
105
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
106
+ "path",
107
+ {
108
+ d: "M6 8.5 10 12l4-3.5",
109
+ stroke: "currentColor",
110
+ strokeWidth: "1.6",
111
+ strokeLinecap: "round",
112
+ strokeLinejoin: "round"
113
+ }
114
+ )
115
+ }
116
+ );
117
+ }
118
+
119
+ // src/components/DiffSection/DiffSection.tsx
120
+ var import_react2 = require("react");
121
+ var import_jsx_runtime3 = require("react/jsx-runtime");
122
+ function plural(n, one, many) {
123
+ return `${n} ${n === 1 ? one : many}`;
124
+ }
125
+ function DiffSection({
126
+ number,
127
+ name,
128
+ mismatchCount,
129
+ elements,
130
+ defaultExpanded = false,
131
+ headerless = false,
132
+ className,
133
+ ...rest
134
+ }) {
135
+ const [expanded, setExpanded] = (0, import_react2.useState)(defaultExpanded);
136
+ const open = headerless || expanded;
137
+ const count = mismatchCount ?? elements.reduce((sum, el) => sum + el.properties.length, 0);
138
+ const classes = [
139
+ "fds-diff-section",
140
+ headerless && "fds-diff-section--flat",
141
+ className
142
+ ].filter(Boolean).join(" ");
143
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
144
+ "div",
145
+ {
146
+ className: classes,
147
+ "data-expanded": open ? "true" : void 0,
148
+ "data-flat": headerless ? "true" : void 0,
149
+ ...rest,
150
+ children: [
151
+ !headerless && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ className: "fds-diff-section__header",
156
+ "aria-expanded": expanded,
157
+ onClick: () => setExpanded((v) => !v),
158
+ children: [
159
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "fds-diff-section__head-left", children: [
160
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Chevron2, { open: expanded }),
161
+ number != null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__number", children: number }),
162
+ name != null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__name", children: name })
163
+ ] }),
164
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__count", children: plural(count, "mismatch", "mismatches") })
165
+ ]
166
+ }
167
+ ),
168
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fds-diff-section__body", children: elements.map((el, ei) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ElementRow, { element: el }, `${el.name}-${ei}`)) })
169
+ ]
170
+ }
171
+ );
172
+ }
173
+ function ElementRow({ element }) {
174
+ const hasDetail = element.detail != null;
175
+ const [open, setOpen] = (0, import_react2.useState)(element.defaultOpen ?? false);
176
+ const lines = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
177
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fds-diff-section__element-head-inner", children: [
178
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__element-name", children: element.name }),
179
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__element-count", children: plural(element.properties.length, "property", "properties") }),
180
+ hasDetail && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Chevron2, { open, className: "fds-diff-section__element-caret" })
181
+ ] }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fds-diff-section__lines", children: element.properties.map((line, li) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fds-diff-section__line", children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__prop", children: line.property }),
184
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "fds-diff-section__delta", children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__live", children: line.live }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Arrow, {}),
187
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fds-diff-section__design", children: line.design })
188
+ ] })
189
+ ] }, `${line.property}-${li}`)) })
190
+ ] });
191
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fds-diff-section__element", "data-open": hasDetail && open ? "true" : void 0, children: [
192
+ hasDetail ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
193
+ "button",
194
+ {
195
+ type: "button",
196
+ className: "fds-diff-section__element-head fds-diff-section__element-head--button",
197
+ "aria-expanded": open,
198
+ onClick: () => setOpen((v) => !v),
199
+ children: lines
200
+ }
201
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fds-diff-section__element-head", children: lines }),
202
+ hasDetail && open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fds-diff-section__detail", children: element.detail })
203
+ ] });
204
+ }
205
+ function Chevron2({ open, className }) {
206
+ const classes = [
207
+ "fds-diff-section__chevron",
208
+ open && "fds-diff-section__chevron--open",
209
+ className
210
+ ].filter(Boolean).join(" ");
211
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", className: classes, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
212
+ "path",
213
+ {
214
+ d: "M4.5 3 8 6l-3.5 3",
215
+ stroke: "currentColor",
216
+ strokeWidth: "1.3",
217
+ strokeLinecap: "round",
218
+ strokeLinejoin: "round"
219
+ }
220
+ ) });
221
+ }
222
+ function Arrow() {
223
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
224
+ "svg",
225
+ {
226
+ className: "fds-diff-section__arrow",
227
+ width: "10",
228
+ height: "10",
229
+ viewBox: "0 0 10 10",
230
+ fill: "none",
231
+ "aria-hidden": "true",
232
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
233
+ "path",
234
+ {
235
+ d: "M1.5 5h6.5M5.5 2.5 8 5 5.5 7.5",
236
+ stroke: "currentColor",
237
+ strokeWidth: "1",
238
+ strokeLinecap: "round",
239
+ strokeLinejoin: "round"
240
+ }
241
+ )
242
+ }
243
+ );
244
+ }
51
245
  // Annotate the CommonJS export names for ESM import in node:
52
246
  0 && (module.exports = {
53
- Button
247
+ Button,
248
+ DiffSection,
249
+ MismatchCard
54
250
  });
package/dist/index.css CHANGED
@@ -123,3 +123,284 @@
123
123
  color: #a3a3a3;
124
124
  cursor: not-allowed;
125
125
  }
126
+
127
+ /* src/components/MismatchCard/mismatch-card.css */
128
+ .fds-mismatch-card {
129
+ box-sizing: border-box;
130
+ display: flex;
131
+ flex-direction: column;
132
+ align-items: center;
133
+ gap: 14px;
134
+ width: max-content;
135
+ max-width: 368px;
136
+ padding: var(--spacing-6);
137
+ background: rgba(255, 255, 255, 0.4);
138
+ backdrop-filter: blur(24px);
139
+ -webkit-backdrop-filter: blur(24px);
140
+ border: 2px solid var(--color-accent);
141
+ border-radius: var(--radius-lg);
142
+ box-shadow: 0 15px 35px 0 rgba(151, 151, 151, 0.2);
143
+ font-family: var(--font-body);
144
+ overflow: clip;
145
+ }
146
+ .fds-mismatch-card *,
147
+ .fds-mismatch-card *::before,
148
+ .fds-mismatch-card *::after {
149
+ box-sizing: border-box;
150
+ }
151
+ .fds-mismatch-card__content {
152
+ display: flex;
153
+ flex-direction: column;
154
+ align-items: flex-start;
155
+ gap: var(--spacing-5);
156
+ width: 320px;
157
+ }
158
+ .fds-mismatch-card__title {
159
+ margin: 0;
160
+ width: 100%;
161
+ font-family: var(--font-display);
162
+ font-weight: var(--font-weight-semibold);
163
+ font-size: 16px;
164
+ line-height: 24px;
165
+ letter-spacing: -0.0288px;
166
+ color: var(--color-text);
167
+ white-space: nowrap;
168
+ overflow: hidden;
169
+ text-overflow: ellipsis;
170
+ }
171
+ .fds-mismatch-card__rows {
172
+ display: flex;
173
+ flex-direction: column;
174
+ align-items: flex-start;
175
+ gap: var(--spacing-3);
176
+ width: 100%;
177
+ }
178
+ .fds-mismatch-card__row {
179
+ display: flex;
180
+ flex-direction: column;
181
+ align-items: flex-start;
182
+ gap: var(--spacing-2);
183
+ width: 100%;
184
+ border-bottom: 1px solid var(--color-border-strong);
185
+ }
186
+ .fds-mismatch-card__label {
187
+ width: 100%;
188
+ font-weight: var(--font-weight-medium);
189
+ font-size: 14px;
190
+ line-height: 22px;
191
+ color: var(--color-text-secondary);
192
+ }
193
+ .fds-mismatch-card__values {
194
+ display: flex;
195
+ justify-content: space-between;
196
+ width: 100%;
197
+ padding: var(--spacing-1) 0;
198
+ font-weight: var(--font-weight-medium);
199
+ font-size: 14px;
200
+ line-height: 22px;
201
+ white-space: nowrap;
202
+ }
203
+ .fds-mismatch-card__live {
204
+ color: var(--color-actual-text);
205
+ }
206
+ .fds-mismatch-card__design {
207
+ color: #262626;
208
+ }
209
+ .fds-mismatch-card__toggle {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 2px;
213
+ width: 320px;
214
+ padding: var(--spacing-2) 0;
215
+ border: 0;
216
+ background: none;
217
+ border-radius: var(--radius-md);
218
+ cursor: pointer;
219
+ font-family: var(--font-display);
220
+ font-weight: var(--font-weight-semibold);
221
+ font-size: 14px;
222
+ line-height: 20px;
223
+ color: var(--color-text-secondary);
224
+ }
225
+ .fds-mismatch-card__chevron {
226
+ flex-shrink: 0;
227
+ color: currentColor;
228
+ }
229
+ .fds-mismatch-card__chevron--up {
230
+ transform: rotate(180deg);
231
+ }
232
+ .fds-mismatch-card__toggle-label {
233
+ white-space: nowrap;
234
+ }
235
+
236
+ /* src/components/DiffSection/diff-section.css */
237
+ .fds-diff-section {
238
+ box-sizing: border-box;
239
+ display: flex;
240
+ flex-direction: column;
241
+ align-items: stretch;
242
+ width: 100%;
243
+ background: var(--color-surface-raised);
244
+ font-family: var(--font-body);
245
+ }
246
+ .fds-diff-section[data-expanded=true] {
247
+ gap: var(--spacing-3);
248
+ padding-bottom: 14px;
249
+ }
250
+ .fds-diff-section *,
251
+ .fds-diff-section *::before,
252
+ .fds-diff-section *::after {
253
+ box-sizing: border-box;
254
+ }
255
+ .fds-diff-section__header {
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: space-between;
259
+ width: 100%;
260
+ padding: var(--spacing-4) var(--spacing-4) var(--spacing-4) var(--spacing-6);
261
+ border: 0;
262
+ border-bottom: 1px solid var(--color-border);
263
+ background: #fafafa;
264
+ cursor: pointer;
265
+ text-align: left;
266
+ font-family: inherit;
267
+ }
268
+ .fds-diff-section[data-expanded=true] .fds-diff-section__header {
269
+ border-bottom-color: var(--color-border-light);
270
+ }
271
+ .fds-diff-section__head-left {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: var(--spacing-2);
275
+ }
276
+ .fds-diff-section__chevron {
277
+ flex-shrink: 0;
278
+ color: var(--color-text-secondary);
279
+ transition: transform 150ms ease;
280
+ }
281
+ .fds-diff-section__chevron--open {
282
+ transform: rotate(90deg);
283
+ }
284
+ .fds-diff-section__number {
285
+ font-family: var(--font-mono);
286
+ font-weight: var(--font-weight-regular);
287
+ font-size: 12px;
288
+ line-height: 18px;
289
+ letter-spacing: -0.24px;
290
+ color: var(--color-text-secondary);
291
+ }
292
+ .fds-diff-section__name {
293
+ font-weight: var(--font-weight-medium);
294
+ font-size: 14px;
295
+ line-height: 22px;
296
+ color: var(--color-text);
297
+ }
298
+ .fds-diff-section__count {
299
+ font-weight: var(--font-weight-regular);
300
+ font-size: 14px;
301
+ line-height: 22px;
302
+ color: var(--color-text-secondary);
303
+ white-space: nowrap;
304
+ }
305
+ .fds-diff-section__body {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: var(--spacing-3);
309
+ }
310
+ .fds-diff-section__element {
311
+ display: flex;
312
+ flex-direction: column;
313
+ }
314
+ .fds-diff-section__element-head {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 14px;
318
+ width: 100%;
319
+ padding: 6px var(--spacing-5) 6px var(--spacing-6);
320
+ }
321
+ .fds-diff-section__element-head--button {
322
+ border: 0;
323
+ background: none;
324
+ font: inherit;
325
+ color: inherit;
326
+ text-align: left;
327
+ cursor: pointer;
328
+ transition: background var(--duration-quick, 150ms) ease;
329
+ }
330
+ .fds-diff-section__element-head--button:hover {
331
+ background: var(--color-surface-hover);
332
+ }
333
+ .fds-diff-section__element-head-inner {
334
+ display: flex;
335
+ align-items: center;
336
+ gap: var(--spacing-2);
337
+ }
338
+ .fds-diff-section__element-caret {
339
+ margin-left: auto;
340
+ flex-shrink: 0;
341
+ color: var(--color-text-muted);
342
+ }
343
+ .fds-diff-section__detail {
344
+ padding: 0 var(--spacing-5) var(--spacing-3) var(--spacing-6);
345
+ }
346
+ .fds-diff-section--flat .fds-diff-section__body {
347
+ gap: 0;
348
+ }
349
+ .fds-diff-section--flat .fds-diff-section__element + .fds-diff-section__element {
350
+ border-top: 1px solid var(--color-border-light);
351
+ }
352
+ .fds-diff-section__element-name {
353
+ font-weight: var(--font-weight-medium);
354
+ font-size: 14px;
355
+ line-height: 22px;
356
+ color: var(--color-text);
357
+ }
358
+ .fds-diff-section__element-count {
359
+ font-weight: var(--font-weight-regular);
360
+ font-size: 12px;
361
+ line-height: 18px;
362
+ color: var(--color-text-secondary);
363
+ }
364
+ .fds-diff-section__lines {
365
+ display: flex;
366
+ flex-direction: column;
367
+ padding-left: 18px;
368
+ border-left: 1px solid var(--color-border);
369
+ margin-left: 2px;
370
+ }
371
+ .fds-diff-section__line {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: space-between;
375
+ width: 100%;
376
+ }
377
+ .fds-diff-section__prop {
378
+ font-family: var(--font-mono);
379
+ font-weight: var(--font-weight-regular);
380
+ font-size: 14px;
381
+ line-height: 22px;
382
+ letter-spacing: -0.28px;
383
+ color: var(--color-text-secondary);
384
+ white-space: nowrap;
385
+ }
386
+ .fds-diff-section__delta {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: var(--spacing-1);
390
+ font-family: var(--font-mono);
391
+ font-weight: var(--font-weight-medium);
392
+ font-size: 14px;
393
+ line-height: 22px;
394
+ letter-spacing: -0.28px;
395
+ white-space: nowrap;
396
+ }
397
+ .fds-diff-section__live {
398
+ color: #ef4444;
399
+ }
400
+ .fds-diff-section__arrow {
401
+ flex-shrink: 0;
402
+ color: var(--color-text-muted);
403
+ }
404
+ .fds-diff-section__design {
405
+ color: #1e8902;
406
+ }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode, HTMLAttributes } from 'react';
3
3
 
4
4
  type ButtonVariant = 'primary' | 'outlined' | 'text' | 'neutral';
5
5
  type ButtonSize = 'x-small' | 'small' | 'medium' | 'big';
@@ -20,4 +20,107 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
20
20
  */
21
21
  declare function Button({ variant, size, leftIcon, rightIcon, type, className, children, ...rest }: ButtonProps): react.JSX.Element;
22
22
 
23
- export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
23
+ interface MismatchProperty {
24
+ /** Property label, e.g. "color", "font-size". */
25
+ label: string;
26
+ /**
27
+ * Value on the live page — what actually shipped. Rendered in the
28
+ * live/error register (red). Left column, per the Live → Design framing.
29
+ */
30
+ live: string;
31
+ /**
32
+ * Value in the design — the intended target. Rendered in the design
33
+ * register (neutral). Right column.
34
+ */
35
+ design: string;
36
+ }
37
+ interface MismatchCardProps extends HTMLAttributes<HTMLDivElement> {
38
+ /** Element title (its name or text). Single line — truncates with ellipsis. */
39
+ title: string;
40
+ /** The property mismatches to show. */
41
+ properties: MismatchProperty[];
42
+ /**
43
+ * How many rows are visible before the "Show more" toggle appears.
44
+ * @default 2
45
+ */
46
+ collapsedCount?: number;
47
+ /** Render expanded on first paint. @default false */
48
+ defaultExpanded?: boolean;
49
+ }
50
+ /**
51
+ * Fidel design-system MismatchCard — the card shown when hovering a
52
+ * highlighted mismatch region on a validation screenshot.
53
+ *
54
+ * Presentational only: it knows nothing about the diff taxonomy. Callers map
55
+ * their own data (e.g. a ValidationDiff) onto `title` + `properties`.
56
+ *
57
+ * Framing is Live → Design (founder decision 2026-04-24): the left value is
58
+ * what shipped (live), the right value is the design target. When there are
59
+ * more than `collapsedCount` properties, a Show more / Show less toggle
60
+ * reveals the rest.
61
+ */
62
+ declare function MismatchCard({ title, properties, collapsedCount, defaultExpanded, className, ...rest }: MismatchCardProps): react.JSX.Element;
63
+
64
+ interface DiffPropertyLine {
65
+ /** Property name, e.g. "font-size". Rendered in mono. */
66
+ property: string;
67
+ /** Value on the live page — what shipped. Left of the arrow (error register). */
68
+ live: string;
69
+ /** Value in the design — the target. Right of the arrow (success register). */
70
+ design: string;
71
+ }
72
+ interface DiffElement {
73
+ /** Element name, e.g. `"Pricing"`. */
74
+ name: string;
75
+ /** Property mismatches for this element. */
76
+ properties: DiffPropertyLine[];
77
+ /**
78
+ * Optional drill-down content. When provided, the element row becomes a
79
+ * toggle: clicking it reveals this node beneath the property deltas. Use it
80
+ * to host a richer detail view (copy actions, triage, verdicts) while the
81
+ * mono deltas stay as the at-a-glance summary. Presentational — the caller
82
+ * owns whatever lives inside.
83
+ */
84
+ detail?: ReactNode;
85
+ /** When the element has `detail`, render it open on first paint. @default false */
86
+ defaultOpen?: boolean;
87
+ }
88
+ interface DiffSectionProps extends HTMLAttributes<HTMLDivElement> {
89
+ /** Ordinal shown before the name, e.g. "01". Omitted in `headerless` mode. */
90
+ number?: string;
91
+ /** Section name, e.g. "Nav". Omitted in `headerless` mode. */
92
+ name?: string;
93
+ /**
94
+ * Count shown on the right of the header. Defaults to the total number of
95
+ * property lines across all elements.
96
+ */
97
+ mismatchCount?: number;
98
+ /** Elements in this section, each with its own property mismatches. */
99
+ elements: DiffElement[];
100
+ /** Render expanded on first paint. Ignored in `headerless` mode. @default false */
101
+ defaultExpanded?: boolean;
102
+ /**
103
+ * Drop the numbered, collapsible section header and render the element list
104
+ * directly — a flat drawer with no section grouping. `number`/`name`/
105
+ * `mismatchCount` are ignored. @default false
106
+ */
107
+ headerless?: boolean;
108
+ }
109
+ /**
110
+ * Fidel design-system DiffSection — the mismatch drawer's section.
111
+ *
112
+ * Two shapes:
113
+ * - Grouped (default): a numbered, collapsible header (`01 Nav · 2
114
+ * mismatches`) over its elements.
115
+ * - `headerless`: just the element list, for a flat drawer with no section
116
+ * grouping.
117
+ *
118
+ * Each element lists its property deltas as `live → design` mono rows, and may
119
+ * carry a `detail` node that expands beneath the deltas as a drill-down.
120
+ *
121
+ * Presentational only — no diff taxonomy. Framing is Live → Design (founder
122
+ * decision 2026-04-24): each delta reads `live → design`.
123
+ */
124
+ declare function DiffSection({ number, name, mismatchCount, elements, defaultExpanded, headerless, className, ...rest }: DiffSectionProps): react.JSX.Element;
125
+
126
+ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, type DiffElement, type DiffPropertyLine, DiffSection, type DiffSectionProps, MismatchCard, type MismatchCardProps, type MismatchProperty };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode, HTMLAttributes } from 'react';
3
3
 
4
4
  type ButtonVariant = 'primary' | 'outlined' | 'text' | 'neutral';
5
5
  type ButtonSize = 'x-small' | 'small' | 'medium' | 'big';
@@ -20,4 +20,107 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
20
20
  */
21
21
  declare function Button({ variant, size, leftIcon, rightIcon, type, className, children, ...rest }: ButtonProps): react.JSX.Element;
22
22
 
23
- export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
23
+ interface MismatchProperty {
24
+ /** Property label, e.g. "color", "font-size". */
25
+ label: string;
26
+ /**
27
+ * Value on the live page — what actually shipped. Rendered in the
28
+ * live/error register (red). Left column, per the Live → Design framing.
29
+ */
30
+ live: string;
31
+ /**
32
+ * Value in the design — the intended target. Rendered in the design
33
+ * register (neutral). Right column.
34
+ */
35
+ design: string;
36
+ }
37
+ interface MismatchCardProps extends HTMLAttributes<HTMLDivElement> {
38
+ /** Element title (its name or text). Single line — truncates with ellipsis. */
39
+ title: string;
40
+ /** The property mismatches to show. */
41
+ properties: MismatchProperty[];
42
+ /**
43
+ * How many rows are visible before the "Show more" toggle appears.
44
+ * @default 2
45
+ */
46
+ collapsedCount?: number;
47
+ /** Render expanded on first paint. @default false */
48
+ defaultExpanded?: boolean;
49
+ }
50
+ /**
51
+ * Fidel design-system MismatchCard — the card shown when hovering a
52
+ * highlighted mismatch region on a validation screenshot.
53
+ *
54
+ * Presentational only: it knows nothing about the diff taxonomy. Callers map
55
+ * their own data (e.g. a ValidationDiff) onto `title` + `properties`.
56
+ *
57
+ * Framing is Live → Design (founder decision 2026-04-24): the left value is
58
+ * what shipped (live), the right value is the design target. When there are
59
+ * more than `collapsedCount` properties, a Show more / Show less toggle
60
+ * reveals the rest.
61
+ */
62
+ declare function MismatchCard({ title, properties, collapsedCount, defaultExpanded, className, ...rest }: MismatchCardProps): react.JSX.Element;
63
+
64
+ interface DiffPropertyLine {
65
+ /** Property name, e.g. "font-size". Rendered in mono. */
66
+ property: string;
67
+ /** Value on the live page — what shipped. Left of the arrow (error register). */
68
+ live: string;
69
+ /** Value in the design — the target. Right of the arrow (success register). */
70
+ design: string;
71
+ }
72
+ interface DiffElement {
73
+ /** Element name, e.g. `"Pricing"`. */
74
+ name: string;
75
+ /** Property mismatches for this element. */
76
+ properties: DiffPropertyLine[];
77
+ /**
78
+ * Optional drill-down content. When provided, the element row becomes a
79
+ * toggle: clicking it reveals this node beneath the property deltas. Use it
80
+ * to host a richer detail view (copy actions, triage, verdicts) while the
81
+ * mono deltas stay as the at-a-glance summary. Presentational — the caller
82
+ * owns whatever lives inside.
83
+ */
84
+ detail?: ReactNode;
85
+ /** When the element has `detail`, render it open on first paint. @default false */
86
+ defaultOpen?: boolean;
87
+ }
88
+ interface DiffSectionProps extends HTMLAttributes<HTMLDivElement> {
89
+ /** Ordinal shown before the name, e.g. "01". Omitted in `headerless` mode. */
90
+ number?: string;
91
+ /** Section name, e.g. "Nav". Omitted in `headerless` mode. */
92
+ name?: string;
93
+ /**
94
+ * Count shown on the right of the header. Defaults to the total number of
95
+ * property lines across all elements.
96
+ */
97
+ mismatchCount?: number;
98
+ /** Elements in this section, each with its own property mismatches. */
99
+ elements: DiffElement[];
100
+ /** Render expanded on first paint. Ignored in `headerless` mode. @default false */
101
+ defaultExpanded?: boolean;
102
+ /**
103
+ * Drop the numbered, collapsible section header and render the element list
104
+ * directly — a flat drawer with no section grouping. `number`/`name`/
105
+ * `mismatchCount` are ignored. @default false
106
+ */
107
+ headerless?: boolean;
108
+ }
109
+ /**
110
+ * Fidel design-system DiffSection — the mismatch drawer's section.
111
+ *
112
+ * Two shapes:
113
+ * - Grouped (default): a numbered, collapsible header (`01 Nav · 2
114
+ * mismatches`) over its elements.
115
+ * - `headerless`: just the element list, for a flat drawer with no section
116
+ * grouping.
117
+ *
118
+ * Each element lists its property deltas as `live → design` mono rows, and may
119
+ * carry a `detail` node that expands beneath the deltas as a drill-down.
120
+ *
121
+ * Presentational only — no diff taxonomy. Framing is Live → Design (founder
122
+ * decision 2026-04-24): each delta reads `live → design`.
123
+ */
124
+ declare function DiffSection({ number, name, mismatchCount, elements, defaultExpanded, headerless, className, ...rest }: DiffSectionProps): react.JSX.Element;
125
+
126
+ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, type DiffElement, type DiffPropertyLine, DiffSection, type DiffSectionProps, MismatchCard, type MismatchCardProps, type MismatchProperty };
package/dist/index.js CHANGED
@@ -22,6 +22,200 @@ function Button({
22
22
  rightIcon != null && /* @__PURE__ */ jsx("span", { className: "fds-button__icon", "aria-hidden": "true", children: rightIcon })
23
23
  ] });
24
24
  }
25
+
26
+ // src/components/MismatchCard/MismatchCard.tsx
27
+ import { useState } from "react";
28
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
29
+ function MismatchCard({
30
+ title,
31
+ properties,
32
+ collapsedCount = 2,
33
+ defaultExpanded = false,
34
+ className,
35
+ ...rest
36
+ }) {
37
+ const [expanded, setExpanded] = useState(defaultExpanded);
38
+ const canToggle = properties.length > collapsedCount;
39
+ const visible = expanded || !canToggle ? properties : properties.slice(0, collapsedCount);
40
+ const classes = ["fds-mismatch-card", className].filter(Boolean).join(" ");
41
+ return /* @__PURE__ */ jsxs2("div", { className: classes, ...rest, children: [
42
+ /* @__PURE__ */ jsxs2("div", { className: "fds-mismatch-card__content", children: [
43
+ /* @__PURE__ */ jsx2("p", { className: "fds-mismatch-card__title", title, children: title }),
44
+ /* @__PURE__ */ jsx2("div", { className: "fds-mismatch-card__rows", children: visible.map((prop, i) => /* @__PURE__ */ jsxs2("div", { className: "fds-mismatch-card__row", children: [
45
+ /* @__PURE__ */ jsx2("span", { className: "fds-mismatch-card__label", children: prop.label }),
46
+ /* @__PURE__ */ jsxs2("div", { className: "fds-mismatch-card__values", children: [
47
+ /* @__PURE__ */ jsx2("span", { className: "fds-mismatch-card__live", children: prop.live }),
48
+ /* @__PURE__ */ jsx2("span", { className: "fds-mismatch-card__design", children: prop.design })
49
+ ] })
50
+ ] }, `${prop.label}-${i}`)) })
51
+ ] }),
52
+ canToggle && /* @__PURE__ */ jsxs2(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ className: "fds-mismatch-card__toggle",
57
+ "aria-expanded": expanded,
58
+ onClick: () => setExpanded((v) => !v),
59
+ children: [
60
+ /* @__PURE__ */ jsx2(Chevron, { direction: expanded ? "up" : "down" }),
61
+ /* @__PURE__ */ jsx2("span", { className: "fds-mismatch-card__toggle-label", children: expanded ? "Show less" : "Show more" })
62
+ ]
63
+ }
64
+ )
65
+ ] });
66
+ }
67
+ function Chevron({ direction }) {
68
+ return /* @__PURE__ */ jsx2(
69
+ "svg",
70
+ {
71
+ className: `fds-mismatch-card__chevron fds-mismatch-card__chevron--${direction}`,
72
+ width: "20",
73
+ height: "20",
74
+ viewBox: "0 0 20 20",
75
+ fill: "none",
76
+ "aria-hidden": "true",
77
+ children: /* @__PURE__ */ jsx2(
78
+ "path",
79
+ {
80
+ d: "M6 8.5 10 12l4-3.5",
81
+ stroke: "currentColor",
82
+ strokeWidth: "1.6",
83
+ strokeLinecap: "round",
84
+ strokeLinejoin: "round"
85
+ }
86
+ )
87
+ }
88
+ );
89
+ }
90
+
91
+ // src/components/DiffSection/DiffSection.tsx
92
+ import { useState as useState2 } from "react";
93
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
94
+ function plural(n, one, many) {
95
+ return `${n} ${n === 1 ? one : many}`;
96
+ }
97
+ function DiffSection({
98
+ number,
99
+ name,
100
+ mismatchCount,
101
+ elements,
102
+ defaultExpanded = false,
103
+ headerless = false,
104
+ className,
105
+ ...rest
106
+ }) {
107
+ const [expanded, setExpanded] = useState2(defaultExpanded);
108
+ const open = headerless || expanded;
109
+ const count = mismatchCount ?? elements.reduce((sum, el) => sum + el.properties.length, 0);
110
+ const classes = [
111
+ "fds-diff-section",
112
+ headerless && "fds-diff-section--flat",
113
+ className
114
+ ].filter(Boolean).join(" ");
115
+ return /* @__PURE__ */ jsxs3(
116
+ "div",
117
+ {
118
+ className: classes,
119
+ "data-expanded": open ? "true" : void 0,
120
+ "data-flat": headerless ? "true" : void 0,
121
+ ...rest,
122
+ children: [
123
+ !headerless && /* @__PURE__ */ jsxs3(
124
+ "button",
125
+ {
126
+ type: "button",
127
+ className: "fds-diff-section__header",
128
+ "aria-expanded": expanded,
129
+ onClick: () => setExpanded((v) => !v),
130
+ children: [
131
+ /* @__PURE__ */ jsxs3("span", { className: "fds-diff-section__head-left", children: [
132
+ /* @__PURE__ */ jsx3(Chevron2, { open: expanded }),
133
+ number != null && /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__number", children: number }),
134
+ name != null && /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__name", children: name })
135
+ ] }),
136
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__count", children: plural(count, "mismatch", "mismatches") })
137
+ ]
138
+ }
139
+ ),
140
+ open && /* @__PURE__ */ jsx3("div", { className: "fds-diff-section__body", children: elements.map((el, ei) => /* @__PURE__ */ jsx3(ElementRow, { element: el }, `${el.name}-${ei}`)) })
141
+ ]
142
+ }
143
+ );
144
+ }
145
+ function ElementRow({ element }) {
146
+ const hasDetail = element.detail != null;
147
+ const [open, setOpen] = useState2(element.defaultOpen ?? false);
148
+ const lines = /* @__PURE__ */ jsxs3(Fragment, { children: [
149
+ /* @__PURE__ */ jsxs3("div", { className: "fds-diff-section__element-head-inner", children: [
150
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__element-name", children: element.name }),
151
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__element-count", children: plural(element.properties.length, "property", "properties") }),
152
+ hasDetail && /* @__PURE__ */ jsx3(Chevron2, { open, className: "fds-diff-section__element-caret" })
153
+ ] }),
154
+ /* @__PURE__ */ jsx3("div", { className: "fds-diff-section__lines", children: element.properties.map((line, li) => /* @__PURE__ */ jsxs3("div", { className: "fds-diff-section__line", children: [
155
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__prop", children: line.property }),
156
+ /* @__PURE__ */ jsxs3("span", { className: "fds-diff-section__delta", children: [
157
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__live", children: line.live }),
158
+ /* @__PURE__ */ jsx3(Arrow, {}),
159
+ /* @__PURE__ */ jsx3("span", { className: "fds-diff-section__design", children: line.design })
160
+ ] })
161
+ ] }, `${line.property}-${li}`)) })
162
+ ] });
163
+ return /* @__PURE__ */ jsxs3("div", { className: "fds-diff-section__element", "data-open": hasDetail && open ? "true" : void 0, children: [
164
+ hasDetail ? /* @__PURE__ */ jsx3(
165
+ "button",
166
+ {
167
+ type: "button",
168
+ className: "fds-diff-section__element-head fds-diff-section__element-head--button",
169
+ "aria-expanded": open,
170
+ onClick: () => setOpen((v) => !v),
171
+ children: lines
172
+ }
173
+ ) : /* @__PURE__ */ jsx3("div", { className: "fds-diff-section__element-head", children: lines }),
174
+ hasDetail && open && /* @__PURE__ */ jsx3("div", { className: "fds-diff-section__detail", children: element.detail })
175
+ ] });
176
+ }
177
+ function Chevron2({ open, className }) {
178
+ const classes = [
179
+ "fds-diff-section__chevron",
180
+ open && "fds-diff-section__chevron--open",
181
+ className
182
+ ].filter(Boolean).join(" ");
183
+ return /* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", className: classes, children: /* @__PURE__ */ jsx3(
184
+ "path",
185
+ {
186
+ d: "M4.5 3 8 6l-3.5 3",
187
+ stroke: "currentColor",
188
+ strokeWidth: "1.3",
189
+ strokeLinecap: "round",
190
+ strokeLinejoin: "round"
191
+ }
192
+ ) });
193
+ }
194
+ function Arrow() {
195
+ return /* @__PURE__ */ jsx3(
196
+ "svg",
197
+ {
198
+ className: "fds-diff-section__arrow",
199
+ width: "10",
200
+ height: "10",
201
+ viewBox: "0 0 10 10",
202
+ fill: "none",
203
+ "aria-hidden": "true",
204
+ children: /* @__PURE__ */ jsx3(
205
+ "path",
206
+ {
207
+ d: "M1.5 5h6.5M5.5 2.5 8 5 5.5 7.5",
208
+ stroke: "currentColor",
209
+ strokeWidth: "1",
210
+ strokeLinecap: "round",
211
+ strokeLinejoin: "round"
212
+ }
213
+ )
214
+ }
215
+ );
216
+ }
25
217
  export {
26
- Button
218
+ Button,
219
+ DiffSection,
220
+ MismatchCard
27
221
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@usefidel/design-system",
3
- "version": "0.2.0",
4
- "description": "Fidel brand design tokens + React component library — DTCG token source of truth, generated per-surface CSS + Tailwind-v4 parser-compatibility output, and on-brand React components.",
3
+ "version": "0.4.0",
4
+ "description": "Fidel brand design tokens + React component library — DTCG token source of truth, generated per-surface CSS + Tailwind-v4 parser-compatibility output, and on-brand React components (Button, MismatchCard, DiffSection).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",