@cahyo-dimas/freeday 1.7.0 → 1.8.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.
@@ -0,0 +1,189 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, ref, watch, type ComputedRef, type Ref } from 'vue';
3
+ import { usePopover } from '../usePopover';
4
+ import { isFilterActive } from '../../core/table-model.js';
5
+ import type { FdyColumnFilter, FdyColumnFilterType } from '../../core/table-model';
6
+
7
+ // Internal to FdyTable: one column's header funnel button + its type-aware filter popover
8
+ // (text / enum / number / date) over freeday's `.fdy-table__filterbtn` + `.fdy-filter*` classes.
9
+ // Reuses usePopover so the panel escapes the table's `overflow:hidden` via the top layer. Purely
10
+ // controlled — it renders the current `filter` and emits the next one (or null to clear); the
11
+ // parent owns where that goes (client state or an `update:filters` emit). Not exported.
12
+
13
+ const props = defineProps<{
14
+ label: string;
15
+ type: FdyColumnFilterType;
16
+ filter: FdyColumnFilter | undefined;
17
+ /** Distinct values for an enum filter (computed by the parent over the full row set). */
18
+ options: ReadonlyArray<string>;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ /** The next filter for this column, or null to clear it. */
23
+ change: [filter: FdyColumnFilter | null];
24
+ }>();
25
+
26
+ const rootEl: Ref<HTMLSpanElement | null> = ref(null);
27
+ const triggerEl: Ref<HTMLButtonElement | null> = ref(null);
28
+ const panelEl: Ref<HTMLDivElement | null> = ref(null);
29
+ const open: Ref<boolean> = ref(false);
30
+
31
+ usePopover(panelEl, triggerEl, open);
32
+
33
+ const active: ComputedRef<boolean> = computed((): boolean => isFilterActive(props.filter));
34
+
35
+ // Typed reads of the current filter for the inputs (fall back to the empty shape per type).
36
+ const textValue: ComputedRef<string> = computed((): string =>
37
+ props.filter?.type === 'text' ? props.filter.text : '',
38
+ );
39
+ const enumValues: ComputedRef<ReadonlyArray<string>> = computed((): ReadonlyArray<string> =>
40
+ props.filter?.type === 'enum' ? props.filter.values : [],
41
+ );
42
+ const numMin: ComputedRef<string> = computed((): string =>
43
+ props.filter?.type === 'number' && props.filter.min !== null ? String(props.filter.min) : '',
44
+ );
45
+ const numMax: ComputedRef<string> = computed((): string =>
46
+ props.filter?.type === 'number' && props.filter.max !== null ? String(props.filter.max) : '',
47
+ );
48
+ const dateFrom: ComputedRef<string> = computed((): string =>
49
+ props.filter?.type === 'date' && props.filter.from !== null ? props.filter.from : '',
50
+ );
51
+ const dateTo: ComputedRef<string> = computed((): string =>
52
+ props.filter?.type === 'date' && props.filter.to !== null ? props.filter.to : '',
53
+ );
54
+
55
+ function apply(next: FdyColumnFilter): void {
56
+ emit('change', isFilterActive(next) ? next : null);
57
+ }
58
+ function parseNum(v: string): number | null {
59
+ const t: string = v.trim();
60
+ if (t === '') return null;
61
+ const n: number = Number(t);
62
+ return Number.isNaN(n) ? null : n;
63
+ }
64
+ function onText(e: Event): void {
65
+ apply({ type: 'text', text: (e.target as HTMLInputElement).value });
66
+ }
67
+ function onEnumToggle(value: string, checked: boolean): void {
68
+ const set: string[] = enumValues.value.filter((v: string): boolean => v !== value);
69
+ if (checked) set.push(value);
70
+ apply({ type: 'enum', values: set });
71
+ }
72
+ function onNumber(which: 'min' | 'max', e: Event): void {
73
+ const v: number | null = parseNum((e.target as HTMLInputElement).value);
74
+ apply({
75
+ type: 'number',
76
+ min: which === 'min' ? v : parseNum(numMin.value),
77
+ max: which === 'max' ? v : parseNum(numMax.value),
78
+ });
79
+ }
80
+ function onDate(which: 'from' | 'to', e: Event): void {
81
+ const v: string = (e.target as HTMLInputElement).value;
82
+ const val: string | null = v === '' ? null : v;
83
+ apply({
84
+ type: 'date',
85
+ from: which === 'from' ? val : (dateFrom.value || null),
86
+ to: which === 'to' ? val : (dateTo.value || null),
87
+ });
88
+ }
89
+
90
+ function toggle(): void {
91
+ open.value = !open.value;
92
+ }
93
+ function close(returnFocus: boolean): void {
94
+ open.value = false;
95
+ if (returnFocus) triggerEl.value?.focus();
96
+ }
97
+ function reset(): void {
98
+ emit('change', null);
99
+ close(true);
100
+ }
101
+
102
+ // Dismiss on outside pointer or Escape while open.
103
+ function onDocPointerDown(e: MouseEvent): void {
104
+ const t: EventTarget | null = e.target;
105
+ if (rootEl.value !== null && t instanceof Node && !rootEl.value.contains(t) && !panelEl.value?.contains(t)) {
106
+ close(false);
107
+ }
108
+ }
109
+ function onDocKeydown(e: KeyboardEvent): void {
110
+ if (e.key === 'Escape' && open.value) close(true);
111
+ }
112
+ watch(open, (isOpen: boolean): void => {
113
+ if (isOpen) {
114
+ document.addEventListener('mousedown', onDocPointerDown);
115
+ document.addEventListener('keydown', onDocKeydown);
116
+ } else {
117
+ document.removeEventListener('mousedown', onDocPointerDown);
118
+ document.removeEventListener('keydown', onDocKeydown);
119
+ }
120
+ });
121
+ onBeforeUnmount((): void => {
122
+ document.removeEventListener('mousedown', onDocPointerDown);
123
+ document.removeEventListener('keydown', onDocKeydown);
124
+ });
125
+ </script>
126
+
127
+ <template>
128
+ <span ref="rootEl" class="fdy-table__filterwrap">
129
+ <button
130
+ ref="triggerEl"
131
+ type="button"
132
+ :class="active ? 'fdy-table__filterbtn is-active' : 'fdy-table__filterbtn'"
133
+ aria-haspopup="dialog"
134
+ :aria-pressed="active"
135
+ :aria-expanded="open"
136
+ :aria-label="`Filter ${label}`"
137
+ @click.stop="toggle"
138
+ >
139
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
140
+ <path d="M3 5h18l-7 8v5l-4 2v-7z" />
141
+ </svg>
142
+ </button>
143
+
144
+ <div ref="panelEl" class="fdy-filter" popover="manual" :hidden="!open" role="dialog" :aria-label="`Filter ${label}`">
145
+ <template v-if="type === 'text'">
146
+ <div class="fdy-filter__title">Contains text</div>
147
+ <input class="fdy-input" type="search" placeholder="Contains…" :value="textValue" @input="onText" />
148
+ </template>
149
+
150
+ <template v-else-if="type === 'enum'">
151
+ <div class="fdy-filter__title">Show values</div>
152
+ <div class="fdy-filter__list">
153
+ <label v-for="val in options" :key="val" class="fdy-filter__check">
154
+ <input
155
+ type="checkbox"
156
+ class="fdy-checkbox"
157
+ :checked="enumValues.includes(val)"
158
+ @change="onEnumToggle(val, ($event.target as HTMLInputElement).checked)"
159
+ />
160
+ {{ val }}
161
+ </label>
162
+ </div>
163
+ </template>
164
+
165
+ <template v-else-if="type === 'number'">
166
+ <div class="fdy-filter__title">Value range</div>
167
+ <div class="fdy-filter__range">
168
+ <input class="fdy-input" type="text" inputmode="numeric" placeholder="Min" :value="numMin" @input="onNumber('min', $event)" />
169
+ <span aria-hidden="true">–</span>
170
+ <input class="fdy-input" type="text" inputmode="numeric" placeholder="Max" :value="numMax" @input="onNumber('max', $event)" />
171
+ </div>
172
+ </template>
173
+
174
+ <template v-else-if="type === 'date'">
175
+ <div class="fdy-filter__title">Date range</div>
176
+ <div class="fdy-filter__range">
177
+ <input class="fdy-input" type="date" aria-label="From" :value="dateFrom" @input="onDate('from', $event)" />
178
+ <span aria-hidden="true">–</span>
179
+ <input class="fdy-input" type="date" aria-label="To" :value="dateTo" @input="onDate('to', $event)" />
180
+ </div>
181
+ </template>
182
+
183
+ <div class="fdy-filter__foot">
184
+ <button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--sm" @click="reset">Reset</button>
185
+ <button type="button" class="fdy-btn fdy-btn--sm" @click="close(true)">Close</button>
186
+ </div>
187
+ </div>
188
+ </span>
189
+ </template>
@@ -21,6 +21,22 @@ export { default as FdyCascade } from './components/FdyCascade.vue';
21
21
  export type { CascadeNode } from './components/FdyCascade.vue';
