vanilla-vue-ui 0.0.7 → 0.0.9

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 (65) hide show
  1. package/basic/accordion/AccordionContent.ts +4 -0
  2. package/basic/accordion/WAccordion.stories.ts +49 -0
  3. package/basic/accordion/WAccordion.vue +86 -0
  4. package/basic/card/WCard.stories.ts +36 -0
  5. package/basic/card/WCard.vue +22 -0
  6. package/basic/carousel/WCarousel.stories.ts +29 -0
  7. package/basic/carousel/WCarousel.vue +90 -0
  8. package/basic/checkbox/WCheckbox.spec.ts +58 -0
  9. package/basic/checkbox/WCheckbox.stories.ts +51 -0
  10. package/basic/checkbox/WCheckbox.vue +42 -0
  11. package/basic/date-picker/WDatePicker.stories.ts +79 -0
  12. package/basic/date-picker/WDatePicker.vue +271 -0
  13. package/basic/dialog/DialogStore.ts +57 -0
  14. package/basic/dialog/WDialog.stories.ts +38 -0
  15. package/basic/dialog/WDialog.vue +66 -0
  16. package/basic/divider/WDivider.spec.ts +42 -0
  17. package/basic/divider/WDivider.stories.ts +40 -0
  18. package/basic/divider/WDivider.vue +56 -0
  19. package/basic/feed/TimeLine.ts +9 -0
  20. package/basic/feed/WFeed.spec.ts +64 -0
  21. package/basic/feed/WFeed.stories.ts +62 -0
  22. package/basic/feed/WFeed.vue +41 -0
  23. package/basic/floating-button/WFloatingButton.stories.ts +24 -0
  24. package/basic/floating-button/WFloatingButton.vue +25 -0
  25. package/basic/gradient-text/WGradientText.stories.ts +23 -0
  26. package/basic/gradient-text/WGradientText.vue +5 -0
  27. package/basic/horizontal-scroll/WHorizontalScroll.stories.ts +29 -0
  28. package/basic/horizontal-scroll/WHorizontalScroll.vue +5 -0
  29. package/basic/loading/LoadingStore.ts +57 -0
  30. package/basic/loading/WLoading.stories.ts +27 -0
  31. package/basic/loading/WLoading.vue +54 -0
  32. package/basic/menu/Menu.stories.ts +55 -0
  33. package/basic/menu/WMenu.vue +96 -0
  34. package/basic/menu/WMenuOption.ts +5 -0
  35. package/basic/navigation-drawer/NavigationDrawerContent.ts +11 -0
  36. package/basic/navigation-drawer/WNavigationDrawer.stories.ts +59 -0
  37. package/basic/navigation-drawer/WNavigationDrawer.vue +123 -0
  38. package/basic/notification/NotificationStore.ts +48 -0
  39. package/basic/notification/WNotification.stories.ts +27 -0
  40. package/basic/notification/WNotification.vue +44 -0
  41. package/basic/pagination/WPagination.stories.ts +30 -0
  42. package/basic/pagination/WPagination.vue +58 -0
  43. package/basic/popup/PopupStore.ts +40 -0
  44. package/basic/popup/WPopup.stories.ts +27 -0
  45. package/basic/popup/WPopup.vue +67 -0
  46. package/basic/select/SelectOption.ts +4 -0
  47. package/basic/select/WSelect.stories.ts +56 -0
  48. package/basic/select/WSelect.vue +58 -0
  49. package/basic/skeleton-loader/WSkeletonBar.vue +24 -0
  50. package/basic/skeleton-loader/WSkeletonLoader.stories.ts +23 -0
  51. package/basic/skeleton-loader/WSkeletonLoader.vue +17 -0
  52. package/basic/slide-over/WSlideOver.stories.ts +23 -0
  53. package/basic/slide-over/WSlideOver.vue +53 -0
  54. package/basic/step/StepContent.ts +8 -0
  55. package/basic/step/StepStatus.ts +3 -0
  56. package/basic/step/WStep.stories.ts +42 -0
  57. package/basic/step/WStep.vue +48 -0
  58. package/basic/table/WTable.stories.ts +23 -0
  59. package/basic/table/WTable.vue +30 -0
  60. package/basic/text-area/TextAreaSize.ts +2 -0
  61. package/basic/text-area/WTextArea.spec.ts +18 -0
  62. package/basic/text-area/WTextArea.stories.ts +41 -0
  63. package/basic/text-area/WTextArea.vue +158 -0
  64. package/package.json +1 -1
  65. package/page-template/app-template/WAppTemplate.vue +2 -2
