@cahyo-dimas/freeday 1.32.0 → 1.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/CHANGELOG.md CHANGED
@@ -3,6 +3,73 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.33.0] — 2026-08-18
7
+ Three notes from the back-office app, four findings, all four executed.
8
+ ### Added
9
+ - **`pager={false}` on `FdyTable`** (Vue · React · Blazor). Server mode rendered its own footer
10
+ unconditionally: `hasPager` was `pageSize > 0 && totalPages > 1` with no way in, so a responsive
11
+ list — a table at `lg`, `.fdy-list` below it, one pager serving both so the views cannot disagree —
12
+ showed the kit's footer *and* the app's, stacked, on every list with more than one page. The only
13
+ workaround was `display:none` on `.fdy-table-footer`, an app reaching into a component's internals.
14
+ Client mode already had its hatch (`pageIndex`); this gives server mode one, and in server mode it
15
+ is the honest shape — the app owns the page there anyway, it was simply being handed a second
16
+ control.
17
+ - **`usePopover` is exported from the Vue adapter.** Every kit dropdown uses it to escape an
18
+ ancestor's overflow clip (`.fdy-card` is `overflow:hidden`, so a panel inside one is cut at the
19
+ card's edge), and a consuming app that had to build a control the kit does not ship had to
20
+ re-implement it — ~70 lines, from the kit's own description, that will not improve when the kit's
21
+ positioning does. *Correction to the report: React has exported it all along; only Vue was missing
22
+ it. Blazor has no composable layer — its panels go through the JS bridge.*
23
+ - **Quiet destructive buttons.** `--danger` was the solid treatment and nothing else, so
24
+ `fdy-btn--ghost fdy-btn--danger` — which every reader parses as "quiet destructive" — rendered a
25
+ second SOLID button. That is a hierarchy defect: the base class is already the one primary per
26
+ screen, and a solid Delete beside Save is a second primary in all but name.
27
+ `.fdy-menu__item--danger` has always modelled the quiet form; it now reaches `.fdy-btn`.
28
+ ### Fixed
29
+ - **`.fdy-eyebrow` and `.fdy-nav__grouplabel` spend a text ink.** Both set type in
30
+ `--color-text-subtle`, which is gated at 3.0 *by design* (placeholders, separators, decorative
31
+ glyphs) and measures 4.41:1 — axe reports it as a serious `color-contrast` failure. Identical to
32
+ `.fdy-stat__label` in 1.30.0, one release earlier: same ink, same 4.41, same argument.
33
+ The nav group label is the one the report did not know about — the sweep it asked for found it,
34
+ and that rule was *already* brightening to `--color-text-muted` on hover, which is the CSS
35
+ admitting its resting state was too faint.
36
+ ### Notes on the shape of the fix
37
+ - The report proposed scoping the solid danger with `:not(.fdy-btn--ghost):not(.fdy-btn--text)`.
38
+ Shipped without it: the pairing rules are more specific and come later, so they already win —
39
+ the `:not()` would be a second mechanism expressing the same intent, and the mutation run proved
40
+ it changed nothing.
41
+ - **A regression written and caught inside this change:** Vue casts an omitted Boolean prop to
42
+ `false`, not `undefined`, so `props.pager !== false` withheld the footer from every table that
43
+ never mentions `pager`. The existing controlled-`pageIndex` guard failed within seconds. `pager`
44
+ now goes through `withDefaults`, the same trap `FdyModal`'s `dismissible` hit in v1.18.0.
45
+ ### Added — guards
46
+ - **The prose-ink invariant** note 007 asked for: any rule that sets a `font-size` *and* spends
47
+ `--color-text-subtle` is typesetting prose in an ink gated for decoration, and fails. Matched by
48
+ shape, not by a list of names, so the third instance cannot arrive the way the second did. Two
49
+ documented exemptions carry their reason.
50
+ - `pager={false}` is measured in a real browser with two tables sharing one server page state: the
51
+ footer must be **absent from the DOM**, not hidden (a visually hidden pager is still a tab stop
52
+ and still announced), and the other table's pager must still work.
53
+ - Quiet destructive is measured as a rendered `background-image`, the same reason the `aria-pressed`
54
+ fills are: a gradient is a background-*image*, so the CSS reads fine either way and only the engine
55
+ shows which treatment won.
56
+
57
+ ## [1.32.1] — 2026-08-18
58
+ Docs only. Asked by a consumer: *"if I upgrade, will the agent in my project know what it gained?"*
59
+ The answer was no, and two reasons why.
60
+ ### Fixed
61
+ - **`docs/agent-onboarding.md` never mentioned the changelog.** It is the file a consuming agent
62
+ reads first, and it listed everything in the package except the one file that says what changed
63
+ between the version the project had and the one it now has. It now points at
64
+ `node_modules/@cahyo-dimas/freeday/CHANGELOG.md` explicitly, plus a short *reach for this instead
65
+ of hand-rolling that* table covering the last nine releases — the additions most likely to replace
66
+ something an app already worked around.
67
+ - **Three classes existed only in shorthand** in `COMPONENTS.md` — `.fdy-label--required`,
68
+ `.fdy-cal__month` and `.fdy-cal__grid--months` were written as `(+--required)` and `__month`, so
69
+ the full name appeared **zero** times and a grep for the class found nothing. The drift guard
70
+ cannot see this either: it verifies fully-written names and skips shorthand by design, so a class
71
+ introduced only in shorthand is invisible to both the reader and the test.
72
+
6
73
  ## [1.32.0] — 2026-08-18
