vanilla-vue-ui 0.0.1 → 0.0.2
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.
- package/basic/app-bar/WAppBar.stories.ts +41 -0
- package/basic/app-bar/WAppBar.vue +48 -0
- package/basic/banner/BannerStore.ts +54 -0
- package/basic/banner/WBanner.stories.ts +28 -0
- package/basic/banner/WBanner.vue +41 -0
- package/basic/breadcrumb/WBreadcrumb.stories.ts +25 -0
- package/basic/breadcrumb/WBreadcrumb.vue +97 -0
- package/basic/button/WButton.stories.ts +10 -3
- package/basic/button/WButton.vue +3 -3
- package/basic/icon/WIcon.vue +5 -0
- package/basic/range/WRange.vue +113 -17
- package/basic/text-field/TextFieldSize.ts +2 -0
- package/basic/text-field/WTextField.stories.ts +88 -0
- package/basic/text-field/WTextField.vue +188 -0
- package/package.json +1 -1
- package/template/footer-simple/WFooterSimple.stories.ts +23 -0
- package/template/footer-simple/WFooterSimple.vue +20 -0
- package/template/navigation-drawer/NavigationDrawer.stories.ts +59 -0
- package/template/navigation-drawer/NavigationDrawer.vue +121 -0
- package/template/navigation-drawer/NavigationDrawerContent.ts +11 -0
- package/template/primary-button/WPrimaryButton.spec.ts +17 -0
- package/template/primary-button/WPrimaryButton.stories.ts +30 -0
- package/template/primary-button/WPrimaryButton.vue +9 -0
- package/template/secondary-button/WSecondaryButton.spec.ts +17 -0
- package/template/secondary-button/WSecondaryButton.stories.ts +30 -0
- package/template/secondary-button/WSecondaryButton.vue +9 -0
- package/template/tree-menu/TreeMenuContent.ts +11 -0
- package/template/tree-menu/WTreeMenu.stories.ts +55 -0
- package/template/tree-menu/WTreeMenu.vue +152 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Replace vue3 with vue if you are using Storybook for Vue 2
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
3
|
+
import AppBar from './WAppBar.vue';
|
|
4
|
+
|
|
5
|
+
type AppBarProps = InstanceType<typeof AppBar>['$props']
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof AppBar> = {
|
|
8
|
+
component: AppBar,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof AppBar>;
|
|
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: AppBarProps) => ({
|
|
21
|
+
setup() {
|
|
22
|
+
return {
|
|
23
|
+
...args
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
components: { AppBar },
|
|
27
|
+
template: '<AppBar :title="title" :classes="classes"></AppBar>',
|
|
28
|
+
}),
|
|
29
|
+
args: {
|
|
30
|
+
title: 'ダッシュボード',
|
|
31
|
+
classes: {
|
|
32
|
+
text: {
|
|
33
|
+
color: 'text-onSurface dark:text-onSurface-dark'
|
|
34
|
+
},
|
|
35
|
+
icon: {
|
|
36
|
+
color: 'text-onSurface dark:text-onSurface-dark'
|
|
37
|
+
},
|
|
38
|
+
border: 'border-b-2 border-outline-100 dark:border-outline-dark'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['sticky top-0 z-40 flex items-center gap-x-6 px-4 py-4 sm:px-6 lg:pl-80', mergedClasses.border]">
|
|
3
|
+
<button type="button" :class="['-m-2.5 p-2.5 lg:hidden', mergedClasses.icon?.color]" @click="open()">
|
|
4
|
+
<span class="sr-only">Open sidebar</span>
|
|
5
|
+
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
|
6
|
+
</button>
|
|
7
|
+
<div :class="['flex-1 text-sm font-semibold leading-6', mergedClasses.text?.color]">{{ title }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import type { PropType } from 'vue'
|
|
13
|
+
import {
|
|
14
|
+
Bars3Icon,
|
|
15
|
+
} from '@heroicons/vue/24/outline'
|
|
16
|
+
import type { ClassObject } from '../../types/ClassObject';
|
|
17
|
+
import { deepMergeClassObject } from '../../util';
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
title: {
|
|
21
|
+
type: String as PropType<string>,
|
|
22
|
+
default: "Dashboard"
|
|
23
|
+
},
|
|
24
|
+
classes: {
|
|
25
|
+
type: Object as PropType<ClassObject>,
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const defaultClasses: ClassObject = {
|
|
30
|
+
text: {
|
|
31
|
+
color: 'text-onSurface dark:text-onSurface-dark'
|
|
32
|
+
},
|
|
33
|
+
icon: {
|
|
34
|
+
color: 'text-onSurface dark:text-onSurface-dark'
|
|
35
|
+
},
|
|
36
|
+
border: 'border-b border-outline dark:border-outline-dark'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// props.classesが渡されていない場合、defaultClassesを使用する
|
|
40
|
+
const mergedClasses = props.classes ? deepMergeClassObject(defaultClasses, props.classes) : defaultClasses;
|
|
41
|
+
|
|
42
|
+
const emits = defineEmits(['open'])
|
|
43
|
+
|
|
44
|
+
function open() {
|
|
45
|
+
emits('open')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
|
|
4
|
+
export const BannerStore = defineStore('banner', () => {
|
|
5
|
+
|
|
6
|
+
// リアクティブにするために ref を使用する
|
|
7
|
+
const _isOpen = ref(false)
|
|
8
|
+
const _title = ref('')
|
|
9
|
+
const _contentText = ref('')
|
|
10
|
+
const _okButtonText = ref('')
|
|
11
|
+
|
|
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 getPersistent = computed(() => _persistent)
|
|
23
|
+
|
|
24
|
+
function open({ title, contentText, okButtonText = 'OK', persistent = false, completion = null }: { title: string; contentText: string; okButtonText?: string; persistent?: boolean; completion?: ((isConfirmed: boolean | null) => void) | null }) {
|
|
25
|
+
|
|
26
|
+
_title.value = title
|
|
27
|
+
_contentText.value = contentText
|
|
28
|
+
_okButtonText.value = okButtonText
|
|
29
|
+
_persistent.value = persistent
|
|
30
|
+
_isOpen.value = true
|
|
31
|
+
_completion = completion
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function close({ isConfirmed = null }: { isConfirmed?: boolean | null }) {
|
|
35
|
+
|
|
36
|
+
_isOpen.value = false
|
|
37
|
+
_title.value = ''
|
|
38
|
+
_contentText.value = ''
|
|
39
|
+
_okButtonText.value = ''
|
|
40
|
+
_persistent.value = false
|
|
41
|
+
|
|
42
|
+
// コールバック関数が含まれていれば実行
|
|
43
|
+
if (_completion) {
|
|
44
|
+
|
|
45
|
+
// 実行
|
|
46
|
+
_completion(isConfirmed)
|
|
47
|
+
|
|
48
|
+
// リセット
|
|
49
|
+
_completion = null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { isOpen, getPersistent, getTitle, getContentText, getOkButtonText, open, close }
|
|
54
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Replace vue3 with vue if you are using Storybook for Vue 2
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
3
|
+
import Banner from './WBanner.vue';
|
|
4
|
+
import { BannerStore } from './BannerStore';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Banner> = {
|
|
8
|
+
component: Banner,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Banner>;
|
|
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: () => ({
|
|
21
|
+
setup() {
|
|
22
|
+
const bannerStore = BannerStore()
|
|
23
|
+
bannerStore.open({ title: 'セール', contentText: '初回50パーセントオフキャンペーン実施中!!'})
|
|
24
|
+
},
|
|
25
|
+
components: { Banner },
|
|
26
|
+
template: '<Banner></Banner>',
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
|
+
<template>
|
|
3
|
+
<div v-if="isOpen">
|
|
4
|
+
<div class="relative isolate flex items-center gap-x-6 overflow-hidden bg-gray-50 px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
|
|
5
|
+
<div class="absolute left-[max(-7rem,calc(50%-52rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl" aria-hidden="true">
|
|
6
|
+
<div class="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] opacity-30" style="clip-path: polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)" />
|
|
7
|
+
</div>
|
|
8
|
+
<div class="absolute left-[max(45rem,calc(50%+8rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl" aria-hidden="true">
|
|
9
|
+
<div class="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] opacity-30" style="clip-path: polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)" />
|
|
10
|
+
</div>
|
|
11
|
+
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
12
|
+
<p class="text-sm leading-6 text-gray-900">
|
|
13
|
+
<strong class="font-semibold">{{ title }}</strong><svg viewBox="0 0 2 2" class="mx-2 inline h-0.5 w-0.5 fill-current" aria-hidden="true"><circle cx="1" cy="1" r="1" /></svg>{{ contentText }}
|
|
14
|
+
</p>
|
|
15
|
+
<slot />
|
|
16
|
+
</div>
|
|
17
|
+
<div class="flex flex-1 justify-end">
|
|
18
|
+
<button type="button" class="-m-3 p-3 focus-visible:outline-offset-[-4px]" @click="close">
|
|
19
|
+
<span class="sr-only">Dismiss</span>
|
|
20
|
+
<XMarkIcon class="h-5 w-5 text-gray-900" aria-hidden="true" />
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { XMarkIcon } from '@heroicons/vue/20/solid'
|
|
29
|
+
import { BannerStore } from './BannerStore'
|
|
30
|
+
|
|
31
|
+
const bannerStore = BannerStore()
|
|
32
|
+
// ストアの状態とアクションをコンポーネントにマッピング
|
|
33
|
+
const isOpen = bannerStore.isOpen;
|
|
34
|
+
const title = bannerStore.getTitle;
|
|
35
|
+
const contentText = bannerStore.getContentText;
|
|
36
|
+
|
|
37
|
+
function close() {
|
|
38
|
+
bannerStore.close({ isConfirmed: false })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Replace vue3 with vue if you are using Storybook for Vue 2
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
3
|
+
import Breadcrumb from './WBreadcrumb.vue';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Breadcrumb> = {
|
|
7
|
+
component: Breadcrumb,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof Breadcrumb>;
|
|
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
|
+
},
|
|
22
|
+
components: { Breadcrumb },
|
|
23
|
+
template: '<Breadcrumb></Breadcrumb>',
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
|
+
<template>
|
|
3
|
+
<nav class="flex" aria-label="Breadcrumb">
|
|
4
|
+
<ol role="list" class="flex items-center space-x-4">
|
|
5
|
+
<li>
|
|
6
|
+
<div>
|
|
7
|
+
<a href="/" :class="mergedClasses.base">
|
|
8
|
+
<HomeIcon class="h-5 w-5 flex-shrink-0" aria-hidden="true" />
|
|
9
|
+
<span class="sr-only">Home</span>
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
</li>
|
|
13
|
+
<li v-for="page in pages" :key="page.name">
|
|
14
|
+
<div class="flex items-center">
|
|
15
|
+
<ChevronRightIcon :class="mergedClasses.icon?.base" aria-hidden="true" />
|
|
16
|
+
<a
|
|
17
|
+
:href="`${page.href}`"
|
|
18
|
+
:class="mergedClasses.text?.base"
|
|
19
|
+
:aria-current="page.current ? 'page' : undefined"
|
|
20
|
+
>
|
|
21
|
+
{{ page.name }}
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
</li>
|
|
25
|
+
</ol>
|
|
26
|
+
</nav>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { ChevronRightIcon, HomeIcon } from '@heroicons/vue/20/solid'
|
|
31
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
32
|
+
import type { RouteMeta } from 'vue-router';
|
|
33
|
+
import { ref, onMounted, watch, type PropType } from 'vue';
|
|
34
|
+
import type { ClassObject } from '../../types/ClassObject';
|
|
35
|
+
import { deepMergeClassObject } from '../../util';
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
classes: {
|
|
39
|
+
type: Object as PropType<ClassObject>,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const defaultClasses: ClassObject = {
|
|
44
|
+
base: 'text-gray-400 hover:text-gray-500',
|
|
45
|
+
text: {
|
|
46
|
+
base: 'ml-4 text-sm font-medium text-gray-500 hover:text-gray-700'
|
|
47
|
+
},
|
|
48
|
+
icon: {
|
|
49
|
+
base: 'h-5 w-5 flex-shrink-0 text-gray-400'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// props.classesが渡されていない場合、defaultClassesを使用する
|
|
54
|
+
const mergedClasses = props.classes ? deepMergeClassObject(defaultClasses, props.classes) : defaultClasses;
|
|
55
|
+
|
|
56
|
+
type Page = {
|
|
57
|
+
name: string;
|
|
58
|
+
href: string;
|
|
59
|
+
current: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const pages = ref<Page[]>([]);
|
|
63
|
+
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
const route = useRoute();
|
|
66
|
+
|
|
67
|
+
onMounted(set);
|
|
68
|
+
watch(() => route.path, set);
|
|
69
|
+
|
|
70
|
+
function set() {
|
|
71
|
+
const pathSegments = splitPath(route.path);
|
|
72
|
+
let fullPath = '';
|
|
73
|
+
|
|
74
|
+
pages.value = pathSegments.map((segment, index) => {
|
|
75
|
+
fullPath += '/' + segment;
|
|
76
|
+
|
|
77
|
+
const metaData = getRouteMeta(fullPath);
|
|
78
|
+
// タイトルが undefined の場合は segment をそのまま使用
|
|
79
|
+
const title = metaData?.title as string || segment;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: title, // ここで string 型を強制的に指定
|
|
83
|
+
href: fullPath,
|
|
84
|
+
current: (index === pathSegments.length - 1) // 最後のセグメントだけを現在とマーク
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function splitPath(path: string): string[] {
|
|
90
|
+
return path.replace(/^\//, '').split('/');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getRouteMeta(path: string): RouteMeta | undefined {
|
|
94
|
+
const matchedRoute = router.getRoutes().find(route => route.path === path);
|
|
95
|
+
return matchedRoute?.meta;
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
@@ -25,15 +25,15 @@ export const Primary: Story = {
|
|
|
25
25
|
},
|
|
26
26
|
components: { WButton },
|
|
27
27
|
template: `
|
|
28
|
-
<div><w-button
|
|
29
|
-
<div><w-button
|
|
28
|
+
<div><w-button>OK</w-button></div>
|
|
29
|
+
<div><w-button href="/">OK</w-button></div>
|
|
30
30
|
`,
|
|
31
31
|
}),
|
|
32
32
|
args: {
|
|
33
33
|
classes: {
|
|
34
34
|
color: 'text-onSurface dark:text-onSurface-dark',
|
|
35
35
|
backgroundColor: '',
|
|
36
|
-
border: 'border border-outline
|
|
36
|
+
border: 'border border-outline dark:border-outline-dark'
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
};
|
|
@@ -45,6 +45,13 @@ export const Block: Story = {
|
|
|
45
45
|
}),
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const Disabled: Story = {
|
|
49
|
+
render: () => ({
|
|
50
|
+
components: { WButton },
|
|
51
|
+
template: '<w-button :disabled="true">OK</w-button>',
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
|
|
48
55
|
export const Size: Story = {
|
|
49
56
|
render: () => ({
|
|
50
57
|
components: { WButton },
|
package/basic/button/WButton.vue
CHANGED
|
@@ -67,10 +67,10 @@ const props = defineProps({
|
|
|
67
67
|
const defaultClasses: ClassObject = {
|
|
68
68
|
base: 'font-bold',
|
|
69
69
|
spacing: 'py-2 px-4',
|
|
70
|
-
backgroundColor: '
|
|
70
|
+
backgroundColor: '',
|
|
71
71
|
rounded: 'rounded-full',
|
|
72
|
-
color: 'hover:bg-
|
|
73
|
-
border: 'border border-
|
|
72
|
+
color: 'text-onSurface dark:text-onSurface-dark hover:bg-surfaceHover dark:hover:bg-surfaceHover-dark',
|
|
73
|
+
border: 'border border-outline dark:border-outline-dark',
|
|
74
74
|
size: '',
|
|
75
75
|
}
|
|
76
76
|
|
package/basic/range/WRange.vue
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
<div class="flex">
|
|
3
|
+
<MinusIcon
|
|
4
|
+
class="flex-none"
|
|
5
|
+
:class="[
|
|
6
|
+
mergedClasses.icon?.color
|
|
7
|
+
]"
|
|
8
|
+
@click="decrement()"
|
|
9
|
+
/>
|
|
10
|
+
<div
|
|
11
|
+
class="flex-1 px-2 flex items-center"
|
|
12
|
+
>
|
|
13
|
+
<input
|
|
14
|
+
id="input-range"
|
|
15
|
+
type="range"
|
|
16
|
+
class="w-full"
|
|
17
|
+
:class="[
|
|
18
|
+
mergedClasses.content?.base,
|
|
19
|
+
mergedClasses.content?.backgroundColor,
|
|
20
|
+
mergedClasses.content?.rounded,
|
|
21
|
+
mergedClasses.content?.size,
|
|
22
|
+
]"
|
|
23
|
+
:min="min"
|
|
24
|
+
:max="max"
|
|
25
|
+
:step="step"
|
|
26
|
+
:value="modelValue"
|
|
27
|
+
@input="inputValue"
|
|
28
|
+
>
|
|
29
|
+
</div>
|
|
30
|
+
<PlusIcon
|
|
31
|
+
:class="[
|
|
32
|
+
mergedClasses.icon?.color,
|
|
33
|
+
]"
|
|
34
|
+
@click="increment()"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
17
37
|
</template>
|
|
18
38
|
|
|
19
39
|
<script setup lang="ts">
|
|
20
|
-
import { defineProps, type PropType, ref } from 'vue'
|
|
40
|
+
import { defineProps, onMounted, type PropType, ref } from 'vue'
|
|
41
|
+
import { MinusIcon, PlusIcon } from '@heroicons/vue/24/outline'
|
|
21
42
|
import type { ClassObject } from '../../types/ClassObject';
|
|
22
43
|
import { deepMergeClassObject } from '../../util';
|
|
23
44
|
|
|
@@ -57,12 +78,45 @@ const defaultClasses: ClassObject = {
|
|
|
57
78
|
},
|
|
58
79
|
content: {
|
|
59
80
|
base: 'appearance-none [-webkit-appearance: none]',
|
|
60
|
-
backgroundColor: 'bg-gray-
|
|
81
|
+
backgroundColor: 'bg-gray-300',
|
|
61
82
|
rounded: 'rounded-full',
|
|
62
83
|
size: 'h-2'
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
const inputRangeActiveColor = '#f59e0b'
|
|
88
|
+
const inputRangeInactiveColor = '#d1d5db'
|
|
89
|
+
|
|
90
|
+
type SliderBackgroundOptions = {
|
|
91
|
+
value: number
|
|
92
|
+
max: number
|
|
93
|
+
min: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function updateSliderBg(sliderBackgroundOptions: SliderBackgroundOptions) {
|
|
97
|
+
const inputRange = document.getElementById('input-range') as HTMLInputElement
|
|
98
|
+
if (inputRange) {
|
|
99
|
+
const value = sliderBackgroundOptions.value
|
|
100
|
+
const min = sliderBackgroundOptions.min
|
|
101
|
+
const max = sliderBackgroundOptions.max
|
|
102
|
+
const ratio = ((value - min) / (max - min)) * 100
|
|
103
|
+
|
|
104
|
+
inputRange.style.background = `linear-gradient(90deg, ${inputRangeActiveColor} ${ratio}%, ${inputRangeInactiveColor} ${ratio}%)`
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onMounted(() => {
|
|
109
|
+
// つまみの前後で色を塗り分ける
|
|
110
|
+
const inputRange = document.getElementById('input-range') as HTMLInputElement
|
|
111
|
+
|
|
112
|
+
if (inputRange) {
|
|
113
|
+
updateSliderBg({ value: Number(modelValue.value), max: props.max, min: props.min }) // 初期値を反映
|
|
114
|
+
inputRange.addEventListener('input', function (e) {
|
|
115
|
+
updateSliderBg({ value: Number(modelValue.value), max: props.max, min: props.min })
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
66
120
|
// props.classesが渡されていない場合、defaultClassesを使用する
|
|
67
121
|
const mergedClasses = props.classes ? deepMergeClassObject(defaultClasses, props.classes) : defaultClasses;
|
|
68
122
|
|
|
@@ -73,6 +127,28 @@ const emit = defineEmits<{
|
|
|
73
127
|
'update:modelValue': [value: number]
|
|
74
128
|
}>()
|
|
75
129
|
|
|
130
|
+
function increment() {
|
|
131
|
+
if (Number(modelValue.value) < props.max) {
|
|
132
|
+
const number = Number(modelValue.value)
|
|
133
|
+
modelValue.value = (number + props.step).toString()
|
|
134
|
+
emitCustomEvent(modelValue.value)
|
|
135
|
+
emit('update:modelValue', Number(modelValue.value))
|
|
136
|
+
|
|
137
|
+
updateSliderBg({ value: Number(modelValue.value), max: props.max, min: props.min})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function decrement() {
|
|
142
|
+
if (Number(modelValue.value) > props.min) {
|
|
143
|
+
const number = Number(modelValue.value)
|
|
144
|
+
modelValue.value = (number - props.step).toString()
|
|
145
|
+
emitCustomEvent(modelValue.value)
|
|
146
|
+
emit('update:modelValue', Number(modelValue.value))
|
|
147
|
+
|
|
148
|
+
updateSliderBg({ value: Number(modelValue.value), max: props.max, min: props.min})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
76
152
|
function inputValue(e: Event) {
|
|
77
153
|
const target = e.target as HTMLInputElement | null
|
|
78
154
|
|
|
@@ -91,3 +167,23 @@ function emitCustomEvent(value: string) {
|
|
|
91
167
|
emit('customInput', event)
|
|
92
168
|
}
|
|
93
169
|
</script>
|
|
170
|
+
|
|
171
|
+
<style>
|
|
172
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
173
|
+
background-color: white;
|
|
174
|
+
appearance: none;
|
|
175
|
+
width: 20px;
|
|
176
|
+
height: 20px;
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
border: 2px solid rgb(251, 191, 36);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
input[type="range"]::-moz-range-thumb {
|
|
182
|
+
background-color: white;
|
|
183
|
+
appearance: none;
|
|
184
|
+
width: 20px;
|
|
185
|
+
height: 20px;
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
border: 2px solid rgb(251, 191, 36);
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Replace vue3 with vue if you are using Storybook for Vue 2
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3';
|
|
3
|
+
import TextField from './WTextField.vue';
|
|
4
|
+
|
|
5
|
+
type TextFieldProps = InstanceType<typeof TextField>['$props']
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof TextField> = {
|
|
8
|
+
component: TextField,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof TextField>;
|
|
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: TextFieldProps) => ({
|
|
21
|
+
setup() {
|
|
22
|
+
return {
|
|
23
|
+
...args
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
components: { TextField },
|
|
27
|
+
template: `
|
|
28
|
+
<div class="p-4">
|
|
29
|
+
<div class="mb-2"><TextField size="xs" value="John" :classes="classes"></TextField></div>
|
|
30
|
+
<div class="mb-2"><TextField size="sm" value="John" :classes="classes"></TextField></div>
|
|
31
|
+
<div class="mb-2"><TextField size="base" value="John" :classes="classes"></TextField></div>
|
|
32
|
+
<div class="mb-2"><TextField size="lg" value="John" :classes="classes"></TextField></div>
|
|
33
|
+
<div class="mb-2"><TextField size="2xl" value="John" :classes="classes"></TextField></div>
|
|
34
|
+
<div class="mb-2"><TextField size="3xl" value="John" :classes="classes"></TextField></div>
|
|
35
|
+
<div class="mb-2"><TextField size="6xl" value="John" :classes="classes"></TextField></div>
|
|
36
|
+
</div>
|
|
37
|
+
`,
|
|
38
|
+
}),
|
|
39
|
+
args: {
|
|
40
|
+
classes: {
|
|
41
|
+
content: {
|
|
42
|
+
input: {
|
|
43
|
+
color: 'text-onSurface dark:text-onSurface-dark',
|
|
44
|
+
backgroundColor: 'bg-white dark:bg-surface-dark'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithLabel: Story = {
|
|
52
|
+
render: () => ({
|
|
53
|
+
components: { TextField },
|
|
54
|
+
template: `
|
|
55
|
+
<div class="mb-2"><TextField size="xs" label="Name"></TextField></div>
|
|
56
|
+
<div class="mb-2"><TextField size="sm" label="Name"></TextField></div>
|
|
57
|
+
<div class="mb-2"><TextField size="base" label="Name"></TextField></div>
|
|
58
|
+
<div class="mb-2"><TextField size="lg" label="Name"></TextField></div>
|
|
59
|
+
<div class="mb-2"><TextField size="2xl" label="Name"></TextField></div>
|
|
60
|
+
<div class="mb-2"><TextField size="3xl" label="Name"></TextField></div>
|
|
61
|
+
<div class="mb-2"><TextField size="6xl" label="Name"></TextField></div>
|
|
62
|
+
`,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithPlaceholder: Story = {
|
|
67
|
+
render: () => ({
|
|
68
|
+
components: { TextField },
|
|
69
|
+
template: '<TextField placeholder="入力してください"></TextField>',
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithError: Story = {
|
|
74
|
+
render: (args: TextFieldProps) => ({
|
|
75
|
+
setup() {
|
|
76
|
+
return {
|
|
77
|
+
...args,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
components: { TextField },
|
|
81
|
+
template: '<TextField value="John Smith" :rules="rules"></TextField>',
|
|
82
|
+
}),
|
|
83
|
+
args: {
|
|
84
|
+
rules: [
|
|
85
|
+
(value: string) => value.length <= 5 || 'Max 5 characters',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
};
|