pukaad-ui-lib 1.454.0 → 1.458.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 (40) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/card/card-blog.d.vue.ts +1 -1
  3. package/dist/runtime/components/card/card-blog.vue.d.ts +1 -1
  4. package/dist/runtime/components/card/card-profile/card-profile-header.d.vue.ts +1 -1
  5. package/dist/runtime/components/card/card-profile/card-profile-header.vue.d.ts +1 -1
  6. package/dist/runtime/components/card/card-reaction.d.vue.ts +1 -1
  7. package/dist/runtime/components/card/card-reaction.vue.d.ts +1 -1
  8. package/dist/runtime/components/chat/chat-conversation-item.d.vue.ts +1 -1
  9. package/dist/runtime/components/chat/chat-conversation-item.vue.d.ts +1 -1
  10. package/dist/runtime/components/comment.d.vue.ts +3 -0
  11. package/dist/runtime/components/comment.vue +8 -6
  12. package/dist/runtime/components/comment.vue.d.ts +3 -0
  13. package/dist/runtime/components/display/display-image.vue +6 -1
  14. package/dist/runtime/components/display/display-video.d.vue.ts +1 -1
  15. package/dist/runtime/components/display/display-video.vue.d.ts +1 -1
  16. package/dist/runtime/components/drawer/drawer-post-content.vue +18 -0
  17. package/dist/runtime/components/input/input-comment.d.vue.ts +1 -1
  18. package/dist/runtime/components/input/input-comment.vue.d.ts +1 -1
  19. package/dist/runtime/components/input/input-rating.d.vue.ts +1 -1
  20. package/dist/runtime/components/input/input-rating.vue.d.ts +1 -1
  21. package/dist/runtime/components/input/input-search.d.vue.ts +1 -1
  22. package/dist/runtime/components/input/input-search.vue.d.ts +1 -1
  23. package/dist/runtime/components/input/input-select-place.vue +10 -3
  24. package/dist/runtime/components/input/input-text-field.d.vue.ts +1 -1
  25. package/dist/runtime/components/input/input-text-field.vue.d.ts +1 -1
  26. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  27. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  28. package/dist/runtime/components/preview/preview-content.d.vue.ts +8 -6
  29. package/dist/runtime/components/preview/preview-content.vue +209 -112
  30. package/dist/runtime/components/preview/preview-content.vue.d.ts +8 -6
  31. package/dist/runtime/components/preview/preview-place.d.vue.ts +0 -10
  32. package/dist/runtime/components/preview/preview-place.vue +7 -7
  33. package/dist/runtime/components/preview/preview-place.vue.d.ts +0 -10
  34. package/dist/runtime/components/ui/chart/ChartTooltipContent.d.vue.ts +1 -1
  35. package/dist/runtime/components/ui/chart/ChartTooltipContent.vue.d.ts +1 -1
  36. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.d.vue.ts +2 -2
  37. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.vue.d.ts +2 -2
  38. package/dist/runtime/components/ui/native-select/NativeSelectOption.d.vue.ts +2 -2
  39. package/dist/runtime/components/ui/native-select/NativeSelectOption.vue.d.ts +2 -2
  40. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.454.0",
4
+ "version": "1.458.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -38,8 +38,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
38
38
  "onBlog-updated"?: ((id: string, data: any) => any) | undefined;
39
39
  "onBlog-deleted"?: ((id: string) => any) | undefined;