7
74
  Improvement note 004, both halves: a calendar you could not steer, and a check mark that talked.
8
75
  ### Added
package/COMPONENTS.md CHANGED
@@ -259,6 +259,11 @@ a compact root with one region opted back out (shared chrome that must match a s
259
259
  </button>
260
260
  ```
261
261
 
262
+ **Quiet destructive:** combine `--danger` with `--ghost` or `--text`. The ground stays quiet and only
263
+ the ink turns red — `.fdy-menu__item--danger` has always worked this way. Use it whenever Delete
264
+ shares a screen with the one primary action; a solid Delete beside Save is a second primary in all
265
+ but name.
266
+
262
267
  ## FAB — `.fdy-fab`
263
268
  Floating circular action. Modifiers: `--sm` `--accent` `--danger` `--extended` (pill with a label).
264
269
  Always `aria-label` unless `--extended` carries visible text.
@@ -307,7 +312,7 @@ native control, otherwise a `<div>` + explicitly associated label.
307
312
  </fieldset>
308
313
  ```
309
314
 
310
- **`--required` marks the label, not the accessibility tree.** The control already carries
315
+ **`.fdy-label--required` marks the label, not the accessibility tree.** The control already carries
311
316
  `required`; the asterisk is painted through `::after` with CSS alt text, so a screen reader never
312
317
  reads "star" after the label — something a `<span>` you have to remember to mark `aria-hidden`
313
318
  cannot guarantee.
@@ -517,7 +522,8 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
517
522
  get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
518
523
  **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) — use it in those stacks.
519
524
  **Getting to a distant month.** The title is a **button**: press it and the day grid becomes a
520
- 12-cell month grid for the current year, where the arrows step **years** instead of months. Pick a
525
+ 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) for the current year, where the
526
+ arrows step **years** instead of months. Pick a
521
527
  month and you are back on days — choosing a month is navigation, so nothing is committed until a day
522
528
  is selected. From August 2026 to March 2022 that is 7 clicks; walking month by month is 53.
523
529
 
@@ -716,6 +722,18 @@ accessible name (`Filter <column>`) — a dialog named after its trigger is the
716
722
  Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
717
723
  reach for `getByRole('button', { name: 'Filter Name' })` instead.
718
724
 