22
22
  export { default as FdyCfl } from './components/FdyCfl.vue';
23
23
  export { default as FdyChart } from './components/FdyChart.vue';
24
+ export { default as FdyTable } from './components/FdyTable.vue';
25
+ export { default as FdyModal } from './components/FdyModal.vue';
26
+ export { default as FdyDrawer } from './components/FdyDrawer.vue';
27
+
28
+ /** Controlled data-table types (shared, framework-agnostic core). */
29
+ export type {
30
+ FdyTableColumn,
31
+ FdySortState,
32
+ FdySortDir,
33
+ FdyColumnType,
34
+ FdyColumnAlign,
35
+ FdyColumnFilterType,
36
+ FdyColumnFilter,
37
+ FdyFilterMap,
38
+ FdyPageState,
39
+ } from '../core/table-model';
24
40
 
25
41
  /** One data series for the cartesian chart types (line / area / multi-series & stacked bar). */
26
42
  export interface FdyChartSeries {
@@ -6,3 +6,6 @@ export { default as FdyAutocomplete } from './components/FdyAutocomplete.vue';
6
6
  export { default as FdyCascade } from './components/FdyCascade.vue';
7
7
  export { default as FdyCfl } from './components/FdyCfl.vue';
8
8
  export { default as FdyChart } from './components/FdyChart.vue';
9
+ export { default as FdyTable } from './components/FdyTable.vue';
10
+ export { default as FdyModal } from './components/FdyModal.vue';
11
+ export { default as FdyDrawer } from './components/FdyDrawer.vue';
@@ -915,6 +915,7 @@ a { color: var(--color-primary); }
915
915
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
916
916
  .fdy-drawer__close:hover{background:var(--color-surface-2);color:var(--color-text);}
917
917
  .fdy-drawer__body{flex:1;min-height:0;overflow:auto;padding:var(--space-4);}
918
+ .fdy-drawer__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
918
919
  /* Symmetric enter AND exit on the native <dialog>: @starting-style animates the entry;
919
920
  * the overlay/display allow-discrete transitions keep the panel painted through the exit so
920
921
  * it slides back out on close (not just in). prefers-reduced-motion is honored by the global
@@ -1210,7 +1211,14 @@ a { color: var(--color-primary); }
1210
1211
  .fdy-table tbody tr:last-child td{border-bottom:0;}
1211
1212
  .fdy-table tbody tr{transition:background-color var(--dur-fast) var(--ease-standard);}
1212
1213
  .fdy-table tbody tr:hover{background:var(--color-surface-2);}
1214
+ /* Monospace data — ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
1215
+ or inline (`<span class="fdy-mono">`), not just numeric columns. */
1216
+ .fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
1217
+ /* Numeric cells are mono + right-aligned. Kept self-contained (not composed from .fdy-mono)
1218
+ so existing single-class markup keeps working. */
1213
1219
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
1220
+ /* Loading / empty state cell (FdyTable spans it across all columns). */
1221
+ .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
1214
1222
  .fdy-table th[aria-sort]{cursor:pointer;}
1215
1223
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
1216
1224
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
@@ -1234,7 +1242,8 @@ a { color: var(--color-primary); }
1234
1242
  .fdy-table-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-top:var(--bw) solid var(--color-border);}