40
40
  }>, {
41
- disabledMenu: boolean;
42
41
  isMyProfile: boolean;
42
+ disabledMenu: boolean;
43
43
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
44
44
  declare const _default: typeof __VLS_export;
45
45
  export default _default;
@@ -38,8 +38,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
38
38
  "onBlog-updated"?: ((id: string, data: any) => any) | undefined;
39
39
  "onBlog-deleted"?: ((id: string) => any) | undefined;
40
40
  }>, {
41
- disabledMenu: boolean;
42
41
  isMyProfile: boolean;
42
+ disabledMenu: boolean;
43
43
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
44
44
  declare const _default: typeof __VLS_export;
45
45
  export default _default;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<CardProfileAboutProps,
7
7
  onFollow?: ((isFollowing: boolean) => any) | undefined;
8
8
  }>, {
9
9
  type: "article" | "media" | "sale-info";
10
- preview: boolean;
11
10
  disabledPadding: boolean;
11
+ preview: boolean;
12
12
  optionMenuButtonType: "solid" | "text";
13
13
  actionsHidden: boolean;
14
14
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<CardProfileAboutProps,
7
7
  onFollow?: ((isFollowing: boolean) => any) | undefined;
8
8
  }>, {
9
9
  type: "article" | "media" | "sale-info";
10
- preview: boolean;
11
10
  disabledPadding: boolean;
11
+ preview: boolean;
12
12
  optionMenuButtonType: "solid" | "text";
13
13
  actionsHidden: boolean;
14
14
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -25,7 +25,6 @@ type __VLS_Props = {
25
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
26
26
  disabled: boolean;
27
27
  id: string;
28
- state: string;
29
28
  itemCount: Count;
30
29
  itemActive: Reacted;
31
30
  padding: string | number;
@@ -33,6 +32,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
33
32
  disabledDividerTop: boolean;
34
33
  disabledDividerBottom: boolean;
35
34
  iconSize: number | string;
35
+ state: string;
36
36
  loginPath: string;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
@@ -25,7 +25,6 @@ type __VLS_Props = {
25
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
26
26
  disabled: boolean;
27
27
  id: string;
28
- state: string;
29
28
  itemCount: Count;
30
29
  itemActive: Reacted;
31
30
  padding: string | number;
@@ -33,6 +32,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
33
32
  disabledDividerTop: boolean;
34
33
  disabledDividerBottom: boolean;
35
34
  iconSize: number | string;
35
+ state: string;
36
36
  loginPath: string;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
@@ -26,13 +26,13 @@ declare const __VLS_export: import("vue").DefineComponent<ChatConversationItemPr
26
26
  name: string;
27
27
  avatar: string | null;
28
28
  muted: boolean;
29
+ disabledMenu: boolean;
29
30
  preview: string;
30
31
  to: string;
31
32
  active: boolean;
32
33
  typing: boolean;
33
34
  unreadCount: number;
34
35
  pinned: boolean;
35
- disabledMenu: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
37
  declare const _default: typeof __VLS_export;
38
38
  export default _default;
@@ -26,13 +26,13 @@ declare const __VLS_export: import("vue").DefineComponent<ChatConversationItemPr
26
26
  name: string;
27
27
  avatar: string | null;
28
28
  muted: boolean;
29
+ disabledMenu: boolean;
29
30
  preview: string;
30
31
  to: string;
31
32
  active: boolean;
32
33
  typing: boolean;
33
34
  unreadCount: number;
34
35
  pinned: boolean;
35
- disabledMenu: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
37
  declare const _default: typeof __VLS_export;
38
38
  export default _default;
@@ -35,6 +35,8 @@ type __VLS_Props = {
35
35
  currentUser: User;
36
36
  fetchFn?: (page: number, pageSize: number) => Promise<CommentFetchResult>;
37
37
  pageSize?: number;
38
+ /** ดูอย่างเดียว (เช่นหน้าพรีวิว) — ไม่มีช่องพิมพ์/เมนู กดถูกใจ/ตอบกลับ/ดูโปรไฟล์ไม่ได้ แต่ยังกดดูการตอบกลับ/ดูเพิ่มเติมได้ */
39
+ readonly?: boolean;
38
40
  };
39
41
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
40
42
  addComment: (comment: CommentItem) => void;
@@ -67,6 +69,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
67
69
  comments: CommentItem[];
68
70
  commentCount: number;
69
71
  pageSize: number;
72
+ readonly: boolean;
70
73
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
71
74
  declare const _default: typeof __VLS_export;
72
75
  export default _default;
@@ -8,7 +8,7 @@
8
8
  </div>
9
9
 
10
10
  <!-- กล่องคอมเมนต์หลัก — ปุ่มโผล่ตอนคลิกเข้าช่องพิมพ์ (actions-on-focus) -->
11
- <InputComment ref="mainInputRef" v-model="mainCommentInput" :avatar="currentUser.avatar"
11
+ <InputComment v-if="!props.readonly" ref="mainInputRef" v-model="mainCommentInput" :avatar="currentUser.avatar"
12
12
  placeholder="เพิ่มความคิดเห็น" actions-on-focus @submit="onSendComment" />
13
13
  <div v-for="cmt in resolvedComments" :key="cmt.id" class="flex flex-col gap-[16px] w-full">
14
14
  <div class="flex flex-col gap-[4px] w-full">
@@ -40,7 +40,7 @@
40
40
  </template>
41
41
  </div>
42
42
  </div>
43
- <div class="self-center transition-opacity" :class="
43
+ <div v-if="!props.readonly" class="self-center transition-opacity" :class="
44
44
  isMenuVisible(cmt.id) ? 'opacity-100' : 'opacity-0 pointer-events-none'
45
45
  ">
46
46
  <PickerOptionMenu horizontal variant="ghost" circle size="icon-sm" icon-size="20"
@@ -49,7 +49,7 @@
49
49
  </div>
50
50
  </template>
51
51
  </div>
52
- <div class="pl-[38px] flex gap-[16px] text-gray">
52
+ <div class="pl-[38px] flex gap-[16px] text-gray" :inert="props.readonly || void 0">
53
53
  <div class="font-body-large">
54
54
  {{ convertDateToRelativeText(cmt.created_at) }}
55
55
  </div>
@@ -119,7 +119,7 @@
119
119
  @click="toggleContentExpand(`reply-${reply.id}`)">ดูเพิ่มเติม</span></template>
120
120
  </div>
121
121
  </div>
122
- <div class="self-center transition-opacity" :class="
122
+ <div v-if="!props.readonly" class="self-center transition-opacity" :class="
123
123
  isMenuVisible(reply.id) ? 'opacity-100' : 'opacity-0 pointer-events-none'
124
124
  ">
125
125
  <PickerOptionMenu horizontal variant="ghost" circle size="icon-xs" icon-size="20"
@@ -129,7 +129,7 @@
129
129
  </div>
130
130
  </template>
131
131
  </div>
132
- <div class="pl-[38px] flex gap-[16px] text-gray">
132
+ <div class="pl-[38px] flex gap-[16px] text-gray" :inert="props.readonly || void 0">
133
133
  <div class="font-body-large">
134
134
  {{ convertDateToRelativeText(reply.created_at) }}
135
135
  </div>
@@ -177,7 +177,8 @@ const props = defineProps({
177
177
  commentCount: { type: Number, required: false, default: 0 },
178
178
  currentUser: { type: Object, required: true },
179
179
  fetchFn: { type: Function, required: false },
180
- pageSize: { type: Number, required: false, default: 20 }
180
+ pageSize: { type: Number, required: false, default: 20 },
181
+ readonly: { type: Boolean, required: false, default: false }
181
182
  });
182
183
  const emit = defineEmits(["submit", "reply", "like-comment", "like-reply", "edit-comment", "edit-reply", "delete-comment", "delete-reply", "report-comment", "report-reply"]);
183
184
  const internalComments = ref([]);
@@ -343,6 +344,7 @@ const onToggleLikeReplyComment = (reply) => {
343
344
  emit("like-reply", reply);
344
345
  };
345
346
  const onViewProfileComment = (id) => {
347
+ if (props.readonly) return;
346
348
  if (id) router.push(`/@${id}`);
347
349
  };
348
350
  const onEditClick = (id, content, replyTo) => {
@@ -35,6 +35,8 @@ type __VLS_Props = {
35
35
  currentUser: User;
36
36
  fetchFn?: (page: number, pageSize: number) => Promise<CommentFetchResult>;
37
37
  pageSize?: number;
38
+ /** ดูอย่างเดียว (เช่นหน้าพรีวิว) — ไม่มีช่องพิมพ์/เมนู กดถูกใจ/ตอบกลับ/ดูโปรไฟล์ไม่ได้ แต่ยังกดดูการตอบกลับ/ดูเพิ่มเติมได้ */
39
+ readonly?: boolean;
38
40
  };
39
41
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
40
42
  addComment: (comment: CommentItem) => void;
@@ -67,6 +69,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
67
69
  comments: CommentItem[];
68
70
  commentCount: number;
69
71
  pageSize: number;
72
+ readonly: boolean;
70
73
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
71
74
  declare const _default: typeof __VLS_export;
72
75
  export default _default;
@@ -19,7 +19,7 @@
19
19
  <Image
20
20
  :src="getSrc((props.items ?? [])[0])"
21
21
  class="rounded-[8px]"
22
- width="720"
22
+ :width="widthMainImg"
23
23
  height="auto"
24
24
  />
25
25
  </div>
@@ -66,6 +66,11 @@ const heightBox = computed(() => {
66
66
  if (length > 1 && length <= 4) return 422;
67
67
  return 480;
68
68
  });
69
+ const widthMainImg = computed(() => {
70
+ const length = props.items?.length ?? 0;
71
+ if (length === 3 || length === 4) return 633;
72
+ return 720;
73
+ });
69
74
  const widthSubImg = computed(() => {
70
75
  const length = props.items?.length ?? 0;
71
76
  if (length === 0) return 0;
@@ -102,9 +102,9 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
102
102
  loop: boolean;
103
103
  controls: boolean;
104
104
  preload: "none" | "metadata" | "auto";
105
- playsinline: boolean;
106
105
  sources: DisplayVideoSource[];
107
106
  tracks: DisplayVideoTrack[];
107
+ playsinline: boolean;
108
108
  disablePictureInPicture: boolean;
109
109
  disableRemotePlayback: boolean;
110
110
  volume: number;
@@ -102,9 +102,9 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
102
102
  loop: boolean;
103
103
  controls: boolean;
104
104
  preload: "none" | "metadata" | "auto";
105
- playsinline: boolean;
106
105
  sources: DisplayVideoSource[];
107
106
  tracks: DisplayVideoTrack[];
107
+ playsinline: boolean;
108
108
  disablePictureInPicture: boolean;
109
109
  disableRemotePlayback: boolean;
110
110
  volume: number;
@@ -74,6 +74,9 @@
74
74
  </template>
75
75
  </InputFile>
76
76
  </div>
77
+ <div v-if="needsManualPoster" class="font-body-small text-destructive">
78
+ ไม่สามารถสร้างหน้าปกจากวิดีโอได้ กรุณาอัปโหลดภาพหน้าปก
79
+ </div>
77
80
  <div
78
81
  v-show="hasCoverText && !hasCoverFile"
79
82
  class="flex gap-[8px] py-[4px] px-[12px]"
@@ -190,6 +193,7 @@ const coverText = ref("");
190
193
  const coverFiles = ref([]);
191
194
  const posterFiles = ref([]);
192
195
  const generatedPoster = ref(null);
196
+ const posterGenerationFailed = ref(false);
193
197
  const description = ref("");
194
198
  const fileColumns = ref(false);
195
199
  const selectedPlaces = ref([]);
@@ -245,6 +249,7 @@ watch(
245
249
  ([videoFile, manualPoster]) => {
246
250
  const generation = ++thumbnailGeneration;
247
251
  clearGeneratedPoster();
252
+ posterGenerationFailed.value = false;
248
253
  if (manualPoster || typeof File === "undefined" || !(videoFile instanceof File) || !videoFile.type.startsWith("video/")) {
249
254
  thumbnailPromise = Promise.resolve();
250
255
  return;
@@ -258,6 +263,9 @@ watch(
258
263
  mediaType: "image"
259
264
  };
260
265
  }).catch(() => {
266
+ if (generation === thumbnailGeneration) {
267
+ posterGenerationFailed.value = true;
268
+ }
261
269
  });
262
270
  },
263
271
  { immediate: true }
@@ -269,6 +277,9 @@ onBeforeUnmount(() => {
269
277
  const effectivePoster = computed(
270
278
  () => posterFiles.value[0] ?? generatedPoster.value
271
279
  );
280
+ const needsManualPoster = computed(
281
+ () => coverFileType.value === "video" && !posterFiles.value[0] && (posterGenerationFailed.value || typeof File === "undefined" || !(coverFiles.value[0]?.file instanceof File))
282
+ );
272
283
  const previewItem = computed(() => {
273
284
  if (hasCoverFile.value) {
274
285
  const file = coverFiles.value[0];
@@ -449,6 +460,13 @@ const submitContent = async (status) => {
449
460
  if (contentType === "video" && !posterFiles.value[0]) {
450
461
  await thumbnailPromise;
451
462
  }
463
+ if (contentType === "video" && status === "PUBLISHED" && !effectivePoster.value) {
464
+ posterGenerationFailed.value = true;
465
+ $toast?.error?.(
466
+ "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E2D\u0E31\u0E1B\u0E42\u0E2B\u0E25\u0E14\u0E20\u0E32\u0E1E\u0E2B\u0E19\u0E49\u0E32\u0E1B\u0E01\u0E02\u0E2D\u0E07\u0E27\u0E34\u0E14\u0E35\u0E42\u0E2D"
467
+ );
468
+ return;
469
+ }
452
470
  const [uploadedMedia, uploadedPoster] = await Promise.all([
453
471
  uploadFiles(coverFiles.value, "content-media"),
454
472
  contentType === "video" && effectivePoster.value ? uploadFiles([effectivePoster.value], "content-media") : Promise.resolve([])
@@ -46,8 +46,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  }>, {
47
47
  loading: boolean;
48
48
  disabled: boolean;
49
- cancelText: string;
50
49
  placeholder: string;
50
+ cancelText: string;
51
51
  autofocus: boolean;
52
52
  disabledAvatar: boolean;
53
53
  submitText: string;
@@ -46,8 +46,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  }>, {
47
47
  loading: boolean;
48
48
  disabled: boolean;
49
- cancelText: string;
50
49
  placeholder: string;
50
+ cancelText: string;
51
51
  autofocus: boolean;
52
52
  disabledAvatar: boolean;
53
53
  submitText: string;
@@ -10,10 +10,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
10
  "onUpdate:modelValue"?: ((value: number) => any) | undefined;
11
11
  }>, {
12
12
  size: number;
13
+ readonly: boolean;
13
14
  borderColor: string;
14
15
  modelValue: number;
15
16
  backgroundColor: string;
16
- readonly: boolean;
17
17
  ratingStep: number;
18
18
  borderSize: number;
19
19
  activeColor: string;
@@ -10,10 +10,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
10
  "onUpdate:modelValue"?: ((value: number) => any) | undefined;
11
11
  }>, {
12
12
  size: number;
13
+ readonly: boolean;
13
14
  borderColor: string;
14
15
  modelValue: number;
15
16
  backgroundColor: string;
16
- readonly: boolean;
17
17
  ratingStep: number;
18
18
  borderSize: number;
19
19
  activeColor: string;
@@ -33,11 +33,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
33
33
  type: import("./input-text-field.vue.js").InputTextFieldType;
34
34
  icon: string;
35
35
  disabled: boolean;
36
+ readonly: boolean;
36
37
  id: string;
37
38
  name: string;
38
39
  required: boolean;
39
40
  placeholder: string;
40
- readonly: boolean;
41
41
  disabledErrorMessage: boolean;
42
42
  iconPosition: import("#pukaad-ui/types/components/input/input-search").InputSearchIconPosition;
43
43
  disabledIcon: boolean;
@@ -33,11 +33,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
33
33
  type: import("./input-text-field.vue.js").InputTextFieldType;
34
34
  icon: string;
35
35
  disabled: boolean;
36
+ readonly: boolean;
36
37
  id: string;
37
38
  name: string;
38
39
  required: boolean;
39
40
  placeholder: string;
40
- readonly: boolean;
41
41
  disabledErrorMessage: boolean;
42
42
  iconPosition: import("#pukaad-ui/types/components/input/input-search").InputSearchIconPosition;
43
43
  disabledIcon: boolean;
@@ -1,10 +1,16 @@
1
1
  <template>
2
- <div class="flex flex-col gap-[4px] w-full">
2
+ <div class="relative flex flex-col gap-[4px] w-full">
3
3
  <div class="font-body-large-prominent text-gray">
4
4
  {{ props.label }}
5
5
  <span v-if="props.required" class="text-destructive">*</span>
6
6
  </div>
7
- <div v-show="selectedItems.length === 0">
7
+ <!-- input เป็น anchor ของ popover — ห้ามซ่อนด้วย display:none (v-show) ตอนมีการเลือก ไม่งั้น anchor ไม่มี
8
+ ตำแหน่ง popover ลอยไปมุมซ้ายบนจอ ใช้ invisible + วางทับขอบล่างการ์ดแทน ให้เปิดจากการ์ดแล้วโผล่ใต้การ์ดพอดี -->
9
+ <div
10
+ :class="
11
+ selectedItems.length > 0 ? 'invisible absolute inset-x-0 bottom-0' : ''
12
+ "
13
+ >
8
14
  <InputCombobox
9
15
  ref="comboboxRef"
10
16
  name="input_select_place"
@@ -96,7 +102,7 @@
96
102
  </InputCombobox>
97
103
  </div>
98
104
 
99
- <!-- แสดงแทน input ตอนมีการเลือกอยู่ (input จริงยังอยู่ใน DOM แค่ v-show ซ่อนไว้ ไม่ได้ unmount — กันเสีย
105
+ <!-- แสดงแทน input ตอนมีการเลือกอยู่ (input จริงยังอยู่ใน DOM แค่ซ่อนแบบ invisible ไว้ ไม่ได้ unmount — กันเสีย
100
106
  state/ref ของ InputCombobox) คลิกที่การ์ดเปิด popover กลับมาเพื่อเลือกเพิ่ม/แก้ไขได้ ผ่าน openPopover()
101
107
  ที่ expose ไว้ (ไม่ต้องแตะ template ของ input-combobox.vue เลย ปลอดภัยกับ component กลาง) -->
102
108
  <div
@@ -262,6 +268,7 @@ const toggleSelect = (id) => {
262
268
  }
263
269
  if (item) selectedCache.value.set(id, item);
264
270
  selectedIds.value = [...selectedIds.value, id];
271
+ comboboxRef.value?.closePopover();
265
272
  }
266
273
  };
267
274
  const selectedItems = computed(
@@ -56,11 +56,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
56
56
  }>, {
57
57
  type: InputTextFieldType;
58
58
  disabled: boolean;
59
+ readonly: boolean;
59
60
  id: string;
60
61
  name: string;
61
62
  required: boolean;
62
63
  limit: number;
63
- readonly: boolean;
64
64
  disabledErrorMessage: boolean;
65
65
  disabledBorder: boolean;
66
66
  showCounter: boolean;
@@ -56,11 +56,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
56
56
  }>, {
57
57
  type: InputTextFieldType;
58
58
  disabled: boolean;
59
+ readonly: boolean;
59
60
  id: string;
60
61
  name: string;
61
62
  required: boolean;
62
63
  limit: number;
63
- readonly: boolean;
64
64
  disabledErrorMessage: boolean;
65
65
  disabledBorder: boolean;
66
66
  showCounter: boolean;
@@ -40,6 +40,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
40
40
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
41
41
  }>, {
42
42
  disabled: boolean;
43
+ readonly: boolean;
43
44
  id: string;
44
45
  name: string;
45
46
  required: boolean;
@@ -48,7 +49,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
48
49
  fullHeight: boolean;
49
50
  rows: number;
50
51
  limit: number;
51
- readonly: boolean;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
54
54
  showCounter: boolean;
@@ -40,6 +40,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
40
40
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
41
41
  }>, {
42
42
  disabled: boolean;
43
+ readonly: boolean;
43
44
  id: string;
44
45
  name: string;
45
46
  required: boolean;
@@ -48,7 +49,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
48
49
  fullHeight: boolean;
49
50
  rows: number;
50
51
  limit: number;
51
- readonly: boolean;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
54
54
  showCounter: boolean;
@@ -1,5 +1,11 @@
1
1
  import type { CardReferItem } from "#pukaad-ui/runtime/components/card/card-refer.vue";
2
- export type PreviewContentState = "personal" | "business";
2
+ export type PreviewContentState = "personal" | "office" | "business";
3
+ export interface PreviewContentMedia {
4
+ id?: string | number;
5
+ media_type: string;
6
+ url: string;
7
+ description?: string;
8
+ }
3
9
  export interface PreviewContentData {
4
10
  id?: string;
5
11
  content_type?: "post_it" | "image" | "video";
@@ -11,11 +17,7 @@ export interface PreviewContentData {
11
17
  cover_color?: string;
12
18
  video_poster_url?: string;
13
19
  description?: string;
14
- media?: {
15
- media_type: string;
16
- url: string;
17
- description?: string;
18
- }[];
20
+ media?: PreviewContentMedia[];
19
21
  tags?: {
20
22
  id: string;
21
23
  name: string;
@@ -1,9 +1,8 @@
1
1
  <template>
2
2
  <!-- Figma: Preview - Video (2743:66169) — post_it / image ใช้โครงเดียวกัน -->
3
- <div class="min-h-screen w-full bg-white">
3
+ <div class="h-screen w-full overflow-hidden bg-white flex flex-col">
4
4
  <!-- Figma: Navbar Preview (new tab) — Property 1=Admin -->
5
- <!-- ใช้ scroll ของหน้า (ไม่ใช่ scroll ใน div) เพราะ Comment โหลดหน้าถัดไปจาก window scroll -->
6
- <div class="sticky top-0 z-overlay w-full bg-cloud">
5
+ <div class="shrink-0 w-full bg-cloud">
7
6
  <div class="max-w-[1280px] mx-auto flex justify-end p-[16px]">
8
7
  <Button variant="outline" class="rounded-[6px]" @click="emit('exit')">
9
8
  ออกจากพรีวิว
@@ -11,124 +10,142 @@
11
10
  </div>
12
11
  </div>
13
12
 
14
- <!-- Figma: เนื้อหาเริ่มห่างจากแถบ 22px (ช่องว่าง 6 + padding 16) -->
15
- <div class="flex justify-center px-[16px] pt-[22px] pb-[16px]">
16
- <!-- กว้าง 844 เท่าวิดีโอ — DisplayVideo ยืดตามความกว้างกล่องนี้ ปุ่มเล่นจะได้อยู่กลางวิดีโอ -->
17
- <div class="w-[844px] flex flex-col gap-[16px]">
18
- <Label v-if="labelItems.length" :items="labelItems" />
13
+ <!-- เลื่อนในกล่องนี้ ไม่พึ่ง scroll ของหน้า (บางแอปล็อก html/body ไว้) -->
14
+ <div class="flex-1 overflow-y-auto">
15
+ <!-- Figma: เนื้อหาเริ่มห่างจากแถบ 22px (ช่องว่าง 6 + padding 16) -->
16
+ <div class="flex justify-center px-[16px] pt-[22px] pb-[16px]">
17
+ <!-- กว้าง 844 เท่าวิดีโอ — DisplayVideo ยืดตามความกว้างกล่องนี้ ปุ่มเล่นจะได้อยู่กลางวิดีโอ -->
18
+ <div class="w-[844px] flex flex-col gap-[16px]">
19
+ <Label v-if="labelItems.length" :items="labelItems" />
19
20
 
20
- <!-- Post-it -->
21
- <div
22
- v-if="content.content_type === 'post_it'"
23
- class="flex gap-[16px] py-[16px] items-center"
24
- >
21
+ <!-- Post-it -->
25
22
  <div
26
- class="w-[6px] h-full shrink-0"
27
- :style="{ backgroundColor: content.cover_color }"
28
- />
29
- <div class="font-headline-large-prominent min-w-0 break-words">
30
- {{ content.cover_text }}
23
+ v-if="contentData.content_type === 'post_it'"
24
+ class="flex gap-[16px] py-[16px] items-center"
25
+ >
26
+ <div
27
+ class="w-[6px] h-full shrink-0"
28
+ :style="{ backgroundColor: contentData.cover_color }"
29
+ />
30
+ <div class="font-headline-large-prominent min-w-0 break-words">
31
+ {{ contentData.cover_text }}
32
+ </div>
31
33
  </div>
32
- </div>
33
- <!-- Image -->
34
- <DisplayImage
35
- v-else-if="content.content_type === 'image'"
36
- :items="content.media"
37
- item-src="url"
38
- />
39
- <!-- Video -->
40
- <DisplayVideo
41
- v-else-if="content.content_type === 'video'"
42
- :src="content.media?.[0]?.url"
43
- :poster="content.video_poster_url"
44
- play-button
45
- width="844"
46
- height="475"
47
- preload="metadata"
48
- class="bg-black"
49
- />
34
+ <!-- Image -->
35
+ <DisplayImage
36
+ v-else-if="contentData.content_type === 'image'"
37
+ :items="contentData.media"
38
+ item-src="url"
39
+ @select="openImage"
40
+ />
41
+ <!-- Video -->
42
+ <DisplayVideo
43
+ v-else-if="contentData.content_type === 'video'"
44
+ :src="contentData.media?.[0]?.url"
45
+ :poster="contentData.video_poster_url"
46
+ play-button
47
+ width="844"
48
+ height="475"
49
+ preload="metadata"
50
+ class="bg-black"
51
+ />
50
52
 
51
- <CardRefer v-if="content.mentions?.[0]" :item="content.mentions[0]" />
53
+ <CardRefer
54
+ v-if="contentData.mentions?.[0]"
55
+ :item="contentData.mentions[0]"
56
+ />
52
57
 
53
- <!-- Figma: Announce Description -->
54
- <div
55
- v-if="content.author"
56
- class="flex items-center justify-between gap-[16px]"
57
- >
58
- <div class="flex items-center gap-[8px] min-w-0">
59
- <Avatar :src="content.author.avatar ?? ''" :size="40" />
60
- <div class="flex flex-col gap-[4px] min-w-0">
61
- <div class="font-body-large-prominent truncate">
62
- {{ content.author.name }}
63
- </div>
64
- <div class="flex gap-[8px] font-body-large">
65
- <div class="flex gap-[4px]">
66
- <span class="text-gray">ผู้ติดตาม</span>
67
- {{ convertNumber(content.author.follower_count ?? 0) }}
68
- </div>
69
- <div class="flex gap-[4px]">
70
- <span class="text-gray">กำลังเผยแพร่</span>
71
- {{ convertNumber(content.author.published_count ?? 0) }}
58
+ <!-- Figma: Announce Description -->
59
+ <div
60
+ v-if="contentData.author"
61
+ class="flex items-center justify-between gap-[16px]"
62
+ >
63
+ <div class="flex items-center gap-[8px] min-w-0">
64
+ <Avatar :src="contentData.author.avatar ?? ''" :size="40" />
65
+ <div class="flex flex-col gap-[4px] min-w-0">
66
+ <div class="font-body-large-prominent truncate">
67
+ {{ contentData.author.name }}
72
68
  </div>
73
- <div class="flex gap-[4px]">
74
- <span class="text-gray">อัตราตอบแชท</span>
75
- {{ content.author.response_rate ?? 0 }}%
69
+ <div
70
+ v-if="authorStats.length"
71
+ class="flex gap-[8px] font-body-large"
72
+ >
73
+ <div
74
+ v-for="stat in authorStats"
75
+ :key="stat.label"
76
+ class="flex gap-[4px]"
77
+ >
78
+ <span class="text-gray">{{ stat.label }}</span>
79
+ {{ stat.value }}
80
+ </div>
76
81
  </div>
77
82
  </div>
78
83
  </div>
84
+ <!-- พรีวิว = ดูหน้าตาได้แต่กดไม่ได้ -->
85
+ <Button circle size="icon-sm" class="shrink-0" inert>
86
+ <Icon name="lucide:ellipsis-vertical" />
87
+ </Button>
79
88
  </div>
80
- <!-- พรีวิว = ดูหน้าตาได้แต่กดไม่ได้ -->
81
- <Button circle size="icon-sm" class="shrink-0" inert>
82
- <Icon name="lucide:ellipsis-vertical" />
83
- </Button>
84
- </div>
85
89
 
86
- <div
87
- v-if="content.description"
88
- class="font-body-large whitespace-pre-wrap"
89
- >
90
- {{ content.description }}
91
- </div>
90
+ <div
91
+ v-if="contentData.description"
92
+ class="font-body-large whitespace-pre-wrap"
93
+ >
94
+ {{ contentData.description }}
95
+ </div>
92
96
 
93
- <div v-if="place" class="flex gap-[8px] font-body-large">
94
- <span class="text-gray">อยู่ที่</span>
95
- <span class="text-primary">{{ place.name }}</span>
96
- </div>
97
+ <div v-if="place" class="flex gap-[8px] font-body-large">
98
+ <span class="text-gray">อยู่ที่</span>
99
+ <span class="text-primary">{{ place.name }}</span>
100
+ </div>
97
101
 
98
- <div v-if="content.tags?.length" class="flex flex-wrap gap-[8px]">
99
- <Chip v-for="tag in content.tags" :key="tag.id" color="bright">
100
- {{ tag.name }}
101
- </Chip>
102
- </div>
102
+ <div v-if="contentData.tags?.length" class="flex flex-wrap gap-[8px]">
103
+ <Chip v-for="tag in contentData.tags" :key="tag.id" color="bright">
104
+ {{ tag.name }}
105
+ </Chip>
106
+ </div>
103
107
 
104
- <CardReaction
105
- :item-count="{
106
- liked: content.like_count ?? 0,
107
- bookmarked: content.bookmark_count ?? 0,
108
- shared: content.share_count ?? 0
108
+ <CardReaction
109
+ :item-count="{
110
+ liked: contentData.like_count ?? 0,
111
+ bookmarked: contentData.bookmark_count ?? 0,
112
+ shared: contentData.share_count ?? 0
109
113
  }"
110
- disabled
111
- />
114
+ disabled
115
+ />
112
116
 
113
- <!-- Figma: Spotlight -->
114
- <div v-if="recommendedContents.length" inert>
115
- <CardContentRecommend :items="recommendedContents" />
116
- </div>
117
+ <!-- Figma: Spotlight -->
118
+ <div v-if="recommendedContents.length" inert>
119
+ <CardContentRecommend :items="recommendedContents" />
120
+ </div>
117
121
 
118
- <div inert>
122
+ <!-- ส่งรายการเข้าไปเอง (ไม่ใช้ fetchFn) เพราะ fetchFn โหลดหน้าถัดไปจาก window scroll -->
119
123
  <Comment
120
- :key="String(props.contentId)"
121
- :fetch-fn="commentFetchFn"
124
+ :comments="comments"
125
+ :comment-count="commentTotal"
122
126
  :current-user="previewUser"
127
+ readonly
123
128
  />
129
+
130
+ <div ref="sentinel" class="h-[1px]" />
131
+ <div v-if="isLoadingComments" class="flex justify-center py-[16px]">
132
+ <Icon
133
+ name="lucide:loader-circle"
134
+ :size="40"
135
+ class="animate-spin text-cloud"
136
+ />
137
+ </div>
124
138
  </div>
125
139
  </div>
126
140
  </div>
141
+
142
+ <ModalDisplayImage v-model="isImageOpen" :src="selectedImage" />
127
143
  </div>
128
144
  </template>
129
145
 
130
146
  <script setup>
131
- import { computed, onMounted, ref, watch } from "vue";
147
+ import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
148
+ import { useElementVisibility } from "@vueuse/core";
132
149
  import { useApi } from "#pukaad-ui/runtime/composables/useApi";
133
150
  import { useConvert } from "#pukaad-ui/runtime/composables/useConvert";
134
151
  import { mapContentToCard } from "#pukaad-ui/runtime/composables/useContentCard";
@@ -141,24 +158,75 @@ const api = useApi();
141
158
  const { convertNumber } = useConvert();
142
159
  const endpointMap = {
143
160
  personal: "/contents",
161
+ // endpoint เดียวกับที่หน้าจัดการป้ายกำกับเนื้อหาของ office ใช้
162
+ office: "/office/content-labels/contents",
144
163
  business: "/business/contents"
145
164
  };
146
- const content = ref({});
165
+ const endpoint = computed(() => endpointMap[props.state]);
166
+ const contentData = ref({});
147
167
  const recommendedContents = ref([]);
168
+ const comments = ref([]);
169
+ const commentTotal = ref(0);
170
+ const commentPage = ref(1);
171
+ const commentTotalPages = ref(1);
172
+ const commentPageSize = 20;
173
+ const isLoadingComments = ref(false);
174
+ const hasMoreComments = computed(
175
+ () => commentPage.value < commentTotalPages.value
176
+ );
177
+ const isImageOpen = ref(false);
178
+ const selectedImage = ref("");
148
179
  const previewUser = { id: "", name: "", avatar: "" };
149
180
  const labelItems = computed(
150
- () => (content.value.labels ?? []).map((label) => ({
181
+ () => (contentData.value.labels ?? []).map((label) => ({
151
182
  id: label.id,
152
183
  label: label.name
153
184
  }))
154
185
  );
155
- const place = computed(() => content.value.places?.[0]);
156
- const fetchContent = async () => {
186
+ const place = computed(() => contentData.value.places?.[0]);
187
+ const authorStats = computed(() => {
188
+ const author = contentData.value.author;
189
+ const stats = [];
190
+ if (author?.follower_count !== void 0) {
191
+ stats.push({
192
+ label: "\u0E1C\u0E39\u0E49\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21",
193
+ value: convertNumber(author.follower_count)
194
+ });
195
+ }
196
+ if (author?.published_count !== void 0) {
197
+ stats.push({
198
+ label: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E40\u0E1C\u0E22\u0E41\u0E1E\u0E23\u0E48",
199
+ value: convertNumber(author.published_count)
200
+ });
201
+ }
202
+ if (author?.response_rate !== void 0) {
203
+ stats.push({ label: "\u0E2D\u0E31\u0E15\u0E23\u0E32\u0E15\u0E2D\u0E1A\u0E41\u0E0A\u0E17", value: `${author.response_rate}%` });
204
+ }
205
+ return stats;
206
+ });
207
+ const fromOfficeContent = (data) => {
208
+ if (!data?.content) return {};
209
+ const { profile_name, profile_avatar, ...rest } = data.content;
210
+ return {
211
+ ...rest,
212
+ media: data.media,
213
+ author: {
214
+ id: "",
215
+ name: profile_name ?? "",
216
+ avatar: profile_avatar ?? null
217
+ }
218
+ };
219
+ };
220
+ const fetchContentData = async () => {
221
+ const url = `${endpoint.value}/${props.contentId}`;
157
222
  try {
158
- const res = await api(
159
- `${endpointMap[props.state]}/${props.contentId}`
160
- );
161
- content.value = res.data ?? {};
223
+ if (props.state === "office") {
224
+ const res = await api(url);
225
+ contentData.value = fromOfficeContent(res.data);
226
+ } else {
227
+ const res = await api(url);
228
+ contentData.value = res.data ?? {};
229
+ }
162
230
  } catch (err) {
163
231
  console.error(err);
164
232
  }
@@ -200,20 +268,49 @@ const toCommentItem = (raw) => ({
200
268
  is_liked: r.is_liked ?? false
201
269
  }))
202
270
  });
203
- const commentFetchFn = async (page, pageSize) => {
204
- const res = await api(`/contents/${props.contentId}/comments`, {
205
- query: { limit: pageSize, offset: (page - 1) * pageSize }
206
- });
207
- return {
208
- data: (res.data ?? []).map(toCommentItem),
209
- totalComments: res.meta?.total_comments ?? 0,
210
- meta: { total_pages: res.meta?.total_pages ?? 1 }
211
- };
271
+ let commentRequestId = 0;
272
+ const fetchComments = async (page = 1) => {
273
+ if (page > 1 && isLoadingComments.value) return;
274
+ const requestId = ++commentRequestId;
275
+ isLoadingComments.value = true;
276
+ try {
277
+ const res = await api(`/contents/${props.contentId}/comments`, {
278
+ query: {
279
+ limit: commentPageSize,
280
+ offset: (page - 1) * commentPageSize
281
+ }
282
+ });
283
+ if (requestId !== commentRequestId) return;
284
+ const items = (res.data ?? []).map(toCommentItem);
285
+ comments.value = page === 1 ? items : [...comments.value, ...items];
286
+ commentTotal.value = res.meta?.total_comments ?? 0;
287
+ commentTotalPages.value = res.meta?.total_pages ?? 1;
288
+ commentPage.value = page;
289
+ } catch (err) {
290
+ console.error(err);
291
+ if (requestId === commentRequestId && page === 1) comments.value = [];
292
+ } finally {
293
+ if (requestId === commentRequestId) isLoadingComments.value = false;
294
+ }
295
+ };
296
+ const openImage = (item) => {
297
+ if (!item?.url) return;
298
+ selectedImage.value = item.url;
299
+ isImageOpen.value = true;
212
300
  };
213
301
  const loadAll = () => {
214
- fetchContent();
302
+ fetchContentData();
215
303
  fetchRecommendations();
304
+ fetchComments(1);
216
305
  };
306
+ const sentinel = useTemplateRef("sentinel");
307
+ const isSentinelVisible = useElementVisibility(sentinel);
308
+ watch(
309
+ [isSentinelVisible, hasMoreComments, isLoadingComments],
310
+ ([visible, more, loading]) => {
311
+ if (visible && more && !loading) fetchComments(commentPage.value + 1);
312
+ }
313
+ );
217
314
  watch(() => [props.contentId, props.state], loadAll);
218
315
  onMounted(loadAll);
219
316
  </script>
@@ -1,5 +1,11 @@
1
1
  import type { CardReferItem } from "#pukaad-ui/runtime/components/card/card-refer.vue";
2
- export type PreviewContentState = "personal" | "business";
2
+ export type PreviewContentState = "personal" | "office" | "business";
3
+ export interface PreviewContentMedia {
4
+ id?: string | number;
5
+ media_type: string;
6
+ url: string;
7
+ description?: string;
8
+ }
3
9
  export interface PreviewContentData {
4
10
  id?: string;
5
11
  content_type?: "post_it" | "image" | "video";
@@ -11,11 +17,7 @@ export interface PreviewContentData {
11
17
  cover_color?: string;
12
18
  video_poster_url?: string;
13
19
  description?: string;
14
- media?: {
15
- media_type: string;
16
- url: string;
17
- description?: string;
18
- }[];
20
+ media?: PreviewContentMedia[];
19
21
  tags?: {
20
22
  id: string;
21
23
  name: string;
@@ -31,19 +31,10 @@ export interface PreviewPlaceData {
31
31
  opening_hours?: OpeningHour[];
32
32
  special_days?: SpecialDay[];
33
33
  }
34
- export interface PreviewPlaceRecommendItem {
35
- id: string | number;
36
- title: string;
37
- image: string;
38
- rating: number;
39
- review_count: number;
40
- }
41
34
  type __VLS_Props = {
42
35
  placeId: string | number;
43
36
  /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
37
  state?: PreviewPlaceState;
45
- /** "ที่ไหนน่าสนใจ" — ยังไม่มี API ให้แอปส่งมาเอง ไม่ส่ง = ซ่อนการ์ด */
46
- recommendedPlaces?: PreviewPlaceRecommendItem[];
47
38
  };
48
39
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
49
40
  exit: () => any;
@@ -51,7 +42,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
51
42
  onExit?: (() => any) | undefined;
52
43
  }>, {
53
44
  state: PreviewPlaceState;
54
- recommendedPlaces: PreviewPlaceRecommendItem[];
55
45
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
46
  declare const _default: typeof __VLS_export;
57
47
  export default _default;
@@ -4,10 +4,13 @@
4
4
  <!-- Figma: Navbar Preview (new tab) — Property 1=Admin -->
5
5
  <div class="shrink-0 w-full bg-cloud">
6
6
  <div class="max-w-[1280px] mx-auto flex justify-end p-[16px]">
7
- <Button variant="outline" @click="emit('exit')">ออกจากพรีวิว</Button>
7
+ <Button variant="outline" class="rounded-[6px]" @click="emit('exit')">
8
+ ออกจากพรีวิว
9
+ </Button>
8
10
  </div>
9
11
  </div>
10
12
 
13
+ <!-- เลื่อนในกล่องนี้ ไม่พึ่ง scroll ของหน้า (บางแอปล็อก html/body ไว้) -->
11
14
  <div class="flex-1 overflow-y-auto">
12
15
  <div class="max-w-[1280px] mx-auto p-[16px] flex flex-col gap-[16px]">
13
16
  <!-- Place Cover -->
@@ -181,12 +184,10 @@
181
184
  </div>
182
185
  </div>
183
186
 
184
- <!-- ขวา: Place About + Reccommended Places -->
187
+ <!-- ขวา: Place About -->
188
+ <!-- TODO: "ที่ไหนน่าสนใจ" (Figma: Reccommended Places) — ยังไม่มี API -->
185
189
  <div class="w-[380px] shrink-0 flex flex-col gap-[16px]">
186
190
  <CardPlaceDetail :item="placeDetailItem" />
187
- <div v-if="props.recommendedPlaces.length > 0" inert>
188
- <CardRecommendPlace :items="props.recommendedPlaces" />
189
- </div>
190
191
  </div>
191
192
  </div>
192
193
  </div>
@@ -201,8 +202,7 @@ import { useApi } from "#pukaad-ui/runtime/composables/useApi";
201
202
  import { useConvert } from "#pukaad-ui/runtime/composables/useConvert";
202
203
  const props = defineProps({
203
204
  placeId: { type: [String, Number], required: true },
204
- state: { type: String, required: false, default: "personal" },
205
- recommendedPlaces: { type: Array, required: false, default: () => [] }
205
+ state: { type: String, required: false, default: "personal" }
206
206
  });
207
207
  const emit = defineEmits(["exit"]);
208
208
  const api = useApi();
@@ -31,19 +31,10 @@ export interface PreviewPlaceData {
31
31
  opening_hours?: OpeningHour[];
32
32
  special_days?: SpecialDay[];
33
33
  }
34
- export interface PreviewPlaceRecommendItem {
35
- id: string | number;
36
- title: string;
37
- image: string;
38
- rating: number;
39
- review_count: number;
40
- }
41
34
  type __VLS_Props = {
42
35
  placeId: string | number;
43
36
  /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
37
  state?: PreviewPlaceState;
45
- /** "ที่ไหนน่าสนใจ" — ยังไม่มี API ให้แอปส่งมาเอง ไม่ส่ง = ซ่อนการ์ด */
46
- recommendedPlaces?: PreviewPlaceRecommendItem[];
47
38
  };
48
39
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
49
40
  exit: () => any;
@@ -51,7 +42,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
51
42
  onExit?: (() => any) | undefined;
52
43
  }>, {
53
44
  state: PreviewPlaceState;
54
- recommendedPlaces: PreviewPlaceRecommendItem[];
55
45
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
46
  declare const _default: typeof __VLS_export;
57
47
  export default _default;
@@ -18,8 +18,8 @@ type __VLS_Slots = {} & {
18
18
  default?: (props: typeof __VLS_1) => any;
19
19
  };
20
20
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
- payload: Record<string, any>;
22
21
  config: ChartConfig;
22
+ payload: Record<string, any>;
23
23
  indicator: "line" | "dot" | "dashed";
24
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
25
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -18,8 +18,8 @@ type __VLS_Slots = {} & {
18
18
  default?: (props: typeof __VLS_1) => any;
19
19
  };
20
20
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
- payload: Record<string, any>;
22
21
  config: ChartConfig;
22
+ payload: Record<string, any>;
23
23
  indicator: "line" | "dot" | "dashed";
24
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
25
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
14
14
  dir?: string | undefined | undefined;
15
15
  draggable?: (boolean | "true" | "false") | undefined;
16
16
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
17
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
17
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
18
18
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
19
19
  id?: string | undefined | undefined;
20
20
  inert?: (boolean | "true" | "false") | undefined;
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
208
208
  dir?: string | undefined | undefined;
209
209
  draggable?: (boolean | "true" | "false") | undefined;
210
210
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
211
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
211
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
212
212
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
213
213
  id?: string | undefined | undefined;
214
214
  inert?: (boolean | "true" | "false") | undefined;
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
14
14
  dir?: string | undefined | undefined;
15
15
  draggable?: (boolean | "true" | "false") | undefined;
16
16
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
17
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
17
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
18
18
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
19
19
  id?: string | undefined | undefined;
20
20
  inert?: (boolean | "true" | "false") | undefined;
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
208
208
  dir?: string | undefined | undefined;
209
209
  draggable?: (boolean | "true" | "false") | undefined;
210
210
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
211
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
211
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
212
212
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
213
213
  id?: string | undefined | undefined;
214
214
  inert?: (boolean | "true" | "false") | undefined;
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
16
16
  dir?: string | undefined | undefined;
17
17
  draggable?: (boolean | "true" | "false") | undefined;
18
18
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
19
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
19
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
20
20
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
21
21
  id?: string | undefined | undefined;
22
22
  inert?: (boolean | "true" | "false") | undefined;
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
212
212
  dir?: string | undefined | undefined;
213
213
  draggable?: (boolean | "true" | "false") | undefined;
214
214
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
215
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
215
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
216
216
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
217
217
  id?: string | undefined | undefined;
218
218
  inert?: (boolean | "true" | "false") | undefined;
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
16
16
  dir?: string | undefined | undefined;
17
17
  draggable?: (boolean | "true" | "false") | undefined;
18
18
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
19
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
19
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
20
20
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
21
21
  id?: string | undefined | undefined;
22
22
  inert?: (boolean | "true" | "false") | undefined;
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
212
212
  dir?: string | undefined | undefined;
213
213
  draggable?: (boolean | "true" | "false") | undefined;
214
214
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
215
- enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
215
+ enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
216
216
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
217
217
  id?: string | undefined | undefined;
218
218
  inert?: (boolean | "true" | "false") | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.454.0",
3
+ "version": "1.458.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",