725
+ **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
726
+ than one page. Two ways to take it over:
727
+
728
+ - **client mode** — pass `pageIndex` (with `pageSize`, without `page`) and drive it yourself;
729
+ - **either mode** — pass `pager={false}` (Vue `:pager="false"`, Blazor `Pager="false"`) and render
730
+ your own control. In server mode this is the only way: the app already owns the page there, and
731
+ was still being handed a second control. The typical shape is a responsive list — a table at `lg`,
732
+ `.fdy-list` below it — where one pager has to serve both views so they cannot disagree.
733
+
734
+ Do not hide the footer with CSS. `display:none` works, but it reaches into a component's internals
735
+ and breaks the moment the class changes.
736
+
719
737
  ## Pagination — `.fdy-pagination`
720
738
  The block class on the `<nav>` is a **structural hook only** — it carries no rule of its own; the
721
739
  `__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.32.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.32.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.33.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.33.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.32.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.32.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.33.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.33.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit — one source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -36,6 +36,11 @@ public partial class FdyTable<TRow>
36
36
  /// the <c>md</c> breakpoint and renders a card list from <see cref="Process"/> can render one pager
37
37
  /// for both breakpoints and bind it here. Leave null for the internal index (unchanged default).
38
38
  /// </summary>
39
+ /// <summary>Withhold the table's own footer (pager + range) so the screen can render one.
40
+ /// Server mode had no way to do this: the app owns the page there anyway, and was still handed a
41
+ /// second control. Client mode's counterpart is <see cref="PageIndex"/>. Default true.</summary>
42
+ [Parameter] public bool Pager { get; set; } = true;
43
+
39
44
  [Parameter] public int? PageIndex { get; set; }
40
45
 
41
46
  /// <summary>Raised in client mode with <see cref="PageIndex"/> set: the table asks for a new
@@ -97,7 +102,7 @@ public partial class FdyTable<TRow>
97
102
  private int CurrentPage1 => (ServerPaged ? Page!.Index : ClientPageIndex) + 1;
98
103
  private int TotalCount => _totalCount;
99
104
  private int TotalPages => PageSizeEff > 0 ? Math.Max(1, (int)Math.Ceiling((double)TotalCount / PageSizeEff)) : 1;
100
- private bool HasPager => PageSizeEff > 0 && TotalPages > 1;
105
+ private bool HasPager => Pager && PageSizeEff > 0 && TotalPages > 1;
101
106
  private List<int?> Pages => TableModel.PageWindow(CurrentPage1, TotalPages);
102
107
  private int RangeFrom => TotalCount == 0 ? 0 : (CurrentPage1 - 1) * PageSizeEff + 1;
103
108
  private int RangeTo => TotalCount == 0 ? 0 : RangeFrom - 1 + _displayRows.Count;
@@ -51,6 +51,11 @@ export interface FdyTableProps<Row extends object> {
51
51
  * card list from `onProcess` can render one pager for both breakpoints and point it here.
52
52
  * Omit for the internal index (unchanged default).
53
53
  */
54
+ /** Withhold the table's own footer (pager + range) so the screen can render one. Server mode had
55
+ * no way to do this: the app owns the page there ANYWAY, and was still handed a second control —
56
+ * a responsive list that shows a table at one breakpoint and cards at another ended up with the
57
+ * kit's pager stacked under its own. Client mode's counterpart is `pageIndex`. Default true. */
58
+ pager?: boolean;
54
59
  pageIndex?: number;
55
60
  /** Client mode with `pageIndex` provided: the table asks for a new 0-based index (pager click, a
56
61
  * reset to 0 after sort/filter, or a clamp when filtering shrank the set). */
@@ -127,7 +132,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
127
132
  const pageSizeEff: number = serverPaged ? (props.page as FdyPageState).size : (props.pageSize ?? 0);
128
133
  const currentPage1: number = (serverPaged ? (props.page as FdyPageState).index : clientPageIndex) + 1;
129
134
  const totalPages: number = pageSizeEff > 0 ? Math.max(1, Math.ceil(totalCount / pageSizeEff)) : 1;
130
- const hasPager: boolean = pageSizeEff > 0 && totalPages > 1;
135
+ const hasPager: boolean = props.pager !== false && pageSizeEff > 0 && totalPages > 1;
131
136
  const pages: Array<number | 'ellipsis'> = pageWindow(currentPage1, totalPages);
132
137
  const rangeFrom: number = totalCount === 0 ? 0 : (currentPage1 - 1) * pageSizeEff + 1;
133
138
  const rangeTo: number = totalCount === 0 ? 0 : rangeFrom - 1 + displayRows.length;
@@ -29,7 +29,10 @@ import FdyTableFilter from './FdyTableFilter.vue';
29
29
  // (`update:sort` / `update:filters` / `update:page`) for the caller to feed back into its query.
30
30
  // Column filters (text/enum/number/date) apply live; in server mode, debounce the emit if needed.
31
31
 
32
- const props = defineProps<{
32
+ /* `pager` MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop `false`,
33
+ not `undefined`, so a plain `props.pager !== false` would withhold the footer from every table that
34
+ never mentions it. Same trap as FdyModal's `dismissible`. */
35
+ const props = withDefaults(defineProps<{
33
36
  columns: ReadonlyArray<FdyTableColumn<Row>>;
34
37
  rows: ReadonlyArray<Row>;
35
38
  rowKey: (row: Row) => string | number;
@@ -48,6 +51,11 @@ const props = defineProps<{
48
51
  * card list from the `process` event can render one pager for both breakpoints and point it here.
49
52
  * Omit for the internal index (unchanged default).
50
53
  */
54
+ /** Withhold the table's own footer (pager + range) so the screen can render one. Server mode had
55
+ * no way to do this: the app owns the page there ANYWAY, and was still handed a second control —
56
+ * a responsive list that shows a table at one breakpoint and cards at another ended up with the
57
+ * kit's pager stacked under its own. Client mode's counterpart is `pageIndex`. Default true. */
58
+ pager?: boolean;
51
59
  pageIndex?: number;
52
60
  loading?: boolean;
53
61
  emptyText?: string;
@@ -58,7 +66,7 @@ const props = defineProps<{
58
66
  rowClass?: (row: Row) => string | undefined;
59
67
  /** Controlled: row keys whose `row-detail` slot is shown as a full-width row beneath them. */
60
68
  expandedKeys?: ReadonlyArray<string | number>;
61
- }>();
69
+ }>(), { pager: true });
62
70
 
63
71
  const emit = defineEmits<{
64
72
  'update:sort': [sort: FdySortState | null];
@@ -137,7 +145,7 @@ const currentPage1: ComputedRef<number> = computed((): number =>
137
145
  const totalPages: ComputedRef<number> = computed((): number =>
138
146
  pageSizeEff.value > 0 ? Math.max(1, Math.ceil(totalCount.value / pageSizeEff.value)) : 1,
139
147
  );
140
- const hasPager: ComputedRef<boolean> = computed((): boolean => pageSizeEff.value > 0 && totalPages.value > 1);
148
+ const hasPager: ComputedRef<boolean> = computed((): boolean => props.pager !== false && pageSizeEff.value > 0 && totalPages.value > 1);
141
149
  const pages: ComputedRef<Array<number | 'ellipsis'>> = computed((): Array<number | 'ellipsis'> =>
142
150
  pageWindow(currentPage1.value, totalPages.value),
143
151
  );
@@ -12,6 +12,19 @@ export interface UseFreedayReturn {
12
12
  */
13
13
  export declare function useFreeday(rootRef?: Ref<HTMLElement | null>): UseFreedayReturn;
14
14
 
15
+ /**
16
+ * Anchor a dropdown panel to its trigger in the top layer, so it escapes any ancestor overflow clip
17
+ * — `.fdy-card` is `overflow:hidden`, so a panel positioned inside one is otherwise cut at the card's
18
+ * edge. The panel element must carry `popover="manual"`. This is the plumbing every kit dropdown
19
+ * uses; it is exported so a control the kit does not ship can behave like one that it does, instead
20
+ * of each app re-implementing it from the description.
21
+ */
22
+ export declare function usePopover(
23
+ panelRef: Ref<HTMLElement | null>,
24
+ triggerRef: Ref<HTMLElement | null>,
25
+ open: Ref<boolean>,
26
+ ): void;
27
+
15
28
  export { default as FdyCombo } from './components/FdyCombo.vue';
16
29
  export { default as FdyDatepicker } from './components/FdyDatepicker.vue';
17
30
  export { default as FdyDateRange } from './components/FdyDateRange.vue';
@@ -1,4 +1,5 @@
1
1
  export { useFreeday } from './useFreeday.js';
2
+ export { usePopover } from './usePopover.ts';
2
3
  export { default as FdyCombo } from './components/FdyCombo.vue';
3
4
  export { default as FdyDatepicker } from './components/FdyDatepicker.vue';
4
5
  export { default as FdyDateRange } from './components/FdyDateRange.vue';
@@ -510,7 +510,11 @@ a { color: var(--color-primary); }
510
510
  /* Collapsible section: <details class="fdy-nav__group"><summary class="fdy-nav__grouplabel"> */
511
511
  .fdy-nav__group{margin:0;}
512
512
  .fdy-nav__group + .fdy-nav__group,.fdy-nav__item + .fdy-nav__group{border-top:var(--bw) solid var(--color-border-muted);margin-top:var(--space-1);padding-top:var(--space-1);}
513
- .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-subtle);}
513
+ /* Prose, and a control name: this is the <summary> of a nav group, at --text-xs uppercase. It is
514
+ the second class found spending the decorative ink on type (after .fdy-eyebrow) — and it was
515
+ already brightening to --color-text-muted on hover, which is the rule admitting its resting
516
+ state was too faint to read. */
517
+ .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
514
518
  .fdy-nav__grouplabel::-webkit-details-marker{display:none;}
515
519
  .fdy-nav__grouplabel:hover{color:var(--color-text-muted);}
516
520
  .fdy-nav__grouplabel:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -702,9 +706,20 @@ a { color: var(--color-primary); }
702
706
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
703
707
  .fdy-btn--ghost:active{transform:translateY(1px);}
704
708
 
709
+ /* --danger used to be the SOLID treatment and nothing else, so `fdy-btn--ghost fdy-btn--danger` —
710
+ which every reader parses as "quiet destructive" — rendered a second SOLID button beside Save.
711
+ That is a hierarchy defect, not a cosmetic one: the base class is already the one primary per
712
+ screen. `.fdy-menu__item--danger` has always modelled the quiet form (red text, not a red block);
713
+ the pairing rules below bring it to the button. They are more specific than this one and come
714
+ after it, so this rule needs no :not() guard of its own — one mechanism, not two. */
705
715
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
706
716
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
707
717
  .fdy-btn--danger:active{transform:translateY(1px);}
718
+ /* Quiet destructive: the variant keeps its own ground and only the ink turns. */
719
+ .fdy-btn--ghost.fdy-btn--danger{background:transparent;color:var(--color-danger-strong);border-color:color-mix(in srgb,var(--color-danger) 40%,transparent);box-shadow:none;}
720
+ .fdy-btn--ghost.fdy-btn--danger:hover{background:var(--color-danger-soft);border-color:var(--color-danger);color:var(--color-danger-strong);}
721
+ .fdy-btn--text.fdy-btn--danger{background:none;color:var(--color-danger-strong);border-color:transparent;box-shadow:none;}
722
+ .fdy-btn--text.fdy-btn--danger:hover{background:var(--color-danger-soft);color:var(--color-danger-strong);}
708
723
  .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
709
724
 
710
725
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
@@ -1032,7 +1047,11 @@ a { color: var(--color-primary); }
1032
1047
  /* ---- Type roles (three title levels + supporting text) -------------------------------------
1033
1048
  * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
1034
1049
  * page title (the mistake that collapses three levels into one grey mush). */
1035
- .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);}
1050
+ /* --color-text-muted, not -subtle: an eyebrow is a small label naming the thing below it, i.e.
1051
+ prose. -subtle is gated at 3.0 on purpose (placeholders, separators, decorative glyphs) and
1052
+ measures 4.41 on the page ground — axe reports it as a serious color-contrast failure. Same
1053
+ argument, same ink, same 4.41 as .fdy-stat__label in 1.30.0. */
1054
+ .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-muted);}
1036
1055
  .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;}
1037
1056
  .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;}
1038
1057
  .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;}
package/dist/freeday.css CHANGED
@@ -127,7 +127,11 @@ a { color: var(--color-primary); }
127
127
  /* Collapsible section: <details class="fdy-nav__group"><summary class="fdy-nav__grouplabel"> */
128
128
  .fdy-nav__group{margin:0;}
129
129
  .fdy-nav__group + .fdy-nav__group,.fdy-nav__item + .fdy-nav__group{border-top:var(--bw) solid var(--color-border-muted);margin-top:var(--space-1);padding-top:var(--space-1);}
130
- .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-subtle);}
130
+ /* Prose, and a control name: this is the <summary> of a nav group, at --text-xs uppercase. It is
131
+ the second class found spending the decorative ink on type (after .fdy-eyebrow) — and it was
132
+ already brightening to --color-text-muted on hover, which is the rule admitting its resting
133
+ state was too faint to read. */
134
+ .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
131
135
  .fdy-nav__grouplabel::-webkit-details-marker{display:none;}
132
136
  .fdy-nav__grouplabel:hover{color:var(--color-text-muted);}
133
137
  .fdy-nav__grouplabel:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -319,9 +323,20 @@ a { color: var(--color-primary); }
319
323
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
320
324
  .fdy-btn--ghost:active{transform:translateY(1px);}
321
325
 
326
+ /* --danger used to be the SOLID treatment and nothing else, so `fdy-btn--ghost fdy-btn--danger` —
327
+ which every reader parses as "quiet destructive" — rendered a second SOLID button beside Save.
328
+ That is a hierarchy defect, not a cosmetic one: the base class is already the one primary per
329
+ screen. `.fdy-menu__item--danger` has always modelled the quiet form (red text, not a red block);
330
+ the pairing rules below bring it to the button. They are more specific than this one and come
331
+ after it, so this rule needs no :not() guard of its own — one mechanism, not two. */
322
332
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
323
333
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
324
334
  .fdy-btn--danger:active{transform:translateY(1px);}
335
+ /* Quiet destructive: the variant keeps its own ground and only the ink turns. */
336
+ .fdy-btn--ghost.fdy-btn--danger{background:transparent;color:var(--color-danger-strong);border-color:color-mix(in srgb,var(--color-danger) 40%,transparent);box-shadow:none;}
337
+ .fdy-btn--ghost.fdy-btn--danger:hover{background:var(--color-danger-soft);border-color:var(--color-danger);color:var(--color-danger-strong);}
338
+ .fdy-btn--text.fdy-btn--danger{background:none;color:var(--color-danger-strong);border-color:transparent;box-shadow:none;}
339
+ .fdy-btn--text.fdy-btn--danger:hover{background:var(--color-danger-soft);color:var(--color-danger-strong);}
325
340
  .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
326
341
 
327
342
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
@@ -649,7 +664,11 @@ a { color: var(--color-primary); }
649
664
  /* ---- Type roles (three title levels + supporting text) -------------------------------------
650
665
  * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
651
666
  * page title (the mistake that collapses three levels into one grey mush). */
652
- .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);}
667
+ /* --color-text-muted, not -subtle: an eyebrow is a small label naming the thing below it, i.e.
668
+ prose. -subtle is gated at 3.0 on purpose (placeholders, separators, decorative glyphs) and
669
+ measures 4.41 on the page ground — axe reports it as a serious color-contrast failure. Same
670
+ argument, same ink, same 4.41 as .fdy-stat__label in 1.30.0. */
671
+ .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-muted);}
653
672
  .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;}
654
673
  .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;}
655
674
  .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;}
@@ -91,6 +91,7 @@ Blazor). Then verify the agent can actually read those files — an agent that c
91
91
  | `docs/integrations.md` | How to bridge third-party libraries (validation, charts, dates, i18n…). |
92
92
  | `docs/reference-screen.html` | A full screen assembled from the shell down. Open it in a browser. |
93
93
  | `docs/agent-onboarding.md` | This file. |
94
+ | `CHANGELOG.md` | **What changed between the version this project had and the one it has now** — read it after every upgrade; each entry says what broke, what is new, and why. |
94
95
  | `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. |
