@cahyo-dimas/freeday 1.17.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/COMPONENTS.md +748 -0
  3. package/README.id.md +20 -2
  4. package/README.md +32 -8
  5. package/USAGE.md +127 -0
  6. package/adapters/blazor/FdyTable.razor.cs +66 -7
  7. package/adapters/blazor/TableTypes.cs +6 -0
  8. package/adapters/react/components/FdyTable.tsx +43 -8
  9. package/adapters/vue/components/FdyDrawer.vue +5 -2
  10. package/adapters/vue/components/FdyModal.vue +5 -2
  11. package/adapters/vue/components/FdyTable.vue +42 -6
  12. package/dist/freeday.bundle.css +120 -9
  13. package/dist/freeday.css +103 -8
  14. package/dist/freeday.tokens.css +17 -1
  15. package/docs/agent-onboarding.md +151 -0
  16. package/docs/getting-started.md +454 -0
  17. package/docs/integrations.md +321 -0
  18. package/docs/reference-screen.html +461 -0
  19. package/package.json +11 -3
  20. package/src/components/avatar.css +8 -8
  21. package/src/components/card.css +3 -0
  22. package/src/components/chip.css +12 -0
  23. package/src/components/composition.css +49 -0
  24. package/src/components/list.css +29 -0
  25. package/tokens/breakpoints.d.ts +3 -0
  26. package/tokens/breakpoints.mjs +8 -1
  27. package/tokens/tokens.json +14 -2
  28. package/src/components/.gitkeep +0 -0
  29. package/src/freeday-autocomplete.js +0 -135
  30. package/src/freeday-breakpoint.js +0 -51
  31. package/src/freeday-carousel.js +0 -111
  32. package/src/freeday-cascade.js +0 -256
  33. package/src/freeday-cfl.js +0 -213
  34. package/src/freeday-chart.js +0 -429
  35. package/src/freeday-chip.js +0 -83
  36. package/src/freeday-datepicker.js +0 -321
  37. package/src/freeday-datetime.js +0 -83
  38. package/src/freeday-drawer.js +0 -43
  39. package/src/freeday-form.js +0 -181
  40. package/src/freeday-mask.js +0 -114
  41. package/src/freeday-menu.js +0 -93
  42. package/src/freeday-popover.js +0 -69
  43. package/src/freeday-rating.js +0 -50
  44. package/src/freeday-select.js +0 -218
  45. package/src/freeday-slider.js +0 -34
  46. package/src/freeday-stepper.js +0 -90
  47. package/src/freeday-table.js +0 -475
  48. package/src/freeday-tabs.js +0 -68
  49. package/src/freeday-timepicker.js +0 -180
  50. package/src/freeday-toast.js +0 -105
  51. package/src/freeday-tree.js +0 -94
  52. package/src/freeday-upload.js +0 -206
@@ -166,6 +166,14 @@
166
166
  --chart-8: #e34948;
167
167
  --chart-grid: var(--slate-200);
168
168
  --chart-tick: var(--slate-400);
169
+ --tone-1: var(--chart-1);
170
+ --tone-2: var(--chart-2);
171
+ --tone-3: var(--chart-3);
172
+ --tone-4: var(--chart-4);
173
+ --tone-5: var(--chart-5);
174
+ --tone-6: var(--chart-6);
175
+ --tone-7: var(--chart-7);
176
+ --tone-8: var(--chart-8);
169
177
  --focus-ring: var(--azure-600);
170
178
  --focus-ring-width: 2px;
171
179
  }
@@ -339,7 +347,15 @@
339
347
  --chart-tick: var(--slate-400);
340
348
  --focus-ring: var(--azure-600);
341
349
  }
342
- :root[data-density="compact"] {
350
+ /* Density is deliberately NOT scoped to :root. These are custom properties, so they inherit —
351
+ * putting the attribute on any ancestor (a route wrapper, one section) densifies just that
352
+ * subtree, which is how density is actually decided: per screen, not per app. The root still
353
+ * matches, so setting data-density on the html element keeps working exactly as before. */
354
+ [data-density="compact"] {
355
+ --space-3: 0.625rem;
356
+ --space-4: 0.75rem;
357
+ --space-5: 1rem;
358
+ --space-6: 1.25rem;
343
359
  --control-h: 2rem;
344
360
  }
345
361
 
@@ -534,14 +550,14 @@ a { color: var(--color-primary); }
534
550
  * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
535
551
  * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
536
552
  .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
537
- .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
538
- .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
539
- .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
540
- .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
541
- .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
542
- .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
543
- .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
544
- .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
553
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--tone-1);}
554
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--tone-2);}
555
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--tone-3);}
556
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--tone-4);}
557
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--tone-5);}
558
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--tone-6);}
559
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--tone-7);}
560
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--tone-8);}
545
561
 
