@cahyo-dimas/freeday 1.7.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 (130) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/adapters/blazor/freeday-blazor.js +79 -0
  5. package/adapters/react/components/FdyAutocomplete.tsx +159 -0
  6. package/adapters/react/components/FdyCascade.tsx +230 -0
  7. package/adapters/react/components/FdyCfl.tsx +362 -0
  8. package/adapters/react/components/FdyChart.tsx +69 -0
  9. package/adapters/react/components/FdyCombo.tsx +166 -0
  10. package/adapters/react/components/FdyDateRange.tsx +66 -0
  11. package/adapters/react/components/FdyDatepicker.tsx +354 -0
  12. package/adapters/react/index.d.ts +37 -0
  13. package/adapters/react/index.js +9 -0
  14. package/adapters/react/tsconfig.json +14 -0
  15. package/adapters/react/useFreeday.js +44 -0
  16. package/adapters/react/usePopover.ts +64 -0
  17. package/adapters/vue/components/FdyAutocomplete.vue +174 -0
  18. package/adapters/vue/components/FdyCascade.vue +241 -0
  19. package/adapters/vue/components/FdyCfl.vue +354 -0
  20. package/adapters/vue/components/FdyChart.vue +89 -0
  21. package/adapters/vue/components/FdyCombo.vue +232 -0
  22. package/adapters/vue/components/FdyDateRange.vue +78 -0
  23. package/adapters/vue/components/FdyDatepicker.vue +333 -0
  24. package/adapters/vue/index.d.ts +44 -0
  25. package/adapters/vue/index.js +8 -0
  26. package/adapters/vue/useFreeday.js +44 -0
  27. package/adapters/vue/usePopover.ts +78 -0
  28. package/dist/freeday-autocomplete.js +135 -0
  29. package/dist/freeday-breakpoint.js +51 -0
  30. package/dist/freeday-carousel.js +111 -0
  31. package/dist/freeday-cascade.js +256 -0
  32. package/dist/freeday-cfl.js +213 -0
  33. package/dist/freeday-chart.js +403 -0
  34. package/dist/freeday-chip.js +83 -0
  35. package/dist/freeday-datepicker.js +321 -0
  36. package/dist/freeday-datetime.js +59 -0
  37. package/dist/freeday-drawer.js +43 -0
  38. package/dist/freeday-form.js +181 -0
  39. package/dist/freeday-mask.js +114 -0
  40. package/dist/freeday-menu.js +93 -0
  41. package/dist/freeday-popover.js +69 -0
  42. package/dist/freeday-rating.js +50 -0
  43. package/dist/freeday-select.js +194 -0
  44. package/dist/freeday-slider.js +34 -0
  45. package/dist/freeday-stepper.js +90 -0
  46. package/dist/freeday-table.js +475 -0
  47. package/dist/freeday-tabs.js +68 -0
  48. package/dist/freeday-timepicker.js +180 -0
  49. package/dist/freeday-toast.js +84 -0
  50. package/dist/freeday-tree.js +94 -0
  51. package/dist/freeday-upload.js +206 -0
  52. package/dist/freeday.bundle.css +1352 -0
  53. package/dist/freeday.css +1007 -0
  54. package/dist/freeday.js +3632 -0
  55. package/dist/freeday.tokens.css +343 -0
  56. package/package.json +72 -0
  57. package/src/base.css +20 -0
  58. package/src/components/.gitkeep +0 -0
  59. package/src/components/accordion.css +13 -0
  60. package/src/components/alert.css +18 -0
  61. package/src/components/app-shell.css +88 -0
  62. package/src/components/appbar.css +15 -0
  63. package/src/components/autocomplete.css +12 -0
  64. package/src/components/avatar.css +8 -0
  65. package/src/components/badge.css +27 -0
  66. package/src/components/breadcrumb.css +9 -0
  67. package/src/components/breakpoints.css +15 -0
  68. package/src/components/button.css +58 -0
  69. package/src/components/card.css +9 -0
  70. package/src/components/carousel.css +17 -0
  71. package/src/components/cascade.css +36 -0
  72. package/src/components/cfl.css +28 -0
  73. package/src/components/chart.css +55 -0
  74. package/src/components/chip.css +18 -0
  75. package/src/components/combo.css +27 -0
  76. package/src/components/datepicker.css +44 -0
  77. package/src/components/datetimepicker.css +8 -0
  78. package/src/components/description-list.css +6 -0
  79. package/src/components/divider.css +3 -0
  80. package/src/components/drawer.css +25 -0
  81. package/src/components/file-upload.css +34 -0
  82. package/src/components/filterbar.css +25 -0
  83. package/src/components/form-grid.css +8 -0
  84. package/src/components/input-group.css +22 -0
  85. package/src/components/input.css +13 -0
  86. package/src/components/kbd.css +2 -0
  87. package/src/components/menu.css +28 -0
  88. package/src/components/modal.css +22 -0
  89. package/src/components/pagination.css +8 -0
  90. package/src/components/progress.css +7 -0
  91. package/src/components/rating.css +10 -0
  92. package/src/components/selection.css +25 -0
  93. package/src/components/skeleton.css +7 -0
  94. package/src/components/slider.css +12 -0
  95. package/src/components/spinner.css +6 -0
  96. package/src/components/states.css +8 -0
  97. package/src/components/stepper.css +21 -0
  98. package/src/components/table.css +55 -0
  99. package/src/components/tabs.css +10 -0
  100. package/src/components/timeline.css +12 -0
  101. package/src/components/timepicker.css +22 -0
  102. package/src/components/toast.css +17 -0
  103. package/src/components/tooltip.css +6 -0
  104. package/src/components/tree.css +21 -0
  105. package/src/freeday-autocomplete.js +135 -0
  106. package/src/freeday-breakpoint.js +51 -0
  107. package/src/freeday-carousel.js +111 -0
  108. package/src/freeday-cascade.js +256 -0
  109. package/src/freeday-cfl.js +213 -0
  110. package/src/freeday-chart.js +403 -0
  111. package/src/freeday-chip.js +83 -0
  112. package/src/freeday-datepicker.js +321 -0
  113. package/src/freeday-datetime.js +59 -0
  114. package/src/freeday-drawer.js +43 -0
  115. package/src/freeday-form.js +181 -0
  116. package/src/freeday-mask.js +114 -0
  117. package/src/freeday-menu.js +93 -0
  118. package/src/freeday-popover.js +69 -0
  119. package/src/freeday-rating.js +50 -0
  120. package/src/freeday-select.js +194 -0
  121. package/src/freeday-slider.js +34 -0
  122. package/src/freeday-stepper.js +90 -0
  123. package/src/freeday-table.js +475 -0
  124. package/src/freeday-tabs.js +68 -0
  125. package/src/freeday-timepicker.js +180 -0
  126. package/src/freeday-toast.js +84 -0
  127. package/src/freeday-tree.js +94 -0
  128. package/src/freeday-upload.js +206 -0
  129. package/tokens/breakpoints.mjs +4 -0
  130. package/tokens/tokens.json +123 -0
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ export interface DateRangeValue {
3
+ start: string | null;
4
+ end: string | null;
5
+ }
6
+ </script>
7
+
8
+ <script setup lang="ts">
9
+ import { computed, type ComputedRef } from 'vue';
10
+ import FdyDatepicker from './FdyDatepicker.vue';
11
+
12
+ // A controlled date range over freeday's `.fdy-daterange` layout (src/components/datepicker.css):
13
+ // two linked FdyDatepickers where the end can't precede the start (start.max = end, end.min =
14
+ // start). A real `v-model:DateRangeValue` that composes the single-date picker rather than
15
+ // re-implementing the calendar. The vanilla enhancer's cross-calendar `.in-range` day shading is
16
+ // not reproduced (each picker is independent) — the min/max linkage is what keeps the range valid.
17
+
18
+ const props = defineProps<{
19
+ modelValue: DateRangeValue;
20
+ min?: string;
21
+ max?: string;
22
+ locale?: string;
23
+ disabled?: boolean;
24
+ invalid?: boolean;
25
+ describedby?: string;
26
+ startPlaceholder?: string;
27
+ endPlaceholder?: string;
28
+ ariaLabel?: string;
29
+ ariaLabelledby?: string;
30
+ }>();
31
+
32
+ const emit = defineEmits<{
33
+ 'update:modelValue': [value: DateRangeValue];
34
+ change: [value: DateRangeValue];
35
+ }>();
36
+
37
+ const startMax: ComputedRef<string | undefined> = computed((): string | undefined => props.modelValue.end ?? props.max);
38
+ const endMin: ComputedRef<string | undefined> = computed((): string | undefined => props.modelValue.start ?? props.min);
39
+
40
+ function setStart(start: string): void {
41
+ const next: DateRangeValue = { start, end: props.modelValue.end };
42
+ emit('update:modelValue', next);
43
+ emit('change', next);
44
+ }
45
+ function setEnd(end: string): void {
46
+ const next: DateRangeValue = { start: props.modelValue.start, end };
47
+ emit('update:modelValue', next);
48
+ emit('change', next);
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <div class="fdy-daterange" role="group" :aria-label="ariaLabel" :aria-labelledby="ariaLabelledby">
54
+ <FdyDatepicker
55
+ :model-value="modelValue.start"
56
+ :min="min"
57
+ :max="startMax"
58
+ :locale="locale"
59
+ :disabled="disabled"
60
+ :invalid="invalid"
61
+ :describedby="describedby"
62
+ :placeholder="startPlaceholder ?? 'Dari'"
63
+ @update:model-value="setStart"
64
+ />
65
+ <span class="fdy-daterange__sep" aria-hidden="true">–</span>
66
+ <FdyDatepicker
67
+ :model-value="modelValue.end"
68
+ :min="endMin"
69
+ :max="max"
70
+ :locale="locale"
71
+ :disabled="disabled"
72
+ :invalid="invalid"
73
+ :describedby="describedby"
74
+ :placeholder="endPlaceholder ?? 'Sampai'"
75
+ @update:model-value="setEnd"
76
+ />
77
+ </div>
78
+ </template>
@@ -0,0 +1,333 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, type ComputedRef, type Ref } from 'vue';
3
+ import { usePopover } from '../usePopover';
4
+
5
+ // A Vue-native binding over freeday's `.fdy-datepicker` / `.fdy-cal__*` classes
6
+ // (see src/components/datepicker.css). freeday ships the styled trigger+popover
7
+ // pair plus a framework-agnostic enhancer (freeday-datepicker.js) whose own
8
+ // header says the DOM it builds is meant for markup it owns wholesale. Letting
9
+ // that enhancer manage the DOM would fight Vue's rendering, so this component
10
+ // re-implements its single-date calendar behavior (month grid, keyboard nav,
11
+ // min/max clamping, Intl locale formatting) in Vue, giving a real `v-model`
12
+ // over the same kit classes. Open/close (outside-click + focusout) mirrors the
13
+ // already-approved FdyCombo.vue pattern. Range mode is out of scope (v1 = single date).
14
+
15
+ interface DayCell {
16
+ date: Date;
17
+ iso: string;
18
+ label: number;
19
+ ariaLabel: string;
20
+ outside: boolean;
21
+ today: boolean;
22
+ selected: boolean;
23
+ disabled: boolean;
24
+ focusable: boolean;
25
+ }
26
+
27
+ const props = defineProps<{
28
+ modelValue: string | null;
29
+ min?: string;
30
+ max?: string;
31
+ locale?: string;
32
+ placeholder?: string;
33
+ disabled?: boolean;
34
+ invalid?: boolean;
35
+ describedby?: string;
36
+ id?: string;
37
+ ariaLabelledby?: string;
38
+ }>();
39
+
40
+ const emit = defineEmits<{
41
+ 'update:modelValue': [value: string];
42
+ change: [value: string];
43
+ }>();
44
+
45
+ function pad(n: number): string {
46
+ return n < 10 ? `0${n}` : `${n}`;
47
+ }
48
+ function toISO(d: Date): string {
49
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
50
+ }
51
+ function parseISO(s: string | null | undefined): Date | null {
52
+ if (s === null || s === undefined || s === '') return null;
53
+ const parts: string[] = s.split('-');
54
+ if (parts.length !== 3) return null;
55
+ const y: number = Number(parts[0]);
56
+ const m: number = Number(parts[1]);
57
+ const day: number = Number(parts[2]);
58
+ const d: Date = new Date(y, m - 1, day);
59
+ return Number.isNaN(d.getTime()) ? null : d;
60
+ }
61
+ function startOfDay(d: Date): Date {
62
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
63
+ }
64
+ function startOfMonth(d: Date): Date {
65
+ return new Date(d.getFullYear(), d.getMonth(), 1);
66
+ }
67
+ function sameDay(a: Date | null, b: Date | null): boolean {
68
+ return a !== null && b !== null && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
69
+ }
70
+ function addDays(d: Date, n: number): Date {
71
+ const x: Date = new Date(d);
72
+ x.setDate(x.getDate() + n);
73
+ return x;
74
+ }
75
+ function addMonths(d: Date, n: number): Date {
76
+ const x: Date = new Date(d);
77
+ x.setMonth(x.getMonth() + n);
78
+ return x;
79
+ }
80
+
81
+ const baseId: string = useId();
82
+ const triggerId: ComputedRef<string> = computed((): string => props.id ?? `${baseId}-trigger`);
83
+ const titleId: string = `${baseId}-title`;
84
+
85
+ const rootEl: Ref<HTMLDivElement | null> = ref(null);
86
+ const triggerEl: Ref<HTMLButtonElement | null> = ref(null);
87
+ const panelEl: Ref<HTMLDivElement | null> = ref(null);
88
+ const dayRefs: Record<string, HTMLButtonElement | null> = {};
89
+
90
+ const open: Ref<boolean> = ref(false);
91
+
92
+ // Render the calendar panel in the top layer (popover) so it never gets clipped by a card or
93
+ // scroll container; positions against the trigger.
94
+ usePopover(panelEl, triggerEl, open);
95
+
96
+ const selectedDate: ComputedRef<Date | null> = computed((): Date | null => parseISO(props.modelValue));
97
+ const minDate: ComputedRef<Date | null> = computed((): Date | null => parseISO(props.min));
98
+ const maxDate: ComputedRef<Date | null> = computed((): Date | null => parseISO(props.max));
99
+ const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
100
+ const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
101
+ const displayPlaceholder: ComputedRef<string> = computed((): string => props.placeholder ?? 'Pilih tanggal');
102
+
103
+ const view: Ref<Date> = ref(startOfMonth(selectedDate.value ?? new Date()));
104
+ const focusDate: Ref<Date> = ref(selectedDate.value ?? new Date());
105
+
106
+ // Keep the roving-tabindex cell inside the visible month so the grid stays keyboard-reachable
107
+ // after navigating with Home/End/PageUp/PageDown (mirrors freeday-datepicker.js's clamp).
108
+ const effectiveFocusDate: ComputedRef<Date> = computed((): Date => {
109
+ const f: Date = focusDate.value;
110
+ if (f.getMonth() === view.value.getMonth() && f.getFullYear() === view.value.getFullYear()) return f;
111
+ const daysInMonth: number = new Date(view.value.getFullYear(), view.value.getMonth() + 1, 0).getDate();
112
+ return new Date(view.value.getFullYear(), view.value.getMonth(), Math.min(f.getDate(), daysInMonth));
113
+ });
114
+
115
+ const monthFmt: ComputedRef<Intl.DateTimeFormat> = computed(
116
+ (): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { month: 'long', year: 'numeric' }),
117
+ );
118
+ const valueFmt: ComputedRef<Intl.DateTimeFormat> = computed(
119
+ (): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { day: '2-digit', month: 'short', year: 'numeric' }),
120
+ );
121
+ const dayLabelFmt: ComputedRef<Intl.DateTimeFormat> = computed(
122
+ (): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }),
123
+ );
124
+ const dowFmt: ComputedRef<Intl.DateTimeFormat> = computed(
125
+ (): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { weekday: 'short' }),
126
+ );
127
+
128
+ const displayValue: ComputedRef<string> = computed((): string =>
129
+ selectedDate.value !== null ? valueFmt.value.format(selectedDate.value) : displayPlaceholder.value,
130
+ );
131
+
132
+ const weekdayHeaders: ComputedRef<string[]> = computed((): string[] => {
133
+ const monday: Date = new Date(2024, 0, 1); // a known Monday
134
+ return Array.from({ length: 7 }, (_unused: unknown, i: number): string => dowFmt.value.format(addDays(monday, i)));
135
+ });
136
+
137
+ function isDayDisabled(d: Date): boolean {
138
+ if (minDate.value !== null && startOfDay(d).getTime() < startOfDay(minDate.value).getTime()) return true;
139
+ if (maxDate.value !== null && startOfDay(d).getTime() > startOfDay(maxDate.value).getTime()) return true;
140
+ return false;
141
+ }
142
+
143
+ const gridCells: ComputedRef<DayCell[]> = computed((): DayCell[] => {
144
+ const first: Date = new Date(view.value.getFullYear(), view.value.getMonth(), 1);
145
+ const offset: number = (first.getDay() + 6) % 7; // Monday-first
146
+ const startCell: Date = addDays(first, -offset);
147
+ const today: Date = new Date();
148
+ const focus: Date = effectiveFocusDate.value;
149
+ const cells: DayCell[] = [];
150
+ for (let i = 0; i < 42; i++) {
151
+ const d: Date = addDays(startCell, i);
152
+ cells.push({
153
+ date: d,
154
+ iso: toISO(d),
155
+ label: d.getDate(),
156
+ ariaLabel: dayLabelFmt.value.format(d),
157
+ outside: d.getMonth() !== view.value.getMonth(),
158
+ today: sameDay(d, today),
159
+ selected: sameDay(d, selectedDate.value),
160
+ disabled: isDayDisabled(d),
161
+ focusable: sameDay(d, focus),
162
+ });
163
+ }
164
+ return cells;
165
+ });
166
+
167
+ function setDayRef(iso: string, el: Element | null): void {
168
+ dayRefs[iso] = el as HTMLButtonElement | null;
169
+ }
170
+
171
+ function focusCell(): void {
172
+ dayRefs[toISO(effectiveFocusDate.value)]?.focus();
173
+ }
174
+
175
+ function moveFocus(next: Date): void {
176
+ focusDate.value = next;
177
+ view.value = new Date(next.getFullYear(), next.getMonth(), 1);
178
+ void nextTick((): void => focusCell());
179
+ }
180
+
181
+ function pick(d: Date): void {
182
+ if (isDayDisabled(d)) return;
183
+ const day: Date = startOfDay(d);
184
+ focusDate.value = day;
185
+ const iso: string = toISO(day);
186
+ emit('update:modelValue', iso);
187
+ emit('change', iso);
188
+ closePanel(true);
189
+ }
190
+
191
+ function openPanel(): void {
192
+ if (isDisabled.value || open.value) return;
193
+ focusDate.value = selectedDate.value ?? focusDate.value ?? new Date();
194
+ view.value = new Date(focusDate.value.getFullYear(), focusDate.value.getMonth(), 1);
195
+ open.value = true;
196
+ void nextTick((): void => focusCell());
197
+ }
198
+
199
+ function closePanel(returnFocus: boolean): void {
200
+ if (!open.value) return;
201
+ open.value = false;
202
+ if (returnFocus) triggerEl.value?.focus();
203
+ }
204
+
205
+ function toggle(): void {
206
+ if (open.value) closePanel(false);
207
+ else openPanel();
208
+ }
209
+
210
+ function onGridKeydown(e: KeyboardEvent): void {
211
+ const f: Date = effectiveFocusDate.value;
212
+ switch (e.key) {
213
+ case 'ArrowLeft':
214
+ e.preventDefault();
215
+ moveFocus(addDays(f, -1));
216
+ break;
217
+ case 'ArrowRight':
218
+ e.preventDefault();
219
+ moveFocus(addDays(f, 1));
220
+ break;
221
+ case 'ArrowUp':
222
+ e.preventDefault();
223
+ moveFocus(addDays(f, -7));
224
+ break;
225
+ case 'ArrowDown':
226
+ e.preventDefault();
227
+ moveFocus(addDays(f, 7));
228
+ break;
229
+ case 'Home':
230
+ e.preventDefault();
231
+ moveFocus(addDays(f, -((f.getDay() + 6) % 7)));
232
+ break;
233
+ case 'End':
234
+ e.preventDefault();
235
+ moveFocus(addDays(f, 6 - ((f.getDay() + 6) % 7)));
236
+ break;
237
+ case 'PageUp':
238
+ e.preventDefault();
239
+ moveFocus(addMonths(f, e.shiftKey ? -12 : -1));
240
+ break;
241
+ case 'PageDown':
242
+ e.preventDefault();
243
+ moveFocus(addMonths(f, e.shiftKey ? 12 : 1));
244
+ break;
245
+ case 'Enter':
246
+ case ' ':
247
+ e.preventDefault();
248
+ pick(f);
249
+ break;
250
+ case 'Escape':
251
+ e.preventDefault();
252
+ closePanel(true);
253
+ break;
254
+ default:
255
+ break;
256
+ }
257
+ }
258
+
259
+ // Close when a pointer lands outside the whole control.
260
+ function onDocPointerDown(e: MouseEvent): void {
261
+ const target: EventTarget | null = e.target;
262
+ if (rootEl.value !== null && target instanceof Node && !rootEl.value.contains(target)) {
263
+ closePanel(false);
264
+ }
265
+ }
266
+
267
+ // Close when focus leaves the control entirely (e.g. Shift+Tab off the trigger).
268
+ function onFocusout(e: FocusEvent): void {
269
+ const next: EventTarget | null = e.relatedTarget;
270
+ if (rootEl.value !== null && !(next instanceof Node && rootEl.value.contains(next))) {
271
+ closePanel(false);
272
+ }
273
+ }
274
+
275
+ onMounted((): void => {
276
+ document.addEventListener('mousedown', onDocPointerDown);
277
+ });
278
+ onBeforeUnmount((): void => {
279
+ document.removeEventListener('mousedown', onDocPointerDown);
280
+ });
281
+ </script>
282
+
283
+ <template>
284
+ <div ref="rootEl" class="fdy-datepicker" :class="{ 'fdy-datepicker--error': isInvalid }" @focusout="onFocusout">
285
+ <button
286
+ :id="triggerId"
287
+ ref="triggerEl"
288
+ type="button"
289
+ class="fdy-datepicker__trigger"
290
+ :class="{ 'is-open': open }"
291
+ aria-haspopup="dialog"
292
+ :aria-expanded="open"
293
+ :aria-labelledby="ariaLabelledby"
294
+ :aria-invalid="isInvalid ? 'true' : undefined"
295
+ :aria-describedby="describedby"
296
+ :disabled="isDisabled"
297
+ @click="toggle"
298
+ >
299
+ <span class="fdy-datepicker__value" :class="{ 'fdy-datepicker__value--placeholder': selectedDate === null }">{{ displayValue }}</span>
300
+ <span class="fdy-datepicker__icon" aria-hidden="true">
301
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
302
+ <rect x="3" y="4" width="18" height="18" rx="2"></rect>
303
+ <path d="M16 2v4M8 2v4M3 10h18"></path>
304
+ </svg>
305
+ </span>
306
+ </button>
307
+
308
+ <div ref="panelEl" class="fdy-datepicker__panel" role="dialog" aria-modal="false" popover="manual" :aria-labelledby="titleId" :hidden="!open">
309
+ <div class="fdy-cal__head">
310
+ <button type="button" class="fdy-cal__nav" aria-label="Bulan sebelumnya" @click="view = addMonths(view, -1)">‹</button>
311
+ <div :id="titleId" class="fdy-cal__title">{{ monthFmt.format(view) }}</div>
312
+ <button type="button" class="fdy-cal__nav" aria-label="Bulan berikutnya" @click="view = addMonths(view, 1)">›</button>
313
+ </div>
314
+ <div class="fdy-cal__grid" role="grid" :aria-labelledby="titleId" @keydown="onGridKeydown">
315
+ <div v-for="w in weekdayHeaders" :key="w" class="fdy-cal__dow" role="columnheader" :aria-label="w">{{ w }}</div>
316
+ <button
317
+ v-for="cell in gridCells"
318
+ :key="cell.iso"
319
+ :ref="(el) => setDayRef(cell.iso, el as Element | null)"
320
+ type="button"
321
+ class="fdy-cal__day"
322
+ :class="{ 'is-outside': cell.outside, 'is-today': cell.today, 'is-selected': cell.selected }"
323
+ role="gridcell"
324
+ :aria-label="cell.ariaLabel"
325
+ :aria-selected="cell.selected ? 'true' : undefined"
326
+ :disabled="cell.disabled"
327
+ :tabindex="cell.focusable ? 0 : -1"
328
+ @click="pick(cell.date)"
329
+ >{{ cell.label }}</button>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </template>
@@ -0,0 +1,44 @@
1
+ import type { Ref } from 'vue';
2
+
3
+ export interface UseFreedayReturn {
4
+ /** Manually re-run all Freeday enhancers over the scoped subtree. */
5
+ rehydrate: () => void;
6
+ }
7
+
8
+ /**
9
+ * Auto-initialise Freeday enhancers over a component's subtree on mount and
10
+ * after every update. Pass a template ref to scope the work; omit it to scan
11
+ * the whole document.
12
+ */
13
+ export declare function useFreeday(rootRef?: Ref<HTMLElement | null>): UseFreedayReturn;
14
+
15
+ export { default as FdyCombo } from './components/FdyCombo.vue';
16
+ export { default as FdyDatepicker } from './components/FdyDatepicker.vue';
17
+ export { default as FdyDateRange } from './components/FdyDateRange.vue';
18
+ export type { DateRangeValue } from './components/FdyDateRange.vue';
19
+ export { default as FdyAutocomplete } from './components/FdyAutocomplete.vue';
20
+ export { default as FdyCascade } from './components/FdyCascade.vue';
21
+ export type { CascadeNode } from './components/FdyCascade.vue';
22
+ export { default as FdyCfl } from './components/FdyCfl.vue';
23
+ export { default as FdyChart } from './components/FdyChart.vue';
24
+
25
+ /** One data series for the cartesian chart types (line / area / multi-series & stacked bar). */
26
+ export interface FdyChartSeries {
27
+ label: string;
28
+ values: number[];
29
+ role?: string;
30
+ }
31
+
32
+ /** `event.detail` shapes for the bubbling `fdy-*` CustomEvents. */
33
+ export interface FdyChangeDetail { value: string }
34
+ export interface FdyAutocompleteSelectDetail { value: string }
35
+ export interface FdyCascadeChangeDetail { value: string; path: string; labels: string[] }
36
+ export interface FdyDatepickerChangeDetail { value: string; date: Date | null }
37
+ export interface FdyTimeSelectDetail { value: string }
38
+ export interface FdyDatetimeChangeDetail { date: string; time: string; value: string }
39
+ export interface FdyMaskDetail { value: string; raw: string }
40
+ export interface FdyFormInvalidDetail { invalid: HTMLElement[] }
41
+ export interface FdyChipChangeDetail { value: string; pressed: boolean; selected: string[] }
42
+ export interface FdyChipRemoveDetail { value: string }
43
+ export interface FdyCflSelectDetail { row?: unknown; rows?: unknown[] }
44
+ export interface FdyTableChangeDetail { page: number; sort: string | null; query: string }
@@ -0,0 +1,8 @@
1
+ export { useFreeday } from './useFreeday.js';
2
+ export { default as FdyCombo } from './components/FdyCombo.vue';
3
+ export { default as FdyDatepicker } from './components/FdyDatepicker.vue';
4
+ export { default as FdyDateRange } from './components/FdyDateRange.vue';
5
+ export { default as FdyAutocomplete } from './components/FdyAutocomplete.vue';
6
+ export { default as FdyCascade } from './components/FdyCascade.vue';
7
+ export { default as FdyCfl } from './components/FdyCfl.vue';
8
+ export { default as FdyChart } from './components/FdyChart.vue';
@@ -0,0 +1,44 @@
1
+ /* Freeday — Vue 3 adapter (thin). The enhancers stay the source of truth; this
2
+ * composable just re-runs every registered window.Freeday*.initAll() over a
3
+ * subtree after Vue renders it, so [data-fdy-*] markup rendered by Vue gets
4
+ * enhanced (and re-enhanced after updates). Enhancers are idempotent, so the
5
+ * repeated calls are safe. Requires `import 'freeday'` once at app entry to
6
+ * register the enhancers.
7
+ *
8
+ * Usage:
9
+ * const root = ref(null);
10
+ * useFreeday(root); // scopes init to this component's subtree
11
+ * // <div ref="root"> ...[data-fdy-*] markup... </div>
12
+ *
13
+ * Events come straight through as native bubbling CustomEvents — bind them with
14
+ * plain v-on (e.g. @fdy-cascade-change) and read event.detail. See index.d.ts
15
+ * for the detail types.
16
+ */
17
+ import { onMounted, onUpdated, nextTick } from 'vue';
18
+
19
+ // Discover Freeday enhancer namespaces once (all registered by the time an app
20
+ // mounts, since `import 'freeday'` runs first). Each exposes initAll(el?).
21
+ function freedayApis() {
22
+ var apis = [];
23
+ var names = Object.getOwnPropertyNames(window);
24
+ for (var i = 0; i < names.length; i++) {
25
+ var key = names[i];
26
+ if (key.slice(0, 7) === 'Freeday') {
27
+ var api = window[key];
28
+ if (api && typeof api.initAll === 'function') apis.push(api);
29
+ }
30
+ }
31
+ return apis;
32
+ }
33
+
34
+ export function useFreeday(rootRef) {
35
+ var apis = null;
36
+ function rehydrate() {
37
+ if (!apis) apis = freedayApis();
38
+ var el = rootRef && 'value' in rootRef ? (rootRef.value || undefined) : undefined;
39
+ for (var i = 0; i < apis.length; i++) apis[i].initAll(el);
40
+ }
41
+ onMounted(rehydrate);
42
+ onUpdated(function () { nextTick(rehydrate); });
43
+ return { rehydrate: rehydrate };
44
+ }
@@ -0,0 +1,78 @@
1
+ import { watch, onBeforeUnmount, type Ref } from 'vue';
2
+
3
+ // Vue equivalent of the vanilla freeday-popover.js helper. Lifts a dropdown panel into the top
4
+ // layer via the native Popover API so it escapes any ancestor overflow clip (a card, a scroll
5
+ // container), then positions it `fixed` against its trigger (flip above when there's no room
6
+ // below; match the trigger width). The panel stays in the component's DOM, so focus / outside-
7
+ // click / ARIA are untouched. Degrades to the panel's own [hidden]/absolute CSS where the
8
+ // Popover API is unavailable. Drive it with the component's existing `open` ref.
9
+
10
+ interface PopoverElement extends HTMLElement {
11
+ showPopover(): void;
12
+ hidePopover(): void;
13
+ }
14
+
15
+ const GAP = 4; // ~ --space-1
16
+ const supported: boolean =
17
+ typeof HTMLElement !== 'undefined'
18
+ && typeof (HTMLElement.prototype as Partial<PopoverElement>).showPopover === 'function';
19
+
20
+ function place(panel: HTMLElement, trigger: HTMLElement): void {
21
+ const r: DOMRect = trigger.getBoundingClientRect();
22
+ panel.style.position = 'fixed';
23
+ panel.style.margin = '0';
24
+ panel.style.inset = 'auto';
25
+ panel.style.minWidth = `${r.width}px`;
26
+ const ph: number = panel.offsetHeight;
27
+ const pw: number = panel.offsetWidth;
28
+ const vw: number = document.documentElement.clientWidth;
29
+ const vh: number = document.documentElement.clientHeight;
30
+ const below: number = vh - r.bottom - GAP;
31
+ const above: number = r.top - GAP;
32
+ const top: number = ph <= below || below >= above ? r.bottom + GAP : Math.max(GAP, r.top - GAP - ph);
33
+ let left: number = r.left;
34
+ if (left + pw > vw - GAP) left = Math.max(GAP, vw - GAP - pw);
35
+ panel.style.top = `${Math.round(top)}px`;
36
+ panel.style.left = `${Math.round(left)}px`;
37
+ }
38
+
39
+ /**
40
+ * Anchor a dropdown `panelRef` to `triggerRef` in the top layer while `open` is true.
41
+ * The panel element must carry `popover="manual"` in the template (ignored where unsupported).
42
+ */
43
+ export function usePopover(
44
+ panelRef: Ref<HTMLElement | null>,
45
+ triggerRef: Ref<HTMLElement | null>,
46
+ open: Ref<boolean>,
47
+ ): void {
48
+ function reposition(): void {
49
+ const panel: PopoverElement | null = panelRef.value as PopoverElement | null;
50
+ const trigger: HTMLElement | null = triggerRef.value;
51
+ if (supported && panel !== null && trigger !== null && panel.matches(':popover-open')) place(panel, trigger);
52
+ }
53
+
54
+ watch(
55
+ open,
56
+ (isOpen: boolean): void => {
57
+ const panel: PopoverElement | null = panelRef.value as PopoverElement | null;
58
+ const trigger: HTMLElement | null = triggerRef.value;
59
+ if (!supported || panel === null || trigger === null) return;
60
+ if (isOpen) {
61
+ if (!panel.matches(':popover-open')) panel.showPopover();
62
+ place(panel, trigger);
63
+ window.addEventListener('scroll', reposition, true);
64
+ window.addEventListener('resize', reposition);
65
+ } else {
66
+ window.removeEventListener('scroll', reposition, true);
67
+ window.removeEventListener('resize', reposition);
68
+ if (panel.matches(':popover-open')) panel.hidePopover();
69
+ }
70
+ },
71
+ { flush: 'post' },
72
+ );
73
+
74
+ onBeforeUnmount((): void => {
75
+ window.removeEventListener('scroll', reposition, true);
76
+ window.removeEventListener('resize', reposition);
77
+ });
78
+ }