95
96
  | `src/components/*.css` | The authoritative source for every class, when a doc is ambiguous. |
96
97
  | `tokens/tokens.json` | Every token in W3C DTCG format — machine-readable. |
@@ -101,6 +102,28 @@ The live docs (with an interactive playground) are at
101
102
  apps under `examples/` (Vue, React, Blazor) that are **not** in the npm tarball — is at
102
103
  <https://github.com/cahyo-dimas/freeday-ui-kit>.
103
104
 
105
+ ### After an upgrade, read the changelog first
106
+
107
+ `npm i @cahyo-dimas/freeday@latest` does not tell you what you gained. Read
108
+ `node_modules/@cahyo-dimas/freeday/CHANGELOG.md` down to the version this project was on before —
109
+ it is written for exactly this moment, and it is the difference between adopting a new affordance
110
+ and re-implementing it locally.
111
+
112
+ Recent additions most likely to replace something an app hand-rolled (all detailed in
113
+ `COMPONENTS.md`):
114
+
115
+ | Reach for | Instead of |
116
+ |---|---|
117
+ | `.fdy-btn--stretch` | a hand-rolled overlay to make a card or list row clickable |
118
+ | `[data-fdy-number]` | a bare `<input type="number">` with the browser's own spin buttons |
119
+ | `clearable` on `FdyCfl` / `FdyDatepicker` | a value that can be set but never unset |
120
+ | `row.waiting(label)` on an upload row | a second status line saying the server is still working |
121
+ | `.fdy-label--required` | a `<span>` asterisk you have to remember to `aria-hidden` |
122
+ | `.fdy-text-warning` / `-danger` / `-success` | a caption class on a line that should stand out |
123
+ | `.fdy-icon` | your own `width:1em;height:1em` rule, once per project |
124
+ | `data-density="comfortable"` | restating the default spacing tokens on a wrapper |
125
+ | the calendar's month grid (press the title) | clicking "previous month" thirty times |
126
+
104
127
  ## 3. Starting a new screen