1235
1243
  .fdy-table-footer__info{font-size:var(--text-sm);color:var(--color-text-muted);}
1236
1244
 
1237
- /* Column filter — funnel button injected into a filterable <th> */
1245
+ /* Column filter — funnel button injected (enhancer) or rendered (FdyTable) into a filterable <th> */
1246
+ .fdy-table__filterwrap{display:inline-flex;align-items:center;vertical-align:middle;}
1238
1247
  .fdy-table__filterbtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin-left:var(--space-1);padding:0;vertical-align:middle;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-subtle);cursor:pointer;}
1239
1248
  .fdy-table__filterbtn:hover{background:var(--color-surface-3);color:var(--color-text);}
1240
1249
  .fdy-table__filterbtn:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
package/dist/freeday.css CHANGED
@@ -570,6 +570,7 @@ a { color: var(--color-primary); }
570
570
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
571
571
  .fdy-drawer__close:hover{background:var(--color-surface-2);color:var(--color-text);}
572
572
  .fdy-drawer__body{flex:1;min-height:0;overflow:auto;padding:var(--space-4);}
573
+ .fdy-drawer__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
573
574
  /* Symmetric enter AND exit on the native <dialog>: @starting-style animates the entry;
574
575
  * the overlay/display allow-discrete transitions keep the panel painted through the exit so
575
576
  * it slides back out on close (not just in). prefers-reduced-motion is honored by the global
@@ -865,7 +866,14 @@ a { color: var(--color-primary); }
865
866
  .fdy-table tbody tr:last-child td{border-bottom:0;}
866
867
  .fdy-table tbody tr{transition:background-color var(--dur-fast) var(--ease-standard);}
867
868
  .fdy-table tbody tr:hover{background:var(--color-surface-2);}
869
+ /* Monospace data — ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
870
+ or inline (`<span class="fdy-mono">`), not just numeric columns. */
871
+ .fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
872
+ /* Numeric cells are mono + right-aligned. Kept self-contained (not composed from .fdy-mono)
873
+ so existing single-class markup keeps working. */
868
874
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
875
+ /* Loading / empty state cell (FdyTable spans it across all columns). */
876
+ .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
869
877
  .fdy-table th[aria-sort]{cursor:pointer;}