546
562
  /* Freeday — Badge */
547
563
  .fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
@@ -661,6 +677,9 @@ a { color: var(--color-primary); }
661
677
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
662
678
  .fdy-card--interactive{cursor:pointer;}
663
679
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
680
+ /* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
681
+ * doesn't jump. The shadow still communicates the affordance. */
682
+ @media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
664
683
  /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
665
684
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
666
685
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
@@ -816,6 +835,18 @@ a { color: var(--color-primary); }
816
835
  /* Freeday — Chip (tag / removable filter) */
817
836
  .fdy-chip{display:inline-flex;align-items:center;gap:var(--space-2);height:1.75rem;padding:0 var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--color-text);background:var(--color-surface-3);border:var(--bw) solid var(--color-border);}
818
837
  .fdy-chip--primary{color:var(--color-primary-strong);background:var(--color-primary-soft);border-color:color-mix(in srgb,var(--color-primary) 22%,transparent);}
838
+ /* Categorical tone chips — non-semantic category tags / legend swatches, from the general
839
+ * --tone-1..8 palette. Same validated AA formula as .fdy-avatar--tone-* (text >=4.5:1 both themes,
840
+ * gated by test/contrast.test.mjs). */
841
+ .fdy-chip--tone-1,.fdy-chip--tone-2,.fdy-chip--tone-3,.fdy-chip--tone-4,.fdy-chip--tone-5,.fdy-chip--tone-6,.fdy-chip--tone-7,.fdy-chip--tone-8{background:color-mix(in srgb,var(--_fdy-chip-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-chip-tone) 50%,var(--color-text));border-color:color-mix(in srgb,var(--_fdy-chip-tone) 30%,var(--color-border));}
842
+ .fdy-chip--tone-1{--_fdy-chip-tone:var(--tone-1);}
843
+ .fdy-chip--tone-2{--_fdy-chip-tone:var(--tone-2);}
844
+ .fdy-chip--tone-3{--_fdy-chip-tone:var(--tone-3);}
845
+ .fdy-chip--tone-4{--_fdy-chip-tone:var(--tone-4);}
846
+ .fdy-chip--tone-5{--_fdy-chip-tone:var(--tone-5);}
847
+ .fdy-chip--tone-6{--_fdy-chip-tone:var(--tone-6);}
848
+ .fdy-chip--tone-7{--_fdy-chip-tone:var(--tone-7);}
849
+ .fdy-chip--tone-8{--_fdy-chip-tone:var(--tone-8);}
819
850
  .fdy-chip__remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;margin-right:calc(var(--space-1)*-1);border:0;padding:0;border-radius:var(--radius-full);background:transparent;color:currentColor;opacity:.6;cursor:pointer;font-size:var(--text-sm);line-height:1;}
820
851
  .fdy-chip__remove:hover{opacity:1;background:color-mix(in srgb,currentColor 16%,transparent);}
