@cahyo-dimas/freeday 1.8.0 → 1.9.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,28 @@
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.9.0] — 2026-07-28
7
+ Follow-ups to 1.8.0's `FdyTable` (improvement notes #25/#26). Additive — the #25 change **widens** a
8
+ generic bound, so every call site that compiled before still compiles.
9
+
10
+ ### Fixed
11
+ - **`FdyTable` now accepts `interface`-typed rows.** The row generic was constrained to
12
+ `Record<string, unknown>`, to which a TypeScript `interface` is never assignable (interfaces get no
13
+ implicit index signature) — so a normally-typed DTO array failed to compile and `FdyTable` was
14
+ effectively unusable in a strict-TS app. Widened the bound to `object` in both adapters; the row type
15
+ now **infers** correctly (`cell-*` slots / `renderCell` are properly typed) and no cast is pushed
16
+ onto consumers. The component never indexes rows directly — all access goes through the
17
+ already-unconstrained core, which is unchanged.
18
+
19
+ ### Added
20
+ - **`FdyTable` row activation (opt-in).** `rowActivatable` makes rows focusable and emits
21
+ `row-activate` (Vue) / calls `onRowActivate` (React) on click, or Enter/Space while the row itself is
22
+ focused — a control inside a cell keeps its own event (the `target !== currentTarget` guard). Adds a
23
+ `rowClass(row)` hook for per-row state (e.g. a selected row) and a `.fdy-table__row--activatable`
24
+ class (pointer + `:focus-visible` ring; hover tint is already inherited from the base row rule).
25
+ Replaces the hand-rolled `<tr tabindex="0" @click @keydown>` + duplicated row CSS consumers were
26
+ each re-writing.
27
+
6
28
  ## [1.8.0] — 2026-07-28
7
29
  Release **1.8 — framework-safe data table, modal/drawer wrappers, and a monospace utility**.