105
128
 
106
129
  The order matters; skipping to components is what produces flat, identical-card screens.
@@ -184,7 +184,7 @@ live docs also have a copy button per component.
184
184
  ```bash
185
185
  npm i @cahyo-dimas/freeday
186
186
  ```
187
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.32.0"` (public npm package). `dist/` is
187
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.33.0"` (public npm package). `dist/` is
188
188
  committed and published → no build step; `npm ci` runs without auth.
189
189
 
190
190
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -319,3 +319,17 @@ web-based add-ons (Service Layer + .NET), Freeday + enhancers over JS interop wo
319
319
  *Library recommendations reflect what's common and stable as of 2026; choose based on your
320
320
  project's license and bundle-size budget. Freeday locks you into none of them — everything is
321
321
  optional and replaceable.*
322
+
323
+ ## Building a control the kit does not ship
324
+
325
+ Use the `app-` prefix for its classes, and reuse the kit's plumbing rather than re-deriving it:
326
+
327
+ ```ts
328
+ import { usePopover } from '@cahyo-dimas/freeday/vue'; // or '/react'
329
+ usePopover(panelRef, triggerRef, open); // panel needs popover="manual"
330
+ ```
331
+
332
+ This is what every kit dropdown uses to escape an ancestor's overflow clip — `.fdy-card` is
333
+ `overflow:hidden`, so a panel positioned inside one is otherwise cut at the card's edge. It is
334
+ exported so a control you build behaves like one the kit ships, and improves when the kit's
335
+ positioning does.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,11 @@
48
48
  /* Collapsible section: <details class="fdy-nav__group"><summary class="fdy-nav__grouplabel"> */
