pukaad-ui-lib 1.332.0 → 1.334.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 (49) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/button.vue +10 -10
  3. package/dist/runtime/components/card/card-place-detail.vue +5 -4
  4. package/dist/runtime/components/card/card-place-search-row.d.vue.ts +20 -0
  5. package/dist/runtime/components/card/card-place-search-row.vue +47 -0
  6. package/dist/runtime/components/card/card-place-search-row.vue.d.ts +20 -0
  7. package/dist/runtime/components/carousel.vue +31 -31
  8. package/dist/runtime/components/chart/chart-approval-activity.d.vue.ts +35 -0
  9. package/dist/runtime/components/chart/chart-approval-activity.vue +152 -0
  10. package/dist/runtime/components/chart/chart-approval-activity.vue.d.ts +35 -0
  11. package/dist/runtime/components/display/display-image-place.vue +98 -98
  12. package/dist/runtime/components/image/image-cropper.d.vue.ts +2 -2
  13. package/dist/runtime/components/image/image-cropper.vue.d.ts +2 -2
  14. package/dist/runtime/components/input/input-autocomplete.d.vue.ts +1 -1
  15. package/dist/runtime/components/input/input-autocomplete.vue.d.ts +1 -1
  16. package/dist/runtime/components/input/input-birth-date.d.vue.ts +1 -1
  17. package/dist/runtime/components/input/input-birth-date.vue.d.ts +1 -1
  18. package/dist/runtime/components/input/input-combobox.d.vue.ts +1 -1
  19. package/dist/runtime/components/input/input-combobox.vue.d.ts +1 -1
  20. package/dist/runtime/components/input/input-date-picker.d.vue.ts +1 -1
  21. package/dist/runtime/components/input/input-date-picker.vue.d.ts +1 -1
  22. package/dist/runtime/components/input/input-rating.vue +6 -6
  23. package/dist/runtime/components/input/input-suggest.d.vue.ts +1 -1
  24. package/dist/runtime/components/input/input-suggest.vue.d.ts +1 -1
  25. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  26. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  27. package/dist/runtime/components/modal/modal-media-view.vue +136 -136
  28. package/dist/runtime/components/ui/chart/ChartContainer.d.vue.ts +25 -0
  29. package/dist/runtime/components/ui/chart/ChartContainer.vue +49 -0
  30. package/dist/runtime/components/ui/chart/ChartContainer.vue.d.ts +25 -0
  31. package/dist/runtime/components/ui/chart/ChartLegendContent.d.vue.ts +12 -0
  32. package/dist/runtime/components/ui/chart/ChartLegendContent.vue +52 -0
  33. package/dist/runtime/components/ui/chart/ChartLegendContent.vue.d.ts +12 -0
  34. package/dist/runtime/components/ui/chart/ChartStyle.d.vue.ts +7 -0
  35. package/dist/runtime/components/ui/chart/ChartStyle.vue +32 -0
  36. package/dist/runtime/components/ui/chart/ChartStyle.vue.d.ts +7 -0
  37. package/dist/runtime/components/ui/chart/ChartTooltipContent.d.vue.ts +32 -0
  38. package/dist/runtime/components/ui/chart/ChartTooltipContent.vue +93 -0
  39. package/dist/runtime/components/ui/chart/ChartTooltipContent.vue.d.ts +32 -0
  40. package/dist/runtime/components/ui/chart/index.d.ts +27 -0
  41. package/dist/runtime/components/ui/chart/index.js +8 -0
  42. package/dist/runtime/components/ui/chart/utils.d.ts +11 -0
  43. package/dist/runtime/components/ui/chart/utils.js +24 -0
  44. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.d.vue.ts +2 -2
  45. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.vue.d.ts +2 -2
  46. package/dist/runtime/components/ui/native-select/NativeSelectOption.d.vue.ts +2 -2
  47. package/dist/runtime/components/ui/native-select/NativeSelectOption.vue.d.ts +2 -2
  48. package/package.json +5 -3
  49. /package/dist/runtime/assets/svg/socials/{Whatsapp.svg → WhatsApp.svg} +0 -0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.332.0",
4
+ "version": "1.334.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -4,16 +4,16 @@
4
4
  props.variant === 'text' || props.variant === 'link' ? '!p-0 !h-auto' : '',