8
30
  Non-breaking, purely additive. No existing selector, markup, or enhancer changed.
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.8.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.8.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.9.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.9.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.8.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.8.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.9.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.9.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`.
@@ -29,7 +29,7 @@ import { FdyTableFilter } from './FdyTableFilter';
29
29
  // `onSortChange` / `onFiltersChange` / `onPageChange` for the caller to feed back into its query.
30
30
  // Column filters (text/enum/number/date) apply live; in server mode, debounce the callback if needed.
31
31
 
32
- export interface FdyTableProps<Row extends Record<string, unknown>> {
32
+ export interface FdyTableProps<Row extends object> {
33
33
  columns: ReadonlyArray<FdyTableColumn<Row>>;
34
34
  rows: ReadonlyArray<Row>;
35
35
  rowKey: (row: Row) => string | number;
@@ -51,9 +51,15 @@ export interface FdyTableProps<Row extends Record<string, unknown>> {
51
51
  renderCell?: (column: FdyTableColumn<Row>, row: Row, value: unknown) => ReactNode;
52
52
  toolbar?: ReactNode;
53
53
  empty?: ReactNode;
54
+ /** Opt in to row activation: rows become focusable and call `onRowActivate` on click/Enter/Space. */
55
+ rowActivatable?: boolean;
56
+ /** Per-row class hook, e.g. to mark a selected row. */
57
+ rowClass?: (row: Row) => string | undefined;
58
+ /** A row was activated (click, or Enter/Space while the row itself is focused). */
59
+ onRowActivate?: (row: Row) => void;
54
60
  }
55
61
 
56
- export function FdyTable<Row extends Record<string, unknown>>(props: FdyTableProps<Row>): JSX.Element {
62
+ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Element {
57
63
  const serverPaged: boolean = props.page != null;
58
64
  const sortControlled: boolean = serverPaged || props.sort !== undefined;
59
65
  const filtersControlled: boolean = serverPaged || props.filters !== undefined;
@@ -143,6 +149,21 @@ export function FdyTable<Row extends Record<string, unknown>>(props: FdyTablePro
143
149
  function alignStyle(col: FdyTableColumn<Row>): CSSProperties | undefined {
144
150
  return col.align !== undefined ? { textAlign: col.align } : undefined;
145
151
  }
152
+ function rowClassName(row: Row): string | undefined {
153
+ const cls: string = [props.rowClass?.(row), props.rowActivatable === true ? 'fdy-table__row--activatable' : undefined]
154
+ .filter(Boolean)
155
+ .join(' ');
156
+ return cls === '' ? undefined : cls;
157
+ }
158
+ // Enter/Space activate only when the row itself is focused — a control inside a cell keeps its own
159
+ // event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
160
+ // stopPropagation, matching the pattern consumers hand-roll today.
161
+ function onRowKeydown(e: React.KeyboardEvent<HTMLTableRowElement>, row: Row): void {
162
+ if (props.rowActivatable !== true || e.target !== e.currentTarget) return;
163
+ if (e.key !== 'Enter' && e.key !== ' ') return;
164
+ e.preventDefault();
165
+ props.onRowActivate?.(row);
166
+ }
146
167
  function renderCellContent(col: FdyTableColumn<Row>, row: Row): ReactNode {
147
168
  if (props.renderCell !== undefined) {
148
169
  const custom: ReactNode = props.renderCell(col, row, cellValue(row, col));
@@ -192,7 +213,15 @@ export function FdyTable<Row extends Record<string, unknown>>(props: FdyTablePro
192
213
  </tr>
193
214
  ) : (
194
215
  displayRows.map((row: Row): JSX.Element => (
195
- <tr key={props.rowKey(row)}>
216
+ <tr
217
+ key={props.rowKey(row)}
218
+ className={rowClassName(row)}
219
+ tabIndex={props.rowActivatable ? 0 : undefined}
220
+ onClick={(): void => {
221
+ if (props.rowActivatable === true) props.onRowActivate?.(row);
222
+ }}
223
+ onKeyDown={(e): void => onRowKeydown(e, row)}
224
+ >
196
225
  {props.columns.map((col: FdyTableColumn<Row>): JSX.Element => (
197
226
  <td key={col.key} className={cellClass(col)} style={alignStyle(col)}>{renderCellContent(col, row)}</td>
198
227
  ))}
@@ -1,4 +1,4 @@
1
- <script setup lang="ts" generic="Row extends Record<string, unknown>">
1
+ <script setup lang="ts" generic="Row extends object">
2
2
  import { computed, ref, watch, type ComputedRef, type Ref } from 'vue';
3
3
  import {
4
4
  cellValue,
@@ -44,12 +44,18 @@ const props = defineProps<{
44
44
  loading?: boolean;
45
45
  emptyText?: string;
46
46
  ariaLabel?: string;
47
+ /** Opt in to row activation: rows become focusable and emit `row-activate` on click/Enter/Space. */
48
+ rowActivatable?: boolean;
49
+ /** Per-row class hook, e.g. to mark a selected row. */
50
+ rowClass?: (row: Row) => string | undefined;
47
51
  }>();
48
52
 
49
53
  const emit = defineEmits<{
50
54
  'update:sort': [sort: FdySortState | null];
51
55
  'update:filters': [filters: FdyFilterMap];
52
56
  'update:page': [page: FdyPageState];
57
+ /** A row was activated (click, or Enter/Space while the row itself is focused). */
58
+ 'row-activate': [row: Row];
53
59
  }>();
54
60
 
55
61
  const internalSort: Ref<FdySortState | null> = ref(null);
@@ -162,6 +168,22 @@ function cellClass(col: FdyTableColumn<Row>): string | undefined {
162
168
  function alignStyle(col: FdyTableColumn<Row>): Record<string, string> | undefined {
163
169
  return col.align !== undefined ? { textAlign: col.align } : undefined;
164
170
  }
171
+
172
+ function rowClasses(row: Row): Array<string | undefined> {
173
+ return [props.rowClass?.(row), props.rowActivatable === true ? 'fdy-table__row--activatable' : undefined];
174
+ }
175
+ function onRowClick(row: Row): void {
176
+ if (props.rowActivatable === true) emit('row-activate', row);
177
+ }
178
+ // Enter/Space activate only when the row itself is focused — a control inside a cell keeps its own
179
+ // event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
180
+ // stopPropagation, matching the pattern consumers hand-roll today.
181
+ function onRowKeydown(e: KeyboardEvent, row: Row): void {
182
+ if (props.rowActivatable !== true || e.target !== e.currentTarget) return;
183
+ if (e.key !== 'Enter' && e.key !== ' ') return;
184
+ e.preventDefault();
185
+ emit('row-activate', row);
186
+ }
165
187
  </script>
166
188
 
167
189
  <template>
@@ -202,7 +224,15 @@ function alignStyle(col: FdyTableColumn<Row>): Record<string, string> | undefine
202
224
  <slot name="empty">{{ emptyText ?? 'No data' }}</slot>
203
225
  </td>
204
226
  </tr>
205
- <tr v-for="row in displayRows" v-else :key="rowKey(row)">
227
+ <tr
228
+ v-for="row in displayRows"
229
+ v-else
230
+ :key="rowKey(row)"
231
+ :class="rowClasses(row)"
232
+ :tabindex="rowActivatable ? 0 : undefined"
233
+ @click="onRowClick(row)"
234
+ @keydown="onRowKeydown($event, row)"
235
+ >
206
236
  <td v-for="col in columns" :key="col.key" :class="cellClass(col)" :style="alignStyle(col)">
207
237
  <slot :name="`cell-${col.key}`" :row="row" :value="cellValue(row, col)">{{ cellText(row, col) }}</slot>
208
238
  </td>
@@ -1219,6 +1219,10 @@ a { color: var(--color-primary); }
1219
1219
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
1220
1220
  /* Loading / empty state cell (FdyTable spans it across all columns). */
1221
1221
  .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
1222
+ /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
1223
+ comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
1224
+ .fdy-table__row--activatable{cursor:pointer;}
1225
+ .fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
1222
1226
  .fdy-table th[aria-sort]{cursor:pointer;}
1223
1227
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
1224
1228
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
package/dist/freeday.css CHANGED
@@ -874,6 +874,10 @@ a { color: var(--color-primary); }
874
874
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
875
875
  /* Loading / empty state cell (FdyTable spans it across all columns). */
876
876
  .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
877
+ /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
878
+ comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
879
+ .fdy-table__row--activatable{cursor:pointer;}
880
+ .fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
877
881
  .fdy-table th[aria-sort]{cursor:pointer;}
878
882
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
879
883
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,10 @@
15
15
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
16
16
  /* Loading / empty state cell (FdyTable spans it across all columns). */
17
17
  .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
18
+ /* Activatable row (FdyTable rowActivatable): pointer + keyboard focus ring. The hover tint already
19
+ comes from the base `.fdy-table tbody tr:hover` rule, so this only adds the affordance + focus. */
20
+ .fdy-table__row--activatable{cursor:pointer;}
21
+ .fdy-table__row--activatable:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;}
18
22
  .fdy-table th[aria-sort]{cursor:pointer;}
19
23
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
20
24
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}