49
49
  .fdy-nav__group{margin:0;}
50
50
  .fdy-nav__group + .fdy-nav__group,.fdy-nav__item + .fdy-nav__group{border-top:var(--bw) solid var(--color-border-muted);margin-top:var(--space-1);padding-top:var(--space-1);}
51
- .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-subtle);}
51
+ /* Prose, and a control name: this is the <summary> of a nav group, at --text-xs uppercase. It is
52
+ the second class found spending the decorative ink on type (after .fdy-eyebrow) — and it was
53
+ already brightening to --color-text-muted on hover, which is the rule admitting its resting
54
+ state was too faint to read. */
55
+ .fdy-nav__grouplabel{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-3) var(--space-3) var(--space-1);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
52
56
  .fdy-nav__grouplabel::-webkit-details-marker{display:none;}
53
57
  .fdy-nav__grouplabel:hover{color:var(--color-text-muted);}
54
58
  .fdy-nav__grouplabel:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -23,9 +23,20 @@
23
23
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
24
24
  .fdy-btn--ghost:active{transform:translateY(1px);}
25
25
 
26
+ /* --danger used to be the SOLID treatment and nothing else, so `fdy-btn--ghost fdy-btn--danger` —
27
+ which every reader parses as "quiet destructive" — rendered a second SOLID button beside Save.
28
+ That is a hierarchy defect, not a cosmetic one: the base class is already the one primary per
29
+ screen. `.fdy-menu__item--danger` has always modelled the quiet form (red text, not a red block);
30
+ the pairing rules below bring it to the button. They are more specific than this one and come
31
+ after it, so this rule needs no :not() guard of its own — one mechanism, not two. */
26
32
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
27
33
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
28
34
  .fdy-btn--danger:active{transform:translateY(1px);}