5
5
  props.circle && 'rounded-full aspect-square',
6
6
  props.class
7
- ]" :variant="props.variant" :color="props.color" :size="resolvedSize" :type="props.type"
8
- :disabled="props.loading || props.disabled">
9
- <span class="inline-grid place-items-center">
10
- <span class="inline-flex items-center justify-center gap-[8px] [grid-area:1/1]"
11
- :class="{ invisible: props.loading }">
12
- <slot />
13
- </span>
14
- <ShadSpinner v-if="props.loading" class="[grid-area:1/1]" />
15
- </span>
16
- </ShadButton>
7
+ ]" :variant="props.variant" :color="props.color" :size="resolvedSize" :type="props.type"
8
+ :disabled="props.loading || props.disabled">
9
+ <span class="inline-grid place-items-center">
10
+ <span class="inline-flex items-center justify-center gap-[8px] [grid-area:1/1]"
11
+ :class="{ invisible: props.loading }">
12
+ <slot />
13
+ </span>
14
+ <ShadSpinner v-if="props.loading" class="[grid-area:1/1]" />
15
+ </span>
16
+ </ShadButton>
17
17
  </template>
18
18
 
19
19
  <script setup>
@@ -92,17 +92,18 @@ const DAY_NAMES = [
92
92
  const props = defineProps({
93
93
  item: { type: Object, required: false, default: () => ({}) }
94
94
  });
95
+ const toArray = (value) => Array.isArray(value) ? value : [];
95
96
  const categoryText = computed(
96
- () => (props.item.categories ?? []).map((c) => c.category_name).join(", ")
97
+ () => toArray(props.item.categories).filter((c) => c?.category_name).map((c) => c.category_name).join(", ")
97
98
  );
98
99
  const businessHours = computed(
99
- () => (props.item.opening_hours ?? []).map((h) => ({
100
+ () => toArray(props.item.opening_hours).filter((h) => !!h).map((h) => ({
100
101
  day: DAY_NAMES[h.day] ?? "",
101
- time_range: !h.isOpen ? "\u0E2B\u0E22\u0E38\u0E14\u0E17\u0E33\u0E01\u0E32\u0E23" : h.isAllDay ? "\u0E40\u0E1B\u0E34\u0E14\u0E15\u0E25\u0E2D\u0E14 24 \u0E0A\u0E21." : (h.timeSlots ?? []).map((t) => `${t.start} - ${t.end}`).join(", ") || "\u0E40\u0E1B\u0E34\u0E14"
102
+ time_range: !h.isOpen ? "\u0E2B\u0E22\u0E38\u0E14\u0E17\u0E33\u0E01\u0E32\u0E23" : h.isAllDay ? "\u0E40\u0E1B\u0E34\u0E14\u0E15\u0E25\u0E2D\u0E14 24 \u0E0A\u0E21." : toArray(h.timeSlots).map((t) => `${t.start} - ${t.end}`).join(", ") || "\u0E40\u0E1B\u0E34\u0E14"
102
103
  }))
103
104
  );
104
105
  const socialLinks = computed(
105
- () => (props.item.contact_channels ?? []).filter((ch) => ch.value).map((ch) => ({
106
+ () => toArray(props.item.contact_channels).filter((ch) => ch?.value).map((ch) => ({
106
107
  name: ch.channel_type ?? "link",
107
108
  link: ch.value
108
109
  }))
@@ -0,0 +1,20 @@
1
+ export interface PlaceSearchRowItem {
2
+ id: string;
3
+ business_name: string;
4
+ image_cover?: string;
5
+ rating?: number;
6
+ review_count?: number;
7
+ address?: {
8
+ full_address?: string;
9
+ } | null;
10
+ }
11
+ export interface CardPlaceSearchRowProps {
12
+ item: PlaceSearchRowItem;
13
+ }
14
+ declare const __VLS_export: import("vue").DefineComponent<CardPlaceSearchRowProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ select: (item: PlaceSearchRowItem) => any;
16
+ }, string, import("vue").PublicProps, Readonly<CardPlaceSearchRowProps> & Readonly<{
17
+ onSelect?: ((item: PlaceSearchRowItem) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div
3
+ class="flex items-center gap-[8px] px-[8px] py-[6px] rounded-md hover:bg-bright cursor-pointer"
4
+ @click="emit('select', props.item)"
5
+ >
6
+ <!-- thumbnail -->
7
+ <Image
8
+ v-if="props.item.image_cover"
9
+ :src="props.item.image_cover"
10
+ :width="71"
11
+ :height="40"
12
+ class="shrink-0 rounded-[4px] object-cover"
13
+ />
14
+ <div
15
+ v-else
16
+ class="w-[71px] h-[40px] bg-cloud shrink-0 rounded-[4px] flex items-center justify-center"
17
+ >
18
+ <Icon name="fa6-solid:map-location-dot" :size="20" class="text-gray" />
19
+ </div>
20
+
21
+ <!-- content -->
22
+ <div class="flex flex-col gap-[4px] min-w-0 flex-1">
23
+ <div class="font-body-medium text-black truncate">
24
+ {{ props.item.business_name }}
25
+ </div>
26
+ <div class="flex items-center gap-[4px] min-w-0">
27
+ <InputRating :model-value="props.item.rating ?? 0" readonly :size="14" />
28
+ <template v-if="props.item.review_count != null">
29
+ <span class="font-body-small text-gray shrink-0">
30
+ {{ $convert.convertNumber(props.item.review_count) }}
31
+ </span>
32
+ <span class="font-body-small text-gray shrink-0">•</span>
33
+ </template>
34
+ <span class="font-body-small text-gray truncate">
35
+ {{ props.item.address?.full_address }}
36
+ </span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </template>
41
+
42
+ <script setup>
43
+ const props = defineProps({
44
+ item: { type: Object, required: true }
45
+ });
46
+ const emit = defineEmits(["select"]);
47
+ </script>
@@ -0,0 +1,20 @@
1
+ export interface PlaceSearchRowItem {
2
+ id: string;
3
+ business_name: string;
4
+ image_cover?: string;
5
+ rating?: number;
6
+ review_count?: number;
7
+ address?: {
8
+ full_address?: string;
9
+ } | null;
10
+ }
11
+ export interface CardPlaceSearchRowProps {
12
+ item: PlaceSearchRowItem;
13
+ }
14
+ declare const __VLS_export: import("vue").DefineComponent<CardPlaceSearchRowProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ select: (item: PlaceSearchRowItem) => any;
16
+ }, string, import("vue").PublicProps, Readonly<CardPlaceSearchRowProps> & Readonly<{
17
+ onSelect?: ((item: PlaceSearchRowItem) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -1,35 +1,35 @@
1
1
  <template>
2
- <div class="relative w-full h-full">
3
- <div class="absolute top-[20px] left-1/2 transform -translate-x-1/2 z-[50]">
4
- <div class="rounded-full py-[6px] px-[16px] bg-black/60 flex items-center justify-center">
5
- <span class="text-white text-sm font-medium tracking-widest">
6
- {{ currentIndex + 1 }} / {{ props.items.length }}
7
- </span>
8
- </div>
9
- </div>
10
-
11
- <ShadCarousel
12
- :opts="{ startIndex: props.selectIndex }"
13
- class="carousel-media w-full h-full"
14
- @init-api="onInitApi"
15
- >
16
- <ShadCarouselContent class="h-full">
17
- <ShadCarouselItem
18
- v-for="(item, i) in props.items"
19
- :key="i"
20
- class="h-full flex items-center justify-center"
21
- >
22
- <img
23
- :src="item.url"
24
- class="w-full h-full object-contain select-none"
25
- draggable="false"
26
- />
27
- </ShadCarouselItem>
28
- </ShadCarouselContent>
29
- <ShadCarouselPrevious v-if="props.items.length > 1" class="!left-[12px]" />
30
- <ShadCarouselNext v-if="props.items.length > 1" class="!right-[12px]" />
31
- </ShadCarousel>
32
- </div>
2
+ <div class="relative w-full h-full">
3
+ <div class="absolute top-[20px] left-1/2 transform -translate-x-1/2 z-[50]">
4
+ <div class="rounded-full py-[6px] px-[16px] bg-black/60 flex items-center justify-center">
5
+ <span class="text-white text-sm font-medium tracking-widest">
6
+ {{ currentIndex + 1 }} / {{ props.items.length }}
7
+ </span>
8
+ </div>
9
+ </div>
10
+
11
+ <ShadCarousel
12
+ :opts="{ startIndex: props.selectIndex }"
13
+ class="carousel-media w-full h-full"
14
+ @init-api="onInitApi"
15
+ >
16
+ <ShadCarouselContent class="h-full">
17
+ <ShadCarouselItem
18
+ v-for="(item, i) in props.items"
19
+ :key="i"
20
+ class="h-full flex items-center justify-center"
21
+ >
22
+ <img
23
+ :src="item.url"
24
+ class="w-full h-full object-contain select-none"
25
+ draggable="false"
26
+ />
27
+ </ShadCarouselItem>
28
+ </ShadCarouselContent>
29
+ <ShadCarouselPrevious v-if="props.items.length > 1" class="!left-[12px]" />
30
+ <ShadCarouselNext v-if="props.items.length > 1" class="!right-[12px]" />
31
+ </ShadCarousel>
32
+ </div>
33
33
  </template>
34
34
 
35
35
  <script setup>
@@ -0,0 +1,35 @@
1
+ export type ApprovalStatusKey = "pending" | "approved" | "rejected";
2
+ export interface ApprovalSeriesPoint {
3
+ date: string | Date;
4
+ value: number;
5
+ }
6
+ export interface ApprovalSeries {
7
+ /** เดือนที่โฟกัสของแถวนี้ (controlled โดย parent) */
8
+ month?: string | Date;
9
+ /** ข้อมูลรายวันของแถวนี้ */
10
+ data?: ApprovalSeriesPoint[];
11
+ /** ตัวเลขสรุปของแถว — ถ้าไม่ส่งจะรวมจาก data ให้ */
12
+ summary?: number;
13
+ }
14
+ type __VLS_Props = {
15
+ pending?: ApprovalSeries;
16
+ approved?: ApprovalSeries;
17
+ rejected?: ApprovalSeries;
18
+ /** locale ของชื่อเดือน (default en-US ตาม Figma) */
19
+ locale?: string;
20
+ /** ความสูง component เช่น "600px" หรือ 600 — ไม่ส่ง = เต็ม parent */
21
+ height?: string | number;
22
+ };
23
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
24
+ change: (key: ApprovalStatusKey, month: Date) => any;
25
+ prev: (key: ApprovalStatusKey, month: Date) => any;
26
+ next: (key: ApprovalStatusKey, month: Date) => any;
27
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
28
+ onChange?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
29
+ onPrev?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
30
+ onNext?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
31
+ }>, {
32
+ locale: string;
33
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
@@ -0,0 +1,152 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { VisAxis, VisGroupedBar, VisXYContainer } from "@unovis/vue";
4
+ import { ChartContainer } from "#pukaad-ui/runtime/components/ui/chart";
5
+ const props = defineProps({
6
+ pending: { type: Object, required: false },
7
+ approved: { type: Object, required: false },
8
+ rejected: { type: Object, required: false },
9
+ locale: { type: String, required: false, default: "en-US" },
10
+ height: { type: [String, Number], required: false }
11
+ });
12
+ const emit = defineEmits(["change", "prev", "next"]);
13
+ const rows = [
14
+ { key: "pending", title: "\u0E23\u0E2D\u0E01\u0E32\u0E23\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19", color: "#FEF3C7" },
15
+ { key: "approved", title: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E41\u0E25\u0E49\u0E27", color: "#E3F5E3" },
16
+ { key: "rejected", title: "\u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18", color: "#FDE3E3" }
17
+ ];
18
+ const summaryMeta = [
19
+ { key: "pending", label: "\u0E23\u0E2D\u0E01\u0E32\u0E23\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19", color: "#F9A825" },
20
+ { key: "approved", label: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E41\u0E25\u0E49\u0E27", color: "#43A047" },
21
+ { key: "rejected", label: "\u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18", color: "#D32F2F" }
22
+ ];
23
+ const chartConfig = {};
24
+ const seriesByKey = computed(() => ({
25
+ pending: props.pending ?? {},
26
+ approved: props.approved ?? {},
27
+ rejected: props.rejected ?? {}
28
+ }));
29
+ const normalizedByKey = computed(() => {
30
+ const out = { pending: [], approved: [], rejected: [] };
31
+ for (const r of rows) {
32
+ out[r.key] = (seriesByKey.value[r.key].data ?? []).map((d) => ({
33
+ _date: d.date instanceof Date ? d.date : new Date(d.date),
34
+ value: d.value ?? 0
35
+ }));
36
+ }
37
+ return out;
38
+ });
39
+ const monthByKey = computed(() => {
40
+ const out = {};
41
+ for (const r of rows) {
42
+ const m = seriesByKey.value[r.key].month;
43
+ out[r.key] = m ? m instanceof Date ? m : new Date(m) : /* @__PURE__ */ new Date();
44
+ }
45
+ return out;
46
+ });
47
+ const summaryByKey = computed(() => {
48
+ const out = {};
49
+ for (const r of rows) {
50
+ const s = seriesByKey.value[r.key].summary;
51
+ out[r.key] = s ?? normalizedByKey.value[r.key].reduce((a, p) => a + p.value, 0);
52
+ }
53
+ return out;
54
+ });
55
+ const rootHeight = computed(
56
+ () => props.height == null ? void 0 : typeof props.height === "number" ? `${props.height}px` : props.height
57
+ );
58
+ const addMonths = (d, delta) => new Date(d.getFullYear(), d.getMonth() + delta, 1);
59
+ const fmtMonth = (d, withYear) => d.toLocaleDateString(props.locale, {
60
+ month: "long",
61
+ ...withYear ? { year: "numeric" } : {}
62
+ });
63
+ const monthLabelOf = (k) => fmtMonth(monthByKey.value[k], true);
64
+ const prevLabelOf = (k) => fmtMonth(addMonths(monthByKey.value[k], -1), false);
65
+ const nextLabelOf = (k) => fmtMonth(addMonths(monthByKey.value[k], 1), false);
66
+ const goPrev = (k) => {
67
+ const m = addMonths(monthByKey.value[k], -1);
68
+ emit("prev", k, m);
69
+ emit("change", k, m);
70
+ };
71
+ const goNext = (k) => {
72
+ const m = addMonths(monthByKey.value[k], 1);
73
+ emit("next", k, m);
74
+ emit("change", k, m);
75
+ };
76
+ const indicesOf = (k) => normalizedByKey.value[k].map((_, i) => i);
77
+ const dayLabelOf = (k, i) => {
78
+ const d = normalizedByKey.value[k][i]?._date;
79
+ return d ? String(d.getDate()) : "";
80
+ };
81
+ </script>
82
+
83
+ <template>
84
+ <div
85
+ class="flex flex-col gap-[16px] w-full"
86
+ :class="rootHeight ? '' : 'h-full'"
87
+ :style="rootHeight ? { height: rootHeight } : void 0"
88
+ >
89
+ <div
90
+ v-for="(row, ri) in rows"
91
+ :key="row.key"
92
+ class="flex flex-col gap-[16px] flex-1 min-h-0"
93
+ >
94
+ <div class="flex items-end justify-between gap-[16px]">
95
+ <div class="font-title-small-prominent">{{ row.title }}</div>
96
+ <div v-if="ri === 0" class="flex gap-[17px]">
97
+ <div
98
+ v-for="m in summaryMeta"
99
+ :key="m.key"
100
+ class="flex flex-col items-center border rounded-[8px] py-[8px] px-[16px] min-w-[114px]"
101
+ >
102
+ <div
103
+ class="font-title-medium-prominent"
104
+ :style="{ color: m.color }"
105
+ >
106
+ {{ summaryByKey[m.key] }}
107
+ </div>
108
+ <div class="font-body-large-prominent text-gray">{{ m.label }}</div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <ChartContainer
114
+ :config="chartConfig"
115
+ class="flex-1 min-h-0 w-full aspect-auto"
116
+ >
117
+ <VisXYContainer :data="normalizedByKey[row.key]">
118
+ <VisGroupedBar
119
+ :x="(_d, i) => i"
120
+ :y="[(d) => d.value]"
121
+ :color="[row.color]"
122
+ :rounded-corners="4"
123
+ bar-padding="0.25"
124
+ />
125
+ <VisAxis
126
+ type="x"
127
+ :x="(_d, i) => i"
128
+ :tick-values="indicesOf(row.key)"
129
+ :tick-format="(i) => dayLabelOf(row.key, i)"
130
+ :tick-line="false"
131
+ :domain-line="false"
132
+ :grid-line="false"
133
+ />
134
+ </VisXYContainer>
135
+ </ChartContainer>
136
+
137
+ <div
138
+ class="flex items-center justify-between font-label-sans-serif-small-prominent text-gray uppercase"
139
+ >
140
+ <div class="flex items-center cursor-pointer" @click="goPrev(row.key)">
141
+ <Icon name="lucide:chevrons-left" size="16" />
142
+ {{ prevLabelOf(row.key) }}
143
+ </div>
144
+ <div>{{ monthLabelOf(row.key) }}</div>
145
+ <div class="flex items-center cursor-pointer" @click="goNext(row.key)">
146
+ {{ nextLabelOf(row.key) }}
147
+ <Icon name="lucide:chevrons-right" size="16" />
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </template>
@@ -0,0 +1,35 @@
1
+ export type ApprovalStatusKey = "pending" | "approved" | "rejected";
2
+ export interface ApprovalSeriesPoint {
3
+ date: string | Date;
4
+ value: number;
5
+ }
6
+ export interface ApprovalSeries {
7
+ /** เดือนที่โฟกัสของแถวนี้ (controlled โดย parent) */
8
+ month?: string | Date;
9
+ /** ข้อมูลรายวันของแถวนี้ */
10
+ data?: ApprovalSeriesPoint[];
11
+ /** ตัวเลขสรุปของแถว — ถ้าไม่ส่งจะรวมจาก data ให้ */
12
+ summary?: number;
13
+ }
14
+ type __VLS_Props = {
15
+ pending?: ApprovalSeries;
16
+ approved?: ApprovalSeries;
17
+ rejected?: ApprovalSeries;
18
+ /** locale ของชื่อเดือน (default en-US ตาม Figma) */
19
+ locale?: string;
20
+ /** ความสูง component เช่น "600px" หรือ 600 — ไม่ส่ง = เต็ม parent */
21
+ height?: string | number;
22
+ };
23
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
24
+ change: (key: ApprovalStatusKey, month: Date) => any;
25
+ prev: (key: ApprovalStatusKey, month: Date) => any;
26
+ next: (key: ApprovalStatusKey, month: Date) => any;
27
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
28
+ onChange?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
29
+ onPrev?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
30
+ onNext?: ((key: ApprovalStatusKey, month: Date) => any) | undefined;
31
+ }>, {
32
+ locale: string;
33
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
@@ -1,102 +1,102 @@
1
1
  <template>
2
- <!-- Personal state → Featured layout — 1 รูป: w-full -->
3
- <div v-if="layoutMode === 'featured' && photos.length === 1" class="h-[360px]">
4
- <div class="w-full h-full rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 0)">
5
- <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
6
- </div>
7
- </div>
8
-
9
- <!-- Personal state → Featured layout — 2 รูป: แบ่งครึ่ง w-full -->
10
- <div v-else-if="layoutMode === 'featured' && photos.length === 2" class="flex gap-[8px] h-[360px]">
11
- <div class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 0)">
12
- <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
13
- </div>
14
- <div class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 1)">
15
- <Image :src="photos[1].url" class="w-full h-full object-cover" width="auto" height="auto" />
16
- </div>
17
- </div>
18
-
19
- <!-- Personal state → Featured layout — 3+ รูป: layout เดิม -->
20
- <div v-else-if="layoutMode === 'featured'" class="flex gap-[8px] h-[360px]">
21
- <div v-if="photos[0]" class="w-[620px] shrink-0 rounded-[8px] overflow-hidden cursor-pointer"
22
- @click="openLightbox('photo', 0)">
23
- <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
24
- </div>
25
-
26
- <div v-if="photos[1]" class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 1)">
27
- <Image :src="photos[1].url" class="w-full h-full object-cover" width="auto" height="auto" />
28
- </div>
29
-
30
- <div class="w-[306px] shrink-0 flex flex-col gap-[8px]">
31
- <div v-if="photos[2]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
32
- @click="openLightbox('photo', 2)">
33
- <Image :src="photos[2].url" class="w-full h-full object-cover" width="auto" height="auto" />
34
- <div v-if="featuredPhoto2Overflow > 0"
35
- class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
36
- <Icon name="fa6-solid:image" :size="42" />
37
- <span class="font-body-large">รูปภาพ ({{ convertNumber(featuredPhoto2Overflow) }})</span>
38
- </div>
39
- </div>
40
-
41
- <div v-if="videos[0]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
42
- @click="openLightbox('video', 0)">
43
- <Image :src="videos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
44
- <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
45
- <Icon name="fa6-solid:video" :size="42" />
46
- <span class="font-body-large">วิดีโอ ({{ videos.length }})</span>
47
- </div>
48
- </div>
49
- <div v-else-if="photos[3]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
50
- @click="openLightbox('photo', 3)">
51
- <Image :src="photos[3].url" class="w-full h-full object-cover" width="auto" height="auto" />
52
- <div v-if="featuredPhoto3Overflow > 0"
53
- class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
54
- <Icon name="fa6-solid:image" :size="42" />
55
- <span class="font-body-large">รูปภาพ ({{ convertNumber(featuredPhoto3Overflow) }})</span>
56
- </div>
57
- </div>
58
- </div>
59
- </div>
60
-
61
- <!-- Office / Business state → Album layout -->
62
- <div v-else-if="layoutMode === 'album'" class="grid grid-cols-5 gap-[10px]">
63
- <!-- Row 1: photos[0..3] -->
64
- <div v-for="(photo, i) in galleryRow1" :key="`g1-${i}`"
65
- class="w-[132px] h-[132px] rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', i)">
66
- <Image :src="photo.url" class="w-full h-full object-cover" width="auto" height="auto" />
67
- </div>
68
-
69
- <!-- Row 1 col 5: image count overlay -->
70
- <div v-show="galleryCountPhoto != null"
71
- class="w-[132px] h-[132px] relative rounded-[8px] overflow-hidden cursor-pointer"
72
- @click="openLightbox('photo', 8)">
73
- <Image v-if="galleryCountPhoto" :src="galleryCountPhoto.url" class="w-full h-full object-cover" width="auto"
74
- height="auto" />
75
- <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
76
- <Icon name="fa6-solid:image" :size="35" />
77
- <span class="font-label-medium">รูปภาพ ({{ convertNumber(photos.length - 8) }})</span>
78
- </div>
79
- </div>
80
-
81
- <!-- Row 2: photos[4..7] -->
82
- <div v-for="(photo, i) in galleryRow2" :key="`g2-${i}`"
83
- class="w-[132px] h-[132px] rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 4 + i)">
84
- <Image :src="photo.url" class="w-full h-full object-cover" width="auto" height="auto" />
85
- </div>
86
-
87
- <!-- Row 2 col 5: video overlay -->
88
- <div v-if="videos[0]" class="w-[132px] h-[132px] relative rounded-[8px] overflow-hidden cursor-pointer"
89
- @click="openLightbox('video', 0)">
90
- <Image :src="videos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
91
- <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
92
- <Icon name="fa6-solid:video" :size="35" />
93
- <span class="font-label-medium">วิดีโอ ({{ videos.length }})</span>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- <!-- Lightbox modal -->
99
- <ModalMediaView v-model="isMediaOpen" :items="mediaItems" :title="props.title" :start-index="mediaStartIndex" />
2
+ <!-- Personal state → Featured layout — 1 รูป: w-full -->
3
+ <div v-if="layoutMode === 'featured' && photos.length === 1" class="h-[360px]">
4
+ <div class="w-full h-full rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 0)">
5
+ <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
6
+ </div>
7
+ </div>
8
+
9
+ <!-- Personal state → Featured layout — 2 รูป: แบ่งครึ่ง w-full -->
10
+ <div v-else-if="layoutMode === 'featured' && photos.length === 2" class="flex gap-[8px] h-[360px]">
11
+ <div class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 0)">
12
+ <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
13
+ </div>
14
+ <div class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 1)">
15
+ <Image :src="photos[1].url" class="w-full h-full object-cover" width="auto" height="auto" />
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Personal state → Featured layout — 3+ รูป: layout เดิม -->
20
+ <div v-else-if="layoutMode === 'featured'" class="flex gap-[8px] h-[360px]">
21
+ <div v-if="photos[0]" class="w-[620px] shrink-0 rounded-[8px] overflow-hidden cursor-pointer"
22
+ @click="openLightbox('photo', 0)">
23
+ <Image :src="photos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
24
+ </div>
25
+
26
+ <div v-if="photos[1]" class="flex-1 rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 1)">
27
+ <Image :src="photos[1].url" class="w-full h-full object-cover" width="auto" height="auto" />
28
+ </div>
29
+
30
+ <div class="w-[306px] shrink-0 flex flex-col gap-[8px]">
31
+ <div v-if="photos[2]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
32
+ @click="openLightbox('photo', 2)">
33
+ <Image :src="photos[2].url" class="w-full h-full object-cover" width="auto" height="auto" />
34
+ <div v-if="featuredPhoto2Overflow > 0"
35
+ class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
36
+ <Icon name="fa6-solid:image" :size="42" />
37
+ <span class="font-body-large">รูปภาพ ({{ convertNumber(featuredPhoto2Overflow) }})</span>
38
+ </div>
39
+ </div>
40
+
41
+ <div v-if="videos[0]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
42
+ @click="openLightbox('video', 0)">
43
+ <Image :src="videos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
44
+ <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
45
+ <Icon name="fa6-solid:video" :size="42" />
46
+ <span class="font-body-large">วิดีโอ ({{ videos.length }})</span>
47
+ </div>
48
+ </div>
49
+ <div v-else-if="photos[3]" class="flex-1 min-h-0 relative rounded-[8px] overflow-hidden cursor-pointer"
50
+ @click="openLightbox('photo', 3)">
51
+ <Image :src="photos[3].url" class="w-full h-full object-cover" width="auto" height="auto" />
52
+ <div v-if="featuredPhoto3Overflow > 0"
53
+ class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
54
+ <Icon name="fa6-solid:image" :size="42" />
55
+ <span class="font-body-large">รูปภาพ ({{ convertNumber(featuredPhoto3Overflow) }})</span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Office / Business state → Album layout -->
62
+ <div v-else-if="layoutMode === 'album'" class="grid grid-cols-5 gap-[10px]">
63
+ <!-- Row 1: photos[0..3] -->
64
+ <div v-for="(photo, i) in galleryRow1" :key="`g1-${i}`"
65
+ class="w-[132px] h-[132px] rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', i)">
66
+ <Image :src="photo.url" class="w-full h-full object-cover" width="auto" height="auto" />
67
+ </div>
68
+
69
+ <!-- Row 1 col 5: image count overlay -->
70
+ <div v-show="galleryCountPhoto != null"
71
+ class="w-[132px] h-[132px] relative rounded-[8px] overflow-hidden cursor-pointer"
72
+ @click="openLightbox('photo', 8)">
73
+ <Image v-if="galleryCountPhoto" :src="galleryCountPhoto.url" class="w-full h-full object-cover" width="auto"
74
+ height="auto" />
75
+ <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
76
+ <Icon name="fa6-solid:image" :size="35" />
77
+ <span class="font-label-medium">รูปภาพ ({{ convertNumber(photos.length - 8) }})</span>
78
+ </div>
79
+ </div>
80
+
81
+ <!-- Row 2: photos[4..7] -->
82
+ <div v-for="(photo, i) in galleryRow2" :key="`g2-${i}`"
83
+ class="w-[132px] h-[132px] rounded-[8px] overflow-hidden cursor-pointer" @click="openLightbox('photo', 4 + i)">
84
+ <Image :src="photo.url" class="w-full h-full object-cover" width="auto" height="auto" />
85
+ </div>
86
+
87
+ <!-- Row 2 col 5: video overlay -->
88
+ <div v-if="videos[0]" class="w-[132px] h-[132px] relative rounded-[8px] overflow-hidden cursor-pointer"
89
+ @click="openLightbox('video', 0)">
90
+ <Image :src="videos[0].url" class="w-full h-full object-cover" width="auto" height="auto" />
91
+ <div class="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white gap-[6px]">
92
+ <Icon name="fa6-solid:video" :size="35" />
93
+ <span class="font-label-medium">วิดีโอ ({{ videos.length }})</span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Lightbox modal -->
99
+ <ModalMediaView v-model="isMediaOpen" :items="mediaItems" :title="props.title" :start-index="mediaStartIndex" />
100
100
  </template>
101
101
 
102
102
  <script setup>