821
852
  .fdy-chip__remove:focus-visible{outline:none;opacity:1;box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -865,6 +896,56 @@ a { color: var(--color-primary); }
865
896
  @media (prefers-reduced-motion:no-preference){.fdy-combo__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
866
897
  @keyframes fdy-combo-in{from{opacity:0;transform:translateY(-4px);}to{opacity:1;transform:none;}}
867
898
 
899
+ /* Freeday — Page composition & type roles.
900
+ *
901
+ * The kit stops at the component; these primitives encode *how a page is assembled* so screens
902
+ * built by different people cohere. They carry the usage doctrine (see USAGE.md) in markup: a
903
+ * named type role gets used, a doc gets skimmed. Everything here is token-driven layout — no new
904
+ * colours, no new shadows. Pair with .fdy-app (the shell) and the components inside.
905
+ */
906
+
907
+ /* ---- Type roles (three title levels + supporting text) -------------------------------------
908
+ * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
909
+ * page title (the mistake that collapses three levels into one grey mush). */
910
+ .fdy-eyebrow{font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
911
+ .fdy-title-page{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:var(--leading-tight);color:var(--color-text);margin:0;text-wrap:balance;}
912
+ .fdy-title-section{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-snug);color:var(--color-text);margin:0;text-wrap:balance;}
913
+ .fdy-title-card{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0;}
914
+ .fdy-text-muted{color:var(--color-text-muted);}
915
+ .fdy-text-subtle{color:var(--color-text-subtle);}
916
+ .fdy-text-caption{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;}
917
+
918
+ /* ---- Page: the vertical stack of sections, with the section rhythm between them ------------- */
919
+ .fdy-page{display:flex;flex-direction:column;gap:var(--space-8);}
920
+
921
+ /* ---- Page header: eyebrow + title + description on the left, primary action(s) on the right -- */
922
+ .fdy-page__header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;}
923
+ .fdy-page__heading{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;}
924
+ .fdy-page__desc{font-size:var(--text-base);color:var(--color-text-muted);margin:0;max-width:62ch;line-height:var(--leading-normal);}
925
+ .fdy-page__actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
926
+
927
+ /* ---- Page section: a heading (+ optional toolbar) and its body, with the intra-group rhythm --- */
928
+ .fdy-page-section{display:flex;flex-direction:column;gap:var(--space-4);}
929
+ .fdy-page-section__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;}
930
+
931
+ /* ---- Toolbar: a horizontal control row (search + actions), wraps cleanly ---------------------- */
932
+ .fdy-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
933
+ .fdy-toolbar__spacer{flex:1;}
934
+ .fdy-toolbar__search{max-width:18rem;flex:1 1 12rem;}
935
+
936
+ /* ---- Stat tile: a KPI metric that is deliberately NOT a card, so a KPI strip doesn't become an
937
+ * identical-white-card grid. Flat by default; wrap the .fdy-stats grid in one card if you want a
938
+ * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
939
+ .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
940
+ .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
941
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
942
+ .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
943
+ .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
944
+ .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
945
+ .fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
946
+ .fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
947
+ .fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
948
+
868
949
  /* Freeday — Date picker (input-styled trigger + calendar popover).
869
950
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
870
951
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
@@ -1107,6 +1188,36 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1107
1188
  /* Freeday — Keyboard key */
1108
1189
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
1109
1190
 
1191
+ /* Freeday — List: the FLAT row container.
1192
+ *
1193
+ * USAGE.md §3 puts list rows among the surfaces that should be flat, but until now the only
1194
+ * container the kit shipped was .fdy-card — which carries --shadow-lift (a real 34px lift). A
1195
+ * responsive table that becomes a list below `md` therefore had to choose between a stack of
1196
+ * shadowed cards (against the doctrine) or a hand-built box, which needs a colour and so escapes
1197
+ * the token system. This is that missing container: one flat surface, hairline dividers, no shadow.
1198
+ *
1199
+ * Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
1200
+ * Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
1201
+ .fdy-list{list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
1202
+ .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
1203
+ /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
1204
+ * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
1205
+ * <li>s — a bare `.fdy-list__row + .fdy-list__row` silently matches nothing there). */
1206
+ .fdy-list__row + .fdy-list__row,
1207
+ .fdy-list > * + * > .fdy-list__row{border-top:var(--bw) solid var(--color-border-muted);}
1208
+ /* A row that is itself the control: render it as <button>/<a> and the UA box is reset without
1209
+ * touching the list surface (same contract as .fdy-card--button). */
1210
+ .fdy-list__row--button{width:100%;text-align:inherit;color:inherit;font:inherit;background:none;border:0;appearance:none;-webkit-appearance:none;cursor:pointer;}
1211
+ .fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
1212
+ .fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
1213
+ .fdy-list__row--interactive{cursor:pointer;}
1214
+ /* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
1215
+ .fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
1216
+ .fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1217
+ .fdy-list__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
1218
+ .fdy-list__aside{flex:none;display:flex;align-items:center;gap:var(--space-2);}
1219
+ /* Density: rows tighten with the rest of the kit (--space-* mid-range steps under compact). */
1220
+
1110
1221
  /* Freeday — Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
1111
1222
  .fdy-menu-wrap{position:relative;display:inline-flex;}
1112
1223
  .fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
package/dist/freeday.css CHANGED
@@ -189,14 +189,14 @@ a { color: var(--color-primary); }
189
189
  * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
190
190
  * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
191
191
  .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
192
- .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
193
- .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
194
- .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
195
- .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
196
- .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
197
- .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
198
- .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
199
- .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
192
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--tone-1);}
193
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--tone-2);}
194
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--tone-3);}
195
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--tone-4);}
196
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--tone-5);}
197
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--tone-6);}
198
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--tone-7);}
199
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--tone-8);}
200
200
 
201
201
  /* Freeday — Badge */