870
878
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
871
879
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
@@ -889,7 +897,8 @@ a { color: var(--color-primary); }
889
897
  .fdy-table-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-top:var(--bw) solid var(--color-border);}
890
898
  .fdy-table-footer__info{font-size:var(--text-sm);color:var(--color-text-muted);}
891
899
 
892
- /* Column filter — funnel button injected into a filterable <th> */
900
+ /* Column filter — funnel button injected (enhancer) or rendered (FdyTable) into a filterable <th> */
901
+ .fdy-table__filterwrap{display:inline-flex;align-items:center;vertical-align:middle;}
893
902
  .fdy-table__filterbtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin-left:var(--space-1);padding:0;vertical-align:middle;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-subtle);cursor:pointer;}
894
903
  .fdy-table__filterbtn:hover{background:var(--color-surface-3);color:var(--color-text);}
895
904
  .fdy-table__filterbtn:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,6 +26,7 @@
26
26
  "tokens/tokens.json",
27
27
  "tokens/breakpoints.mjs",
28
28
  "README.md",
29
+ "README.id.md",
29
30
  "CHANGELOG.md"
30
31
  ],
31
32
  "exports": {
@@ -13,6 +13,7 @@
13
13
  .fdy-drawer__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
14
14
  .fdy-drawer__close:hover{background:var(--color-surface-2);color:var(--color-text);}
15
15
  .fdy-drawer__body{flex:1;min-height:0;overflow:auto;padding:var(--space-4);}
16
+ .fdy-drawer__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
16
17
  /* Symmetric enter AND exit on the native <dialog>: @starting-style animates the entry;
17
18
  * the overlay/display allow-discrete transitions keep the panel painted through the exit so
18
19
  * it slides back out on close (not just in). prefers-reduced-motion is honored by the global
@@ -7,7 +7,14 @@
7
7
  .fdy-table tbody tr:last-child td{border-bottom:0;}
8
8
  .fdy-table tbody tr{transition:background-color var(--dur-fast) var(--ease-standard);}
9
9
  .fdy-table tbody tr:hover{background:var(--color-surface-2);}
10
+ /* Monospace data — ids, codes, IPs, timestamps. Alignment-neutral so it works in any cell
11
+ or inline (`<span class="fdy-mono">`), not just numeric columns. */
12
+ .fdy-mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
13
+ /* Numeric cells are mono + right-aligned. Kept self-contained (not composed from .fdy-mono)
14
+ so existing single-class markup keeps working. */
10
15
  .fdy-table__num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-mono);}
16
+ /* Loading / empty state cell (FdyTable spans it across all columns). */
17
+ .fdy-table__state{text-align:center;color:var(--color-text-muted);padding:var(--space-6) var(--space-4);}
11
18
  .fdy-table th[aria-sort]{cursor:pointer;}
12
19
  .fdy-table th[aria-sort="ascending"]::after{content:" ↑";color:var(--color-primary);}
13
20
  .fdy-table th[aria-sort="descending"]::after{content:" ↓";color:var(--color-primary);}
@@ -31,7 +38,8 @@
31
38
  .fdy-table-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-top:var(--bw) solid var(--color-border);}
32
39
  .fdy-table-footer__info{font-size:var(--text-sm);color:var(--color-text-muted);}
33
40
 
34
- /* Column filter — funnel button injected into a filterable <th> */
41
+ /* Column filter — funnel button injected (enhancer) or rendered (FdyTable) into a filterable <th> */
42
+ .fdy-table__filterwrap{display:inline-flex;align-items:center;vertical-align:middle;}
35
43
  .fdy-table__filterbtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin-left:var(--space-1);padding:0;vertical-align:middle;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-subtle);cursor:pointer;}
36
44
  .fdy-table__filterbtn:hover{background:var(--color-surface-3);color:var(--color-text);}
37
45
  .fdy-table__filterbtn:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}