@@ -0,0 +1,271 @@
1
+ <template>
2
+ <div class="mt-10 text-center">
3
+ <div :class="['flex items-center', mergedClasses.text?.color]">
4
+ <button
5
+ type="button"
6
+ class="-m-1.5 flex flex-none items-center justify-center p-1.5 text-onSurface dark:text-onSurface-dark hover:text-onSurface/10 dark:hover:text-onSurface-dark/10"
7
+ @click="previousMonth"
8
+ >
9
+ <span class="sr-only">Previous month</span>
10
+ <ChevronLeftIcon class="h-5 w-5" aria-hidden="true" />
11
+ </button>
12
+ <div class="flex-auto text-sm font-semibold">{{ currentYear + '年 ' + currentMonth + '月' }}</div>
13
+ <button
14
+ type="button"
15
+ class="-m-1.5 flex flex-none items-center justify-center p-1.5 text-onSurface dark:text-onSurface-dark hover:text-onSurface/10 dark:hover:text-onSurface-dark/10"
16
+ @click="nextMonth"
17
+ >
18
+ <span class="sr-only">Next month</span>
19
+ <ChevronRightIcon class="h-5 w-5" aria-hidden="true" />
20
+ </button>
21
+ </div>
22
+
23
+ <!-- Week -->
24
+ <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-onSurface dark:text-onSurface-dark">
25
+ <template v-for="dayOfTheWeek in daysOfTheWeek" :key="dayOfTheWeek">
26
+ <div>{{ dayOfTheWeek }}</div>
27
+ </template>
28
+ </div>
29
+ <div class="isolate mt-2 grid grid-cols-7 gap-px rounded-lg bg-outline dark:bg-outline-dark text-sm shadow ring-1 ring-outline dark:ring-outline-dark">
30
+ <button
31
+ v-for="(day, dayIdx) in days"
32
+ :key="day.date" type="button"
33
+ :disabled="day.disabled ? true : false"
34
+ :class="[
35
+ 'py-1.5 hover:bg-surface/10 dark:hober:bg-surface-dark/10 focus:z-10',
36
+ day.isCurrentMonth && !day.disabled ? mergedClasses.backgroundColor : 'bg-surface/10 dark:bg-surface-dark/10',
37
+ (day.isSelected || day.isToday) && 'font-semibold',
38
+ day.isSelected && 'text-onPrimary dark:text-onPrimary',
39
+ !day.isSelected && day.isCurrentMonth && !day.isToday && 'text-onSurface/40 dark:text-onSurface-dark/40', // 通常の日付
40
+ !day.isSelected && !day.isCurrentMonth && !day.isToday && 'text-onSurface dark:text-onSurface-dark', // 選択した日付
41
+ day.isToday && !day.isSelected && 'text-primary dark:text-primary-dark',
42
+ dayIdx === 0 && 'rounded-tl-lg',
43
+ dayIdx === 6 && 'rounded-tr-lg',
44
+ dayIdx === days.length - 7 && 'rounded-bl-lg',
45
+ dayIdx === days.length - 1 && 'rounded-br-lg',
46
+ ]"
47
+ @click="changeDatePicker(day)">
48
+ <time
49
+ :datetime="day.date"
50
+ :class="[
51
+ 'mx-auto flex h-7 w-7 items-center justify-center rounded-full',
52
+ day.isSelected && day.isToday && 'bg-primary dark:bg-primary-dark', // 選択した時の円かつ当日
53
+ day.isSelected && !day.isToday && 'bg-primary dark:bg-primary-dark' // 選択した時の円
54
+ ]"
55
+ >
56
+ {{ getDayDisplayString(day) }}
57
+ </time>
58
+ </button>
59
+ </div>
60
+ </div>
61
+ <input
62
+ ref="input"
63
+ hidden
64
+ @change="changeInput($event)"
65
+ >
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, defineProps, defineEmits, type PropType } from 'vue'
70
+ import {
71
+ ChevronLeftIcon,
72
+ ChevronRightIcon,
73
+ } from '@heroicons/vue/20/solid'
74
+ import dayjs from 'dayjs';
75
+ import weekday from 'dayjs/plugin/weekday';
76
+ import localeData from 'dayjs/plugin/localeData';
77
+ import 'dayjs/locale/ja';
78
+ import type { ClassObject } from '../../types/ClassObject';
79
+ import { deepMergeClassObject } from '../../util';
80
+
81
+ dayjs.extend(weekday);
82
+ dayjs.extend(localeData);
83
+
84
+ const props = defineProps({
85
+ locale: {
86
+ type: String as PropType<string>,
87
+ default() {
88
+ return 'en'
89
+ }
90
+ },
91
+ disabledDates: {
92
+ type: Array as PropType<string[]>,
93
+ default() {
94
+ return []
95
+ }
96
+ },
97
+ min: {
98
+ type: Date,
99
+ },
100
+ max: {
101
+ type: Date,
102
+ },
103
+ year: {
104
+ type: Number as PropType<number>,
105
+ default() {
106
+ return new Date().getFullYear()
107
+ }
108
+ },
109
+ month: {
110
+ type: Number as PropType<number>,
111
+ default() {
112
+ return new Date().getMonth() + 1
113
+ }
114
+ },
115
+ classes: {
116
+ type: Object as PropType<ClassObject>,
117
+ }
118
+ })
119
+
120
+ const defaultClasses: ClassObject = {
121
+ base: '',
122
+ backgroundColor: 'bg-surface dark:bg-surface-dark',
123
+ text: {
124
+ color: 'text-onSurface dark:text-onSurface-dark'
125
+ },
126
+ rounded: '',
127
+ color: '',
128
+ border: '',
129
+ }
130
+
131
+ // props.classesが渡されていない場合、defaultClassesを使用する
132
+ const mergedClasses = props.classes ? deepMergeClassObject(defaultClasses, props.classes) : defaultClasses;
133
+
134
+ const input = ref<HTMLElement | null>(null)
135
+ const daysOfTheWeek = ref<string[]>([])
136
+
137
+ // emit を定義
138
+ const emit = defineEmits<{
139
+ change: [value: Event]
140
+ }>()
141
+
142
+ // 曜日を設定する
143
+ daysOfTheWeek.value = getDaysOfTheWeek(props.locale)
144
+
145
+ // ロケールを指定すると曜日を返す関数
146
+ function getDaysOfTheWeek(locale: string): string[] {
147
+ if (locale === 'ja') {
148
+ return ['月', '火', '水', '木', '金', '土', '日']
149
+ } else {
150
+ return ['M', 'T', 'W', 'T', 'F', 'S', 'S']
151
+ }
152
+ }
153
+
154
+ function changeDatePicker(day: Day) {
155
+
156
+ // myInput が HTMLInputElement であることを TypeScript に伝える
157
+ const inputElement = input.value as HTMLInputElement | null;
158
+
159
+ if (inputElement) {
160
+ inputElement.value = day.date;
161
+ const event = new Event('change');
162
+ inputElement.dispatchEvent(event);
163
+ } else {
164
+ console.error('input element is not mounted yet');
165
+ }
166
+ }
167
+
168
+ function changeInput(event: Event) {
169
+
170
+ console.log('input chenge.')
171
+
172
+ // days に反映
173
+ const target = event.target as HTMLInputElement
174
+ selectDay(target.value)
175
+
176
+ emit('change', event)
177
+ }
178
+
179
+ interface Day {
180
+ date: string;
181
+ isCurrentMonth?: boolean;
182
+ isToday?: boolean;
183
+ isSelected?: boolean;
184
+ disabled?: boolean;
185
+ }
186
+
187
+ const currentYear = ref<number>(props.year)
188
+ const currentMonth = ref<number>(props.month)
189
+ const days = ref<Day[]>([])
190
+
191
+ // 日付データを生成
192
+ days.value = generateCalendarDays({ year: currentYear.value, month: currentMonth.value, locale: props.locale })
193
+
194
+ function getDayDisplayString(day: Day): string {
195
+
196
+ const date: string = day.date
197
+ return date.split('-').pop()?.replace(/^0/, '') || ''
198
+ }
199
+
200
+ function generateCalendarDays({ year, month, locale, selectedDates }: { year: number, month: number, locale: string, selectedDates?: string[] }): Day[] {
201
+ dayjs.locale(locale); // ロケールを設定
202
+ const days: Day[] = [];
203
+ const today = dayjs().startOf('day').format('YYYY-MM-DD');
204
+
205
+ // new Date に month を流す時は -1 する必要があるので注意
206
+ const monthIndex = month - 1
207
+
208
+ // 月の最初の日と最後の日を取得
209
+ const firstDayOfMonth = dayjs(new Date(year, monthIndex, 1));
210
+ const lastDayOfMonth = dayjs(new Date(year, monthIndex + 1, 0));
211
+
212
+ // 前月の最後の月曜日から始まるように調整
213
+ const startDay = firstDayOfMonth.subtract((firstDayOfMonth.weekday() + 6) % 7, 'day');
214
+
215
+ // 次月の最初の日曜日で終わるように調整
216
+ const endDay = lastDayOfMonth.add(7 - lastDayOfMonth.weekday(), 'day');
217
+
218
+ for (let d = dayjs(startDay); d.isBefore(endDay) || d.isSame(endDay, 'day'); d = d.add(1, 'day')) {
219
+
220
+ const isDisabled = props.disabledDates.includes(d.format('YYYY-MM-DD')) ||
221
+ (props.max ? d.toDate() > props.max : false) ||
222
+ (props.min ? d.toDate() < props.min : false)
223
+
224
+ days.push({
225
+ date: d.format('YYYY-MM-DD'),
226
+ isCurrentMonth: d.month() === monthIndex,
227
+ isToday: d.format('YYYY-MM-DD') === today,
228
+ isSelected: selectedDates ? selectedDates.includes(d.format('YYYY-MM-DD')) : false,
229
+ disabled: isDisabled
230
+ });
231
+ }
232
+
233
+ return days;
234
+ }
235
+
236
+ function nextMonth() {
237
+
238
+ if (currentMonth.value < 12) {
239
+ currentMonth.value++
240
+ } else {
241
+ currentMonth.value = 1
242
+ currentYear.value++
243
+ }
244
+
245
+ // 日付データを生成
246
+ days.value = generateCalendarDays({ year: currentYear.value, month: currentMonth.value, locale: props.locale })
247
+ }
248
+
249
+ function previousMonth() {
250
+
251
+ if (currentMonth.value > 1) {
252
+ currentMonth.value--
253
+ } else {
254
+ currentMonth.value = 12
255
+ currentYear.value--
256
+ }
257
+
258
+ // 日付データを生成
259
+ days.value = generateCalendarDays({ year: currentYear.value, month: currentMonth.value, locale: props.locale })
260
+ }
261
+
262
+ function selectDay(selectedDate: string) {
263
+
264
+ const newDays = days.value.map(day => ({
265
+ ...day,
266
+ isSelected: day.date === selectedDate ? true : false
267
+ }));
268
+
269
+ days.value = newDays
270
+ }
271
+ </script>
@@ -0,0 +1,57 @@
1
+ import { ref, computed } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export const DialogStore = defineStore('dialog', () => {
5
+
6
+ // リアクティブにするために ref を使用する
7
+ const _isOpen = ref(false)
8
+ const _title = ref('')
9
+ const _contentText = ref('')
10
+ const _okButtonText = ref('')
11
+ const _cancelButtonText = ref('')
12
+ let _completion: ((isConfirmed: boolean | null) => void) | null = null
13
+ // true にするとダイアログ以外の場所をタップしても消えない
14
+ // ボタンでしか閉じれなくなる
15
+ const _persistent = ref(false)
16
+
17
+ // computed にすることで直接変更できなくする
18
+ const isOpen = computed(() => _isOpen)
19
+ const getTitle = computed(() => _title)
20
+ const getContentText = computed(() => _contentText)
21
+ const getOkButtonText = computed(() => _okButtonText)
22
+ const getCancelButtonText = computed(() => _cancelButtonText)
23
+ const getPersistent = computed(() => _persistent)
24
+
25
+ function open({ title, contentText, okButtonText = 'OK', cancelButtonText = '', persistent = false, completion = null }: { title: string; contentText: string; okButtonText?: string; cancelButtonText?: string; persistent?: boolean; completion?: ((isConfirmed: boolean | null) => void) | null }) {
26
+
27
+ _title.value = title
28
+ _contentText.value = contentText
29
+ _okButtonText.value = okButtonText
30
+ _cancelButtonText.value = cancelButtonText
31
+ _persistent.value = persistent
32
+ _isOpen.value = true
33
+ _completion = completion
34
+ }
35
+
36
+ function close({ isConfirmed = null }: { isConfirmed?: boolean | null }) {
37
+
38
+ _isOpen.value = false
39
+ _title.value = ''
40
+ _contentText.value = ''
41
+ _okButtonText.value = ''
42
+ _cancelButtonText.value = ''
43
+ _persistent.value = false
44
+
45
+ // コールバック関数が含まれていれば実行
46
+ if (_completion) {
47
+
48
+ // 実行
49
+ _completion(isConfirmed)
50
+
51
+ // リセット
52
+ _completion = null
53
+ }
54
+ }
55
+
56
+ return { isOpen, getPersistent, getTitle, getContentText, getOkButtonText, getCancelButtonText, open, close }
57
+ })
@@ -0,0 +1,38 @@
1
+ // Replace vue3 with vue if you are using Storybook for Vue 2
2
+ import type { Meta, StoryObj } from '@storybook/vue3';
3
+ import { DialogStore } from './DialogStore';
4
+ import Dialog from './Dialog.vue';
5
+
6
+ const meta: Meta<typeof Dialog> = {
7
+ component: Dialog,
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof Dialog>;
12
+
13
+ /*
14
+ *👇 Render functions are a framework specific feature to allow you control on how the component renders.
15
+ * See https://storybook.js.org/docs/api/csf
16
+ * to learn how to use render functions.
17
+ */
18
+ export const Primary: Story = {
19
+ render: () => ({
20
+ setup() {
21
+ const dialogStore = DialogStore()
22
+ dialogStore.open({ title: 'セール', contentText: '初回50パーセントオフキャンペーン実施中!!', persistent: true })
23
+ },
24
+ components: { Dialog },
25
+ template: '<Dialog></Dialog>',
26
+ }),
27
+ };
28
+
29
+ export const WithCancelButton: Story = {
30
+ render: () => ({
31
+ setup() {
32
+ const dialogStore = DialogStore()
33
+ dialogStore.open({ title: 'セール', contentText: '初回50パーセントオフキャンペーン実施中!!', cancelButtonText: 'キャンセル', persistent: true })
34
+ },
35
+ components: { Dialog },
36
+ template: '<Dialog></Dialog>',
37
+ }),
38
+ };
@@ -0,0 +1,66 @@
1
+ <!-- eslint-disable vue/multi-word-component-names -->
2
+ <template>
3
+ <TransitionRoot as="template" :show="isOpen">
4
+ <Dialog as="div" class="relative z-10" @close="close({ isConfirmed: null })">
5
+ <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
6
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
7
+ </TransitionChild>
8
+
9
+ <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
10
+ <div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
11
+ <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
12
+ <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 w-full sm:w-full sm:max-w-sm sm:p-6">
13
+ <div>
14
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
15
+ <CheckIcon class="h-6 w-6 text-green-600" aria-hidden="true" />
16
+ </div>
17
+ <div class="mt-3 text-center sm:mt-5">
18
+ <DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">{{ title }}</DialogTitle>
19
+ <div class="mt-2">
20
+ <p class="text-sm text-gray-500">{{ contentText }}</p>
21
+ <slot />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <div v-if="cancelButtonText.length == 0" class="mt-5 sm:mt-6">
26
+ <PrimaryButton type="button" class="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm" @click="close({ isConfirmed: true })">{{ okButtonText }}</PrimaryButton>
27
+ </div>
28
+
29
+ <div v-else class="mt-5 grid grid-flow-row-dense grid-cols-2 gap-3 sm:mt-6">
30
+ <SecondaryButton ref="cancelButtonRef" type="button" class="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm" @click="close({ isConfirmed: false })">{{ cancelButtonText }}</SecondaryButton>
31
+ <PrimaryButton type="button" class="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm" @click="close({ isConfirmed: true })">{{ okButtonText }}</PrimaryButton>
32
+ </div>
33
+ </DialogPanel>
34
+ </TransitionChild>
35
+ </div>
36
+ </div>
37
+ </Dialog>
38
+ </TransitionRoot>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
43
+ import { CheckIcon } from '@heroicons/vue/24/outline'
44
+ import { DialogStore } from './DialogStore'
45
+ import PrimaryButton from '../../template/primary-button/PrimaryButton.vue';
46
+ import SecondaryButton from '../../template/secondary-button/SecondaryButton.vue'
47
+
48
+ // ダイアログ用 state を購読
49
+ const dialogStore = DialogStore()
50
+ // ストアの状態とアクションをコンポーネントにマッピング
51
+ const isOpen = dialogStore.isOpen;
52
+ const title = dialogStore.getTitle;
53
+ const contentText = dialogStore.getContentText;
54
+ const okButtonText = dialogStore.getOkButtonText
55
+ const cancelButtonText = dialogStore.getCancelButtonText
56
+ const persistent = dialogStore.getPersistent
57
+
58
+ // ダイアログを閉じる
59
+ function close({ isConfirmed }: { isConfirmed: boolean | null }) {
60
+
61
+ // 回答が強制されていて、ボタンを押してない場合はダイアログを閉じさせない
62
+ if (persistent.value == false || isConfirmed != null) {
63
+ dialogStore.close({ isConfirmed: isConfirmed })
64
+ }
65
+ }
66
+ </script>
@@ -0,0 +1,42 @@
1
+ // Divider.spec.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { mount } from '@vue/test-utils';
4
+ import Divider from './Divider.vue';
5
+
6
+ // コンポーネントのテストを記述します
7
+ describe('Divider', () => {
8
+ // コンポーネントが正しくレンダリングされるかテスト
9
+ it('renders properly', () => {
10
+ const wrapper = mount(Divider);
11
+ expect(wrapper.exists()).toBeTruthy();
12
+ expect(wrapper.classes()).toContain('relative');
13
+ });
14
+
15
+ // スロットを通じて渡された内容が正しく表示されるかテスト
16
+ it('displays slot content', () => {
17
+ const slotContent = 'テストコンテンツ';
18
+ const wrapper = mount(Divider, {
19
+ slots: {
20
+ default: slotContent,
21
+ },
22
+ });
23
+ expect(wrapper.text()).toContain(slotContent);
24
+ });
25
+
26
+ // ボーダーのクラスが適用されているかテスト
27
+ it('applies border class correctly', () => {
28
+ const wrapper = mount(Divider);
29
+ const border = wrapper.find('.border-t');
30
+ expect(border.exists()).toBeTruthy();
31
+ expect(border.classes()).toContain('border-gray-300');
32
+ });
33
+
34
+ // テキスト色とサイズのクラスが適用されているかテスト
35
+ it('applies text color and size classes correctly', () => {
36
+ const wrapper = mount(Divider);
37
+ const text = wrapper.find('.text-sm');
38
+ expect(text.exists()).toBeTruthy();
39
+ expect(text.classes()).toContain('text-gray-500');
40
+ expect(text.classes()).toContain('bg-white');
41
+ });
42
+ });
@@ -0,0 +1,40 @@
1
+ // Replace vue3 with vue if you are using Storybook for Vue 2
2
+ import type { Meta, StoryObj } from '@storybook/vue3';
3
+ import Divider from './Divider.vue';
4
+
5
+ type DividerProps = InstanceType<typeof Divider>['$props']
6
+
7
+ const meta: Meta<typeof Divider> = {
8
+ component: Divider,
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Divider>;
13
+
14
+ /*
15
+ *👇 Render functions are a framework specific feature to allow you control on how the component renders.
16
+ * See https://storybook.js.org/docs/api/csf
17
+ * to learn how to use render functions.
18
+ */
19
+ export const Primary: Story = {
20
+ render: (args: DividerProps) => ({
21
+ setup() {
22
+ return {
23
+ ...args
24
+ }
25
+ },
26
+ components: { Divider },
27
+ template: '<Divider :classes="classes">または</Divider>',
28
+ }),
29
+ args: {
30
+ classes: {
31
+ text: {
32
+ color: 'text-onSurface dark:text-onSurface-dark',
33
+ backgroundColor: 'bg-surface dark:bg-surface-dark'
34
+ },
35
+ content: {
36
+ border: 'border-t border-outline dark:border-outline-dark'
37
+ }
38
+ }
39
+ }
40
+ };
@@ -0,0 +1,56 @@
1
+ <!-- eslint-disable vue/multi-word-component-names -->
2
+ <template>
3
+ <div class="relative">
4
+ <div class="absolute inset-0 flex items-center" aria-hidden="true">
5
+ <div
6
+ :class="[
7
+ mergedClasses.content?.spacing,
8
+ mergedClasses.content?.border
9
+ ]"
10
+ />
11
+ </div>
12
+ <div class="relative flex justify-center">
13
+ <span
14
+ :class="[
15
+ mergedClasses.text?.backgroundColor,
16
+ mergedClasses.text?.spacing,
17
+ mergedClasses.text?.size,
18
+ mergedClasses.text?.color,
19
+ mergedClasses.text?.rounded
20
+ ]"
21
+ >
22
+ <slot />
23
+ </span>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { deepMergeClassObject } from '../../util';
30
+ import type { ClassObject } from '../../types/ClassObject';
31
+ import { defineProps, type PropType } from 'vue';
32
+
33
+ const props = defineProps({
34
+ classes: {
35
+ type: Object as PropType<ClassObject>,
36
+ }
37
+ })
38
+
39
+ const defaultClasses: ClassObject = {
40
+ content: {
41
+ spacing: 'w-full',
42
+ border: 'border-t border-gray-300',
43
+ },
44
+ text: {
45
+ backgroundColor: 'bg-white',
46
+ spacing: 'px-2',
47
+ size: 'text-sm',
48
+ color: 'text-gray-500',
49
+ rounded: 'rounded-full'
50
+ }
51
+ }
52
+
53
+ // props.classesが渡されていない場合、defaultClassesを使用する
54
+ const mergedClasses = props.classes ? deepMergeClassObject(defaultClasses, props.classes) : defaultClasses;
55
+
56
+ </script>
@@ -0,0 +1,9 @@
1
+ import type { Component } from "vue"
2
+
3
+ export type TimeLine = {
4
+ id: number
5
+ content: string
6
+ date: string
7
+ icon: Component
8
+ iconBackground: string
9
+ }
@@ -0,0 +1,64 @@
1
+ // Feed.spec.ts
2
+ import { describe, it, expect, beforeEach } from 'vitest'
3
+ import type { VueWrapper } from '@vue/test-utils';
4
+ import { mount } from '@vue/test-utils'
5
+ import Feed from './Feed.vue'
6
+ import { CheckIcon, HandThumbUpIcon, UserIcon } from '@heroicons/vue/20/solid'
7
+
8
+ // タイムラインのダミーデータを用意します
9
+ const timeline = [
10
+ {
11
+ id: 1,
12
+ content: 'Applied to',
13
+ date: '2020-09-20',
14
+ icon: UserIcon,
15
+ iconBackground: 'bg-gray-400',
16
+ },
17
+ {
18
+ id: 2,
19
+ content: 'Advanced to phone screening by',
20
+ date: '2020-09-22',
21
+ icon: HandThumbUpIcon,
22
+ iconBackground: 'bg-blue-500',
23
+ },
24
+ {
25
+ id: 3,
26
+ content: 'Completed phone screening with',
27
+ date: '2020-09-28',
28
+ icon: CheckIcon,
29
+ iconBackground: 'bg-green-500',
30
+ },
31
+ ]
32
+
33
+ describe('Feed.vue', () => {
34
+ let wrapper: VueWrapper
35
+
36
+ beforeEach(() => {
37
+ // 各テストケース実行前にコンポーネントをマウントします
38
+ wrapper = mount(Feed, {
39
+ props: {
40
+ timeline: timeline
41
+ }
42
+ })
43
+ })
44
+
45
+ it('タイムラインが正しく表示されること', () => {
46
+ // タイムラインのイベント数が期待通りであることを検証します
47
+ const events = wrapper.findAll('li')
48
+ expect(events).toHaveLength(timeline.length)
49
+
50
+ // 各イベントが正しくレンダリングされていることを検証します
51
+ events.forEach((event, index) => {
52
+ expect(event.text()).toContain(timeline[index].content)
53
+ expect(event.text()).toContain(timeline[index].date)
54
+ })
55
+ })
56
+
57
+ it('各イベントに正しいアイコンと背景色が設定されていること', () => {
58
+ // 各イベントのアイコンと背景色を検証します
59
+ timeline.forEach((item, index) => {
60
+ const eventIconWrapper = wrapper.findAll('.flex.items-center')[index]
61
+ expect(eventIconWrapper.classes()).toContain(item.iconBackground)
62
+ })
63
+ })
64
+ })