202
202
  .fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
@@ -316,6 +316,9 @@ a { color: var(--color-primary); }
316
316
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
317
317
  .fdy-card--interactive{cursor:pointer;}
318
318
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
319
+ /* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
320
+ * doesn't jump. The shadow still communicates the affordance. */
321
+ @media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
319
322
  /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
320
323
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
321
324
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
@@ -471,6 +474,18 @@ a { color: var(--color-primary); }
471
474
  /* Freeday — Chip (tag / removable filter) */
472
475
  .fdy-chip{display:inline-flex;align-items:center;gap:var(--space-2);height:1.75rem;padding:0 var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--color-text);background:var(--color-surface-3);border:var(--bw) solid var(--color-border);}
473
476
  .fdy-chip--primary{color:var(--color-primary-strong);background:var(--color-primary-soft);border-color:color-mix(in srgb,var(--color-primary) 22%,transparent);}
477
+ /* Categorical tone chips — non-semantic category tags / legend swatches, from the general
478
+ * --tone-1..8 palette. Same validated AA formula as .fdy-avatar--tone-* (text >=4.5:1 both themes,
479
+ * gated by test/contrast.test.mjs). */
480
+ .fdy-chip--tone-1,.fdy-chip--tone-2,.fdy-chip--tone-3,.fdy-chip--tone-4,.fdy-chip--tone-5,.fdy-chip--tone-6,.fdy-chip--tone-7,.fdy-chip--tone-8{background:color-mix(in srgb,var(--_fdy-chip-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-chip-tone) 50%,var(--color-text));border-color:color-mix(in srgb,var(--_fdy-chip-tone) 30%,var(--color-border));}
481
+ .fdy-chip--tone-1{--_fdy-chip-tone:var(--tone-1);}
482
+ .fdy-chip--tone-2{--_fdy-chip-tone:var(--tone-2);}
483
+ .fdy-chip--tone-3{--_fdy-chip-tone:var(--tone-3);}
484
+ .fdy-chip--tone-4{--_fdy-chip-tone:var(--tone-4);}
485
+ .fdy-chip--tone-5{--_fdy-chip-tone:var(--tone-5);}
486
+ .fdy-chip--tone-6{--_fdy-chip-tone:var(--tone-6);}
487
+ .fdy-chip--tone-7{--_fdy-chip-tone:var(--tone-7);}
488
+ .fdy-chip--tone-8{--_fdy-chip-tone:var(--tone-8);}
474
489
  .fdy-chip__remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;margin-right:calc(var(--space-1)*-1);border:0;padding:0;border-radius:var(--radius-full);background:transparent;color:currentColor;opacity:.6;cursor:pointer;font-size:var(--text-sm);line-height:1;}
475
490
  .fdy-chip__remove:hover{opacity:1;background:color-mix(in srgb,currentColor 16%,transparent);}