35
+ /* Quiet destructive: the variant keeps its own ground and only the ink turns. */
36
+ .fdy-btn--ghost.fdy-btn--danger{background:transparent;color:var(--color-danger-strong);border-color:color-mix(in srgb,var(--color-danger) 40%,transparent);box-shadow:none;}
37
+ .fdy-btn--ghost.fdy-btn--danger:hover{background:var(--color-danger-soft);border-color:var(--color-danger);color:var(--color-danger-strong);}
38
+ .fdy-btn--text.fdy-btn--danger{background:none;color:var(--color-danger-strong);border-color:transparent;box-shadow:none;}
39
+ .fdy-btn--text.fdy-btn--danger:hover{background:var(--color-danger-soft);color:var(--color-danger-strong);}
29
40
  .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
30
41
 
31
42
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
@@ -9,7 +9,11 @@
9
9
  /* ---- Type roles (three title levels + supporting text) -------------------------------------
10
10
  * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
11
11
  * page title (the mistake that collapses three levels into one grey mush). */
12
- .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);}
12
+ /* --color-text-muted, not -subtle: an eyebrow is a small label naming the thing below it, i.e.
13
+ prose. -subtle is gated at 3.0 on purpose (placeholders, separators, decorative glyphs) and
14
+ measures 4.41 on the page ground — axe reports it as a serious color-contrast failure. Same
15
+ argument, same ink, same 4.41 as .fdy-stat__label in 1.30.0. */
16
+ .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-muted);}
13
17
  .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;}
14
18
  .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;}
15
19
  .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;}