476
491
  .fdy-chip__remove:focus-visible{outline:none;opacity:1;box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -520,6 +535,56 @@ a { color: var(--color-primary); }
520
535
  @media (prefers-reduced-motion:no-preference){.fdy-combo__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
521
536
  @keyframes fdy-combo-in{from{opacity:0;transform:translateY(-4px);}to{opacity:1;transform:none;}}
522
537
 
538
+ /* Freeday — Page composition & type roles.
539
+ *
540
+ * The kit stops at the component; these primitives encode *how a page is assembled* so screens
541
+ * built by different people cohere. They carry the usage doctrine (see USAGE.md) in markup: a
542
+ * named type role gets used, a doc gets skimmed. Everything here is token-driven layout — no new
543
+ * colours, no new shadows. Pair with .fdy-app (the shell) and the components inside.
544
+ */
545
+
546
+ /* ---- Type roles (three title levels + supporting text) -------------------------------------
547
+ * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
548
+ * page title (the mistake that collapses three levels into one grey mush). */
549
+ .fdy-eyebrow{font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
550
+ .fdy-title-page{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:var(--leading-tight);color:var(--color-text);margin:0;text-wrap:balance;}
551
+ .fdy-title-section{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-snug);color:var(--color-text);margin:0;text-wrap:balance;}
552
+ .fdy-title-card{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0;}
553
+ .fdy-text-muted{color:var(--color-text-muted);}
554
+ .fdy-text-subtle{color:var(--color-text-subtle);}
555
+ .fdy-text-caption{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;}
556
+
557
+ /* ---- Page: the vertical stack of sections, with the section rhythm between them ------------- */
558
+ .fdy-page{display:flex;flex-direction:column;gap:var(--space-8);}
559
+
560
+ /* ---- Page header: eyebrow + title + description on the left, primary action(s) on the right -- */
561
+ .fdy-page__header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;}
562
+ .fdy-page__heading{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;}
563
+ .fdy-page__desc{font-size:var(--text-base);color:var(--color-text-muted);margin:0;max-width:62ch;line-height:var(--leading-normal);}
564
+ .fdy-page__actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
565
+
566
+ /* ---- Page section: a heading (+ optional toolbar) and its body, with the intra-group rhythm --- */
567
+ .fdy-page-section{display:flex;flex-direction:column;gap:var(--space-4);}
568
+ .fdy-page-section__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;}
569
+
570
+ /* ---- Toolbar: a horizontal control row (search + actions), wraps cleanly ---------------------- */
571
+ .fdy-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
572
+ .fdy-toolbar__spacer{flex:1;}
573
+ .fdy-toolbar__search{max-width:18rem;flex:1 1 12rem;}
574
+
575
+ /* ---- Stat tile: a KPI metric that is deliberately NOT a card, so a KPI strip doesn't become an
576
+ * identical-white-card grid. Flat by default; wrap the .fdy-stats grid in one card if you want a
577
+ * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
578
+ .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
579
+ .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
580
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
581
+ .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
582
+ .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
583
+ .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
584
+ .fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
585
+ .fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
586
+ .fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
587
+
523
588
  /* Freeday — Date picker (input-styled trigger + calendar popover).
524
589
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
525
590
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
@@ -762,6 +827,36 @@ fieldset.fdy-field>legend{padding:0;float:none;}
762
827
  /* Freeday — Keyboard key */
763
828
  .fdy-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:1.5rem;height:1.5rem;padding:0 var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text);background:var(--color-surface-2);border:var(--bw) solid var(--color-border-strong);border-bottom-width:2px;border-radius:var(--radius-sm);}
764
829
 
830
+ /* Freeday — List: the FLAT row container.
831
+ *
832
+ * USAGE.md §3 puts list rows among the surfaces that should be flat, but until now the only
833
+ * container the kit shipped was .fdy-card — which carries --shadow-lift (a real 34px lift). A
834
+ * responsive table that becomes a list below `md` therefore had to choose between a stack of
835
+ * shadowed cards (against the doctrine) or a hand-built box, which needs a colour and so escapes
836
+ * the token system. This is that missing container: one flat surface, hairline dividers, no shadow.
837
+ *
838
+ * Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
839
+ * Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
840
+ .fdy-list{list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
841
+ .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
842
+ /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
843
+ * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
844
+ * <li>s — a bare `.fdy-list__row + .fdy-list__row` silently matches nothing there). */
845
+ .fdy-list__row + .fdy-list__row,
846
+ .fdy-list > * + * > .fdy-list__row{border-top:var(--bw) solid var(--color-border-muted);}
847
+ /* A row that is itself the control: render it as <button>/<a> and the UA box is reset without
848
+ * touching the list surface (same contract as .fdy-card--button). */
849
+ .fdy-list__row--button{width:100%;text-align:inherit;color:inherit;font:inherit;background:none;border:0;appearance:none;-webkit-appearance:none;cursor:pointer;}
850
+ .fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
851
+ .fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
852
+ .fdy-list__row--interactive{cursor:pointer;}
853
+ /* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
854
+ .fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
855
+ .fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
856
+ .fdy-list__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
857
+ .fdy-list__aside{flex:none;display:flex;align-items:center;gap:var(--space-2);}
858
+ /* Density: rows tighten with the rest of the kit (--space-* mid-range steps under compact). */
859
+
765
860
  /* Freeday — Menu (action popup) + Split button. Enhanced by freeday-menu.js. */
766
861
  .fdy-menu-wrap{position:relative;display:inline-flex;}
767
862
  .fdy-menu{position:absolute;top:calc(100% + var(--space-1));left:0;z-index:130;min-width:11rem;margin:0;padding:var(--space-1);list-style:none;background:var(--color-surface);color:var(--color-text);border:var(--bw) solid var(--color-border-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-3);}
@@ -165,6 +165,14 @@
165
165
  --chart-8: #e34948;
166
166
  --chart-grid: var(--slate-200);
167
167
  --chart-tick: var(--slate-400);
168
+ --tone-1: var(--chart-1);
169
+ --tone-2: var(--chart-2);
170
+ --tone-3: var(--chart-3);
171
+ --tone-4: var(--chart-4);
172
+ --tone-5: var(--chart-5);
173
+ --tone-6: var(--chart-6);
174
+ --tone-7: var(--chart-7);
175
+ --tone-8: var(--chart-8);
168
176
  --focus-ring: var(--azure-600);
169
177
  --focus-ring-width: 2px;
170
178
  }
@@ -338,6 +346,14 @@
338
346
  --chart-tick: var(--slate-400);
339
347
  --focus-ring: var(--azure-600);
340
348
  }
341
- :root[data-density="compact"] {
349
+ /* Density is deliberately NOT scoped to :root. These are custom properties, so they inherit —
350
+ * putting the attribute on any ancestor (a route wrapper, one section) densifies just that
351
+ * subtree, which is how density is actually decided: per screen, not per app. The root still
352
+ * matches, so setting data-density on the html element keeps working exactly as before. */
353
+ [data-density="compact"] {
354
+ --space-3: 0.625rem;
355
+ --space-4: 0.75rem;
356
+ --space-5: 1rem;
357
+ --space-6: 1.25rem;
342
358
  --control-h: 2rem;
343
359
  }
@@ -0,0 +1,151 @@
1
+ # Freeday — AI agent onboarding
2
+
3
+ For **a coding agent working in a project that consumes Freeday** (Claude Code, Codex, Cursor,
4
+ Copilot…). No model has Freeday in its training data, so an agent that is merely told "use Freeday"
5
+ will invent class names or silently fall back to Bootstrap/Tailwind conventions. This file is the
6
+ fix: paste the block below into the consuming project's agent instruction file, once.
7
+
8
+ > Working on **the kit itself**, not a consuming app? That's [`../CLAUDE.md`](../CLAUDE.md) — this
9
+ > file is about *using* the published package.
10
+
11
+ ---
12
+
13
+ ## 1. Paste this into your project's agent instructions
14
+
15
+ Into `CLAUDE.md` (Claude Code) / `AGENTS.md` (Codex, others) / `.github/copilot-instructions.md` at
16
+ the **root of the consuming project**:
17
+
18
+ ```markdown
19
+ ## UI: Freeday design system (@cahyo-dimas/freeday)
20
+
21
+ All UI in this project is built from Freeday. It is a **token-driven CSS kit + zero-dependency JS
22
+ enhancers**, not a component framework — components are plain markup with `fdy-*` classes.
23
+
24
+ **Before writing or editing any markup/CSS, read these (they ship inside the package):**
25
+ - `node_modules/@cahyo-dimas/freeday/COMPONENTS.md` — every class that exists, with minimal markup
26
+ skeletons, enhancer hooks and the a11y contract per component. **The class list is closed:
27
+ if a class is not in that file, it does not exist — do not invent one.**
28
+ - `node_modules/@cahyo-dimas/freeday/USAGE.md` — the doctrine: which token/role/shadow to use when.
29
+ - `node_modules/@cahyo-dimas/freeday/docs/reference-screen.html` — one complete screen, assembled
30
+ the intended way. Copy this structure for a new screen.
31
+
32
+ **Non-negotiables:**
33
+ 1. No raw hex or px in app CSS. Use tokens: `var(--color-primary)`, `var(--space-4)` (4px scale),
34
+ `var(--radius-md)`, `var(--shadow-1)`, `var(--dur-2)`.
35
+ 2. Components only touch semantic tokens (`--color-*`) — never the primitive ramp (`--azure-600`).
36
+ 3. `.fdy-btn` is already the primary action (there is no `--primary`). One per screen; everything
37
+ else is `--ghost` or `--text`.
38
+ 4. Three title roles only: `.fdy-title-page` (one `<h1>`) / `.fdy-title-section` / `.fdy-title-card`.
39
+ Never reuse a card title as a page title.
40
+ 5. Assemble from the frame down: `.fdy-app` → `.fdy-page` → `.fdy-page-section` → components.
41
+ 6. Form errors: `aria-invalid="true"` + `aria-describedby` → a `.fdy-help.fdy-help--error`.
42
+ Icon-only buttons need `aria-label`. Status is never colour-only.
43
+ 7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md);
44
+ in an SPA, re-hydrate dynamic DOM (`useFreeday` in Vue/React does this).
45
+ 8. Freeday owns components + tokens, **not layout**. Grids/stacks/one-off gaps come from our own
46
+ layout layer — build its theme on `var(--space-N)` so both systems stay in step.
47
+ ```
48
+
49
+ Adjust the paths if the package lives somewhere else (a workspace, a vendored copy, `wwwroot/` for
50
+ Blazor). Then verify the agent can actually read those files — an agent that can't open
51
+ `node_modules` will keep guessing.
52
+
53
+ ## 2. What ships in the package
54
+
55
+ | File | What it answers |
56
+ |---|---|
57
+ | `COMPONENTS.md` | The complete class surface — what exists, its modifiers, minimal markup, a11y. |
58
+ | `USAGE.md` | The doctrine — which token/role/shadow/emphasis to use when. |
59
+ | `docs/getting-started.md` | Install + import + theme, per stack (Static HTML · Vue · React · Blazor). |
60
+ | `docs/integrations.md` | How to bridge third-party libraries (validation, charts, dates, i18n…). |
61
+ | `docs/reference-screen.html` | A full screen assembled from the shell down. Open it in a browser. |
62
+ | `docs/agent-onboarding.md` | This file. |
63
+ | `dist/` | Built CSS + enhancers. **`freeday.bundle.css` = tokens + components** (what `@cahyo-dimas/freeday/css` resolves to); `freeday.css` is components **only**, `freeday.tokens.css` tokens only — linking `freeday.css` alone leaves every `var(--…)` unresolved. Plus `freeday-*.js` and the `.d.ts` files. |
64
+ | `src/components/*.css` | The authoritative source for every class, when a doc is ambiguous. |
65
+ | `tokens/tokens.json` | Every token in W3C DTCG format — machine-readable. |
66
+ | `adapters/vue` · `adapters/react` · `adapters/blazor` | Typed wrappers, 10 components each. |
67
+
68
+ The live docs (with an interactive playground) are at
69
+ <https://cahyo-dimas.github.io/freeday-ui-kit/>, and the repo — including three complete example
70
+ apps under `examples/` (Vue, React, Blazor) that are **not** in the npm tarball — is at
71
+ <https://github.com/cahyo-dimas/freeday-ui-kit>.
72
+
73
+ ## 3. Starting a new screen
74
+
75
+ The order matters; skipping to components is what produces flat, identical-card screens.
76
+
77
+ 0. **Pick the screen shape first.** Which archetype is this — dashboard, master-detail, kanban,
78
+ wizard, POS…? The repo's
79
+ [`reference/README.md`](https://github.com/cahyo-dimas/freeday-ui-kit/blob/main/reference/README.md)
80
+ maps 15 archetypes to the exact primitives that compose each one, and says plainly which shapes
81
+ the kit has **no** component for (kanban columns, calendar month grid, chat bubbles, canvas) so
82
+ you build the frame instead of inventing a class. Not in the npm package — read it on GitHub.
83
+ 1. **Shell** — is `.fdy-app` already in place (usually once, in the app layout)? If not, copy it
84
+ from `docs/getting-started.md` §The app shell.
85
+ 2. **Theme** — `data-theme="light|dark"` + `data-density="comfortable|compact"` on `<html>`, set
86
+ once at the root. Use `compact` for table-heavy back-office screens.
87
+ 3. **Fonts** — the package ships **no** `@font-face`. Load Sora / IBM Plex Sans / JetBrains Mono
88
+ yourself, or override `--font-display`/`--font-body`/`--font-mono`. Skipping this reads as
89
+ "unfinished design", not "missing dependency".
90
+ 4. **Page frame** — `.fdy-page` + `.fdy-page__header` (eyebrow + `.fdy-title-page` + desc on the
91
+ left, **one** primary action on the right).
92
+ 5. **Sections** — one `.fdy-page-section` per region, each with a `.fdy-title-section`.
93
+ 6. **Components** — from `COMPONENTS.md`, inside the sections.
94
+ 7. **Verify** — the checklist in §5.
95
+
96
+ ## 4. Migrating an existing UI to Freeday
97
+
98
+ Migration is a **class-and-structure swap**, not a rewrite. Keep the app's DOM semantics; replace
99
+ the styling layer. Rough equivalents — always confirm the target class in `COMPONENTS.md`, and note
100
+ that Freeday deliberately has **no** layout/spacing utilities, so grid/flex/margin classes stay with
101
+ your own layout layer:
102
+
103
+ | Coming from | Freeday |
104
+ |---|---|
105
+ | `btn btn-primary` / `MudButton Variant=Filled` | `fdy-btn` |
106
+ | `btn btn-secondary` / `btn-outline-*` | `fdy-btn fdy-btn--ghost` |
107
+ | `btn btn-danger` | `fdy-btn fdy-btn--danger` |
108
+ | `btn btn-link` | `fdy-btn fdy-btn--text` |
109
+ | `btn-sm` / `btn-lg` | `fdy-btn--sm` / `fdy-btn--lg` |
110
+ | `form-control` / `MudTextField` | `fdy-field` + `fdy-label` + `fdy-input` |
111
+ | `form-select` / `<select>` / `MudSelect` | `fdy-combo` + `data-fdy-combo` (+ `freeday-select.js`) |
112
+ | `invalid-feedback` / `is-invalid` | `aria-invalid="true"` + `fdy-help fdy-help--error` |
113
+ | `input-group` / `input-group-text` | `fdy-input-group` + `__addon` / `__btn` |
114
+ | `form-check` / `form-switch` | `fdy-check` / `fdy-radio` / `fdy-switch` |
115
+ | `card` / `card-body` / `card-title` | `fdy-card` / `__body` / `__title` |
116
+ | `table table-striped` / `MudTable` | `fdy-table` in `fdy-table-wrap`; interactive → `fdy-datatable` |
117
+ | `badge bg-success` / `MudChip` (status) | `fdy-badge fdy-badge--success` |
118
+ | `alert alert-danger` | `fdy-alert fdy-alert--danger` + `role="alert"` |
119
+ | `modal` / `MudDialog` | `<dialog class="fdy-modal">` (native — drop the JS backdrop plumbing) |
120
+ | `offcanvas` / `MudDrawer` | `<dialog class="fdy-drawer">` + `data-fdy-drawer` |
121
+ | `nav nav-tabs` | `fdy-tabs` + `data-fdy-tabs` |
122
+ | `breadcrumb` / `pagination` | `fdy-breadcrumb` / `fdy-pagination` (same `<nav><ol>` structure) |
123
+ | `spinner-border` / `progress` | `fdy-spinner` / `fdy-progress` + `__bar` |
124
+ | `toast` container + JS | `Freeday.toast({…})` — imperative, no markup to author |
125
+ | `text-muted` | `fdy-text-muted` |
126
+ | `d-none` / `visually-hidden` | `fdy-hidden` / `fdy-visually-hidden` |
127
+ | `container` / `row` / `col-*` / `mb-3` / `gap-2` | **stays yours** — Freeday ships no layout utilities |
128
+
129
+ Order of work that avoids a half-migrated mess:
130
+
131
+ 1. Load Freeday's CSS and **turn off the old framework's reset/preflight** — `base.css` is the
132
+ reset now. Two resets fighting is the usual source of "everything looks slightly off".
133
+ 2. Shell + theme attributes first, so tokens resolve everywhere.
134
+ 3. Then screen by screen: page frame → sections → controls. Convert a whole screen at a time;
135
+ half-converted screens can't be reviewed visually.
136
+ 4. Delete the old framework's CSS only when no screen references it, then grep for leftover class
137
+ prefixes.
138
+ 5. Replace hand-rolled modal/drawer/dropdown JS with the native-`<dialog>` components and the
139
+ enhancers — that is usually where the most code disappears.
140
+
141
+ ## 5. Verification checklist (before claiming a screen is done)
142
+
143
+ - Every `fdy-*` class used appears in `COMPONENTS.md`. Grep the diff for `fdy-` and check.
144
+ - No raw hex/rgb/px in the diff's CSS. Grep for `#` and `px`.
145
+ - Exactly one `.fdy-btn` without a variant modifier on the screen; one `.fdy-title-page`.
146
+ - Toggle `data-theme="dark"` on `<html>` — nothing becomes unreadable, no hard-coded white/black.
147
+ - Toggle `data-density="compact"` — the layout still holds.
148
+ - Keyboard: Tab reaches every control, focus is always visible, Esc closes overlays.
149
+ - Form errors carry `aria-invalid` + a linked message; icon-only buttons have `aria-label`.
150
+ - The interactive components on the screen have their enhancer loaded, and SPA-rendered DOM is
151
+ re-hydrated.