@deplois/template 0.0.0-stage → 0.0.1
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/README.md +113 -2
- package/app/app.config.ts +52 -0
- package/app/components/action/button.vue +35 -0
- package/app/components/content/badge.vue +9 -0
- package/app/components/content/card.vue +9 -0
- package/app/components/content/chart.vue +94 -0
- package/app/components/content/formatted-text.vue +127 -0
- package/app/components/content/table.vue +13 -0
- package/app/components/content/timeline.vue +9 -0
- package/app/components/feedback/alert.vue +9 -0
- package/app/components/feedback/loader.vue +53 -0
- package/app/components/feedback/loading-view.vue +21 -0
- package/app/components/feedback/modal.vue +93 -0
- package/app/components/feedback/toast.vue +79 -0
- package/app/components/feedback/tooltip.vue +9 -0
- package/app/components/file/list.vue +36 -0
- package/app/components/file/upload.vue +114 -0
- package/app/components/form/calendar.vue +10 -0
- package/app/components/form/check-box.vue +64 -0
- package/app/components/form/editor.vue +124 -0
- package/app/components/form/input-error.vue +59 -0
- package/app/components/form/input.vue +43 -0
- package/app/components/form/select.vue +10 -0
- package/app/components/form/terms.vue +54 -0
- package/app/components/form/toggle.vue +29 -0
- package/app/components/navigation/breadcrumb.vue +10 -0
- package/app/components/navigation/tabs.vue +9 -0
- package/app/components/page/dashboard.vue +84 -0
- package/app/components/page/error.vue +45 -0
- package/app/components/page/frame.vue +53 -0
- package/app/error.vue +10 -0
- package/app/languages/en-us.ts +87 -0
- package/app/languages/index.ts +15 -0
- package/app/languages/ja-jp.ts +87 -0
- package/app/languages/ko-kr.ts +87 -0
- package/app/plugins/modal.plugin.ts +103 -0
- package/app/plugins/module.ts +49 -0
- package/app/plugins/toast.plugin.ts +44 -0
- package/app/plugins/variables.plugin.ts +28 -0
- package/app/types/file.ts +8 -0
- package/assets/css/colors.css +61 -0
- package/assets/css/main.css +164 -0
- package/nuxt.config.ts +19 -0
- package/package.json +59 -4
- package/public/_deplois/font/LICENSE +104 -0
- package/public/_deplois/font/PretendardVariable.woff2 +0 -0
- package/public/_deplois/logo.svg +3 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<template lang='pug'>
|
|
2
|
+
UBreadcrumb(:aria-label='$languages.items[$languages.value].components.breadcrumb.label' v-bind='$attrs')
|
|
3
|
+
template(v-for='(_, name) in $slots' #[name]='scope')
|
|
4
|
+
slot(:name='name' v-bind='scope || {}')
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang='ts'>
|
|
8
|
+
const { $languages } = useNuxtApp()
|
|
9
|
+
defineOptions({ inheritAttrs: false })
|
|
10
|
+
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<template lang='pug'>
|
|
2
|
+
div.min-h-svh.bg-default.text-default(class='lg:grid lg:grid-cols-[15rem_minmax(0,1fr)]' :class='props.ui?.root')
|
|
3
|
+
aside.flex.min-w-0.flex-col.border-b.border-default.bg-default(class='lg:sticky lg:top-0 lg:h-svh lg:border-b-0 lg:border-r' :class='props.ui?.sidebar')
|
|
4
|
+
div.flex.h-14.shrink-0.items-center.gap-2.px-3
|
|
5
|
+
slot(name='sidebar-logo')
|
|
6
|
+
img.size-6.shrink-0(src='/_deplois/logo.svg' alt='')
|
|
7
|
+
span.text-lg.font-semibold.paperlogy
|
|
8
|
+
span.text-primary deplois
|
|
9
|
+
span.ml-1 {{ props.name || $languages.items[$languages.value].components.dashboard.workspace }}
|
|
10
|
+
button.ml-auto.flex.size-8.shrink-0.cursor-pointer.items-center.justify-center.rounded-md.text-muted(type='button' class='hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary lg:hidden' :aria-label='isMenuOpen ? $languages.items[$languages.value].components.dashboard.closeMenu : $languages.items[$languages.value].components.dashboard.openMenu' :aria-expanded='isMenuOpen' :aria-controls='`${navigationId}-menu`' @click='isMenuOpen = !isMenuOpen')
|
|
11
|
+
Icon.size-5(:name="isMenuOpen ? 'i-lucide-x' : 'i-lucide-menu'")
|
|
12
|
+
div.grid.min-h-0(class='transition-[grid-template-rows,opacity,visibility] duration-200 ease-out lg:visible lg:flex-1 lg:grid-rows-[1fr] lg:opacity-100 motion-reduce:transition-none' :class="isMenuOpen ? 'visible grid-rows-[1fr] opacity-100' : 'invisible grid-rows-[0fr] opacity-0'" @keydown.esc='isMenuOpen = false')
|
|
13
|
+
div.flex.min-h-0.flex-col.overflow-hidden
|
|
14
|
+
nav.flex.min-h-0.flex-col.gap-1.overflow-y-auto.px-2.pb-3(:id='`${navigationId}-menu`' :aria-label="props.navigationLabel ?? $languages.items[$languages.value].components.dashboard.navigation" class='max-h-72 lg:max-h-none lg:flex-1')
|
|
15
|
+
slot(name='navigation' :menus='props.menus' :selected='selected')
|
|
16
|
+
div(v-for='menu in props.menus' :key='menu.id')
|
|
17
|
+
button.flex.w-full.cursor-pointer.items-center.gap-3.rounded-md.px-3.py-2.text-left.text-sm.transition-colors(
|
|
18
|
+
type='button'
|
|
19
|
+
:disabled='menu.disabled'
|
|
20
|
+
:aria-current="selected == menu.id ? 'page' : undefined"
|
|
21
|
+
:aria-expanded='menu.children?.length ? expanded == menu.id : undefined'
|
|
22
|
+
:aria-controls='menu.children?.length ? `${navigationId}-${menu.id}` : undefined'
|
|
23
|
+
:class="selected == menu.id || menu.children?.some(child => child.id == selected) ? 'bg-primary/10 text-primary font-semibold' : 'text-muted enabled:hover:bg-muted enabled:hover:text-default'"
|
|
24
|
+
class='focus-visible:outline-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none'
|
|
25
|
+
@click="menu.children?.length ? expanded = expanded == menu.id ? '' : menu.id : (selected = menu.id, isMenuOpen = false, emit('select', menu.id))"
|
|
26
|
+
)
|
|
27
|
+
Icon.size-4.shrink-0(v-if='menu.icon' :name='menu.icon')
|
|
28
|
+
span.flex-1 {{ menu.label }}
|
|
29
|
+
span.rounded.bg-muted.px-1.text-xs(v-if='menu.badge != null') {{ menu.badge }}
|
|
30
|
+
Icon.size-4.shrink-0.transition-transform(v-if='menu.children?.length' name='i-lucide-chevron-down' :class="{ 'rotate-180': expanded == menu.id }" class='motion-reduce:transition-none')
|
|
31
|
+
div.grid(v-if='menu.children?.length' :id='`${navigationId}-${menu.id}`' :inert='expanded != menu.id' :class="expanded == menu.id ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'" class='transition-[grid-template-rows,opacity] duration-200 motion-reduce:transition-none')
|
|
32
|
+
div.min-h-0.overflow-hidden
|
|
33
|
+
div.ml-5.mt-1.flex.flex-col.gap-1.border-l.border-default.pl-3
|
|
34
|
+
button.flex.w-full.cursor-pointer.items-center.rounded-md.px-3.py-2.text-left.text-sm.transition-colors(
|
|
35
|
+
v-for='child in menu.children'
|
|
36
|
+
:key='child.id'
|
|
37
|
+
type='button'
|
|
38
|
+
:disabled='menu.disabled || child.disabled'
|
|
39
|
+
:aria-current="selected == child.id ? 'page' : undefined"
|
|
40
|
+
:class="selected == child.id ? 'bg-primary/10 text-primary font-semibold' : 'text-muted enabled:hover:bg-muted enabled:hover:text-default'"
|
|
41
|
+
class='focus-visible:outline-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none'
|
|
42
|
+
@click="selected = child.id; isMenuOpen = false; emit('select', child.id)"
|
|
43
|
+
) {{ child.label }}
|
|
44
|
+
div.border-t.border-default.p-3(v-if='$slots["sidebar-footer"]' class='hidden lg:block')
|
|
45
|
+
slot(name='sidebar-footer')
|
|
46
|
+
main.min-w-0.p-3(class='sm:p-4 lg:p-6' :class='props.ui?.main')
|
|
47
|
+
PageFrame.mx-auto(:padded='false' :show-brand='false' :title='props.title' :subtitle='props.subtitle' :variables='props.variables' :name='props.name' :width='props.width')
|
|
48
|
+
div.mb-4.flex.flex-wrap.items-center.justify-end.gap-2(v-if='$slots.actions')
|
|
49
|
+
slot(name='actions')
|
|
50
|
+
slot(:selected='selected')
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang='ts'>
|
|
54
|
+
const { $languages } = useNuxtApp()
|
|
55
|
+
const props = withDefaults(defineProps<{
|
|
56
|
+
title?: string | null,
|
|
57
|
+
subtitle?: string | null,
|
|
58
|
+
name?: string,
|
|
59
|
+
width?: string | number,
|
|
60
|
+
variables?: Record<string, unknown>,
|
|
61
|
+
navigationLabel?: string,
|
|
62
|
+
menus?: Array<{ id: string, label: string, icon?: string, badge?: string | number, disabled?: boolean, children?: Array<{ id: string, label: string, disabled?: boolean }> }>,
|
|
63
|
+
ui?: Partial<Record<'root' | 'sidebar' | 'main', string>>
|
|
64
|
+
}>(), {
|
|
65
|
+
title: null,
|
|
66
|
+
subtitle: null,
|
|
67
|
+
name: '',
|
|
68
|
+
width: '80rem',
|
|
69
|
+
menus: () => [],
|
|
70
|
+
variables: () => ({})
|
|
71
|
+
})
|
|
72
|
+
const selected = defineModel<string>({ default: '' })
|
|
73
|
+
const navigationId = useId()
|
|
74
|
+
const expanded = ref('')
|
|
75
|
+
const isMenuOpen = ref(false)
|
|
76
|
+
|
|
77
|
+
watch(selected, id => {
|
|
78
|
+
isMenuOpen.value = false
|
|
79
|
+
const parent = props.menus.find(menu => menu.children?.some(child => child.id == id))
|
|
80
|
+
if(parent) expanded.value = parent.id
|
|
81
|
+
}, { immediate: true })
|
|
82
|
+
|
|
83
|
+
const emit = defineEmits<{ select: [id: string] }>()
|
|
84
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template lang='pug'>
|
|
2
|
+
section.w-full.px-2.py-6(:style="{ maxWidth: typeof props.width == 'number' ? `${props.width}px` : props.width }")
|
|
3
|
+
div.mb-6.flex.items-center.justify-between.gap-4
|
|
4
|
+
div.flex.min-w-0.items-center.gap-2
|
|
5
|
+
img.size-5.shrink-0(src='/_deplois/logo.svg' alt='')
|
|
6
|
+
span.text-base.font-semibold.paperlogy
|
|
7
|
+
span.text-primary deplois
|
|
8
|
+
span.ml-1(v-if='props.name') {{ props.name }}
|
|
9
|
+
span.text-xs.font-medium.tabular-nums.text-dimmed {{ props.statusCode }}
|
|
10
|
+
h1.text-xl.font-semibold.leading-snug.paperlogy {{ content.title }}
|
|
11
|
+
p.mt-2.text-sm.leading-6.text-muted {{ content.description }}
|
|
12
|
+
div.mt-6.flex.flex-wrap.items-center.gap-2
|
|
13
|
+
Button.bg-primary.text-white(:auto-loading='false' class='hover:bg-primary-700' @click="emit('home')") {{ messages.home }}
|
|
14
|
+
Button.text-muted(v-if='props.statusCode >= 500' :auto-loading='false' class='hover:bg-muted hover:text-default' @click="emit('retry')") {{ messages.retry }}
|
|
15
|
+
div.mt-4(v-if='$slots.default')
|
|
16
|
+
slot
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang='ts'>
|
|
20
|
+
import { items } from '../../languages'
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<{
|
|
23
|
+
statusCode?: number,
|
|
24
|
+
width?: number | string,
|
|
25
|
+
name?: string
|
|
26
|
+
}>(), { statusCode: 500, width: '26rem', name: '' })
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
home: [],
|
|
30
|
+
retry: []
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
const { $languages } = useNuxtApp()
|
|
34
|
+
const messages = computed(() => ($languages?.items[$languages.value] ?? items['en-us']).components.error)
|
|
35
|
+
const content = computed(() => {
|
|
36
|
+
switch (props.statusCode) {
|
|
37
|
+
case 401: return messages.value.unauthorized
|
|
38
|
+
case 404: return messages.value.notFound
|
|
39
|
+
case 500: return messages.value.server
|
|
40
|
+
default: return props.statusCode >= 500 ? messages.value.server : messages.value.unknown
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
useHead({ title: () => `${content.value.title} - deplois${props.name ? ` ${props.name}` : ''}` })
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template lang='pug'>
|
|
2
|
+
section.w-full(:class="{ 'px-4 py-6': props.padded }" class='transition-[max-width] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none' :style="{ maxWidth: typeof props.width == 'number' ? `${props.width}px` : props.width }")
|
|
3
|
+
div.mb-4(:class='props.ui?.header')
|
|
4
|
+
div.relative.flex.w-max.max-w-full.flex-col.items-start
|
|
5
|
+
TransitionGroup(class='relative flex w-max max-w-full items-center [&>.header-layout-move]:transition-transform [&>.header-layout-move]:duration-[420ms] [&>.header-layout-move]:ease-[cubic-bezier(0.22,1,0.36,1)] [&>.header-layout-leave-active]:absolute motion-reduce:[&>.header-layout-move]:duration-[1ms]' name='header-layout' tag='div')
|
|
6
|
+
div(class='min-h-8 shrink-0 transition-transform duration-[280ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:duration-[1ms]' :class="{ 'translate-y-1': props.subtitle === null }" key='page-title')
|
|
7
|
+
div.relative.flex.min-h-7.items-center.overflow-hidden.whitespace-nowrap
|
|
8
|
+
TransitionGroup(class='relative w-max [&_.slide-up-leave-active]:absolute [&_.slide-up-leave-active]:left-0 [&_.slide-up-leave-active]:top-0 [&_.slide-up-enter-active]:transition-[transform,opacity] [&_.slide-up-enter-active]:duration-[380ms,280ms] [&_.slide-up-enter-active]:ease-[cubic-bezier(0.22,1,0.36,1),ease] [&_.slide-up-leave-active]:transition-[transform,opacity] [&_.slide-up-leave-active]:duration-[380ms,280ms] [&_.slide-up-leave-active]:ease-[cubic-bezier(0.22,1,0.36,1),ease] [&_.slide-up-enter-from]:translate-y-[115%] [&_.slide-up-enter-from]:opacity-0 [&_.slide-up-leave-to]:-translate-y-[115%] [&_.slide-up-leave-to]:opacity-0 motion-reduce:[&_.slide-up-enter-active]:duration-[1ms] motion-reduce:[&_.slide-up-leave-active]:duration-[1ms]' name='slide-up' tag='div')
|
|
9
|
+
h3.font-bold.text-xl.paperlogy(:class='props.ui?.title' v-if='props.title !== null' :key='props.title')
|
|
10
|
+
FormattedText(:value='props.title' :variables='props.variables')
|
|
11
|
+
div.inline-block.w-20.h-2.rounded-full.bg-accented.animate-pulse.absolute(class='top-1/2 -translate-y-1/2' v-if='props.title === null')
|
|
12
|
+
div(class='mx-2 h-4 w-0.5 shrink-0 rounded-full bg-black dark:bg-white' key='divider' v-if='props.showBrand')
|
|
13
|
+
div.flex.shrink-0.items-center(key='brand' v-if='props.showBrand')
|
|
14
|
+
div.w-5.h-5.mr-1
|
|
15
|
+
<svg class='w-full' viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
16
|
+
<rect width="28.35" height="28.44" rx="4.45" class='fill-white dark:fill-white'/>
|
|
17
|
+
<path d="M23.9199 0C26.3661 0.000248215 28.3486 1.98989 28.3486 4.44434V24C28.3486 26.4544 26.366 28.4441 23.9199 28.4443H4.42969C1.98337 28.4443 6.57495e-05 26.4545 0 24V4.44434C0 1.98974 1.98333 0 4.42969 0H23.9199ZM7.47461 15.333C5.79288 15.3331 4.42975 16.7013 4.42969 18.3887V21.167C4.42992 22.8543 5.79299 24.2216 7.47461 24.2217H20.874C22.5558 24.2217 23.9197 22.8543 23.9199 21.167V18.3887C23.9199 16.7012 22.5559 15.333 20.874 15.333H7.47461ZM7.47461 4.44434C5.79284 4.44446 4.42969 5.81254 4.42969 7.5V10.2773C4.42969 11.9648 5.79284 13.3329 7.47461 13.333H20.874C22.5559 13.333 23.9199 11.9649 23.9199 10.2773V7.5C23.9199 5.81246 22.5559 4.44434 20.874 4.44434H7.47461Z" fill="#0088FF"/>
|
|
18
|
+
</svg>
|
|
19
|
+
h3.font-semibold.text-xl.paperlogy
|
|
20
|
+
span(class='text-[#0088FF]') deplois
|
|
21
|
+
span.ml-1.text-default(v-if='props.name') {{ props.name }}
|
|
22
|
+
div(class='-mt-0.5 w-max max-w-full min-w-0' key='subtitle')
|
|
23
|
+
div(class='grid min-w-0 transition-[grid-template-rows,opacity] duration-[280ms,200ms] ease-[cubic-bezier(0.22,1,0.36,1),ease]' :class="props.subtitle === null ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'")
|
|
24
|
+
div(class='relative min-h-0 overflow-hidden [&_.subtitle-enter-active]:transition-[transform,opacity] [&_.subtitle-enter-active]:duration-[380ms,280ms] [&_.subtitle-enter-active]:ease-[cubic-bezier(0.22,1,0.36,1),ease] [&_.subtitle-leave-active]:absolute [&_.subtitle-leave-active]:inset-x-0 [&_.subtitle-leave-active]:top-0 [&_.subtitle-leave-active]:transition-[transform,opacity] [&_.subtitle-leave-active]:duration-[380ms,280ms] [&_.subtitle-leave-active]:ease-[cubic-bezier(0.22,1,0.36,1),ease] [&_.subtitle-enter-from]:translate-y-[115%] [&_.subtitle-enter-from]:opacity-0 [&_.subtitle-leave-to]:-translate-y-[115%] [&_.subtitle-leave-to]:opacity-0 motion-reduce:duration-[1ms] motion-reduce:[&_.subtitle-enter-active]:duration-[1ms] motion-reduce:[&_.subtitle-leave-active]:duration-[1ms]')
|
|
25
|
+
Transition(name='subtitle')
|
|
26
|
+
h3.font-regular.text-sm.paperlogy(:class='props.ui?.subtitle' v-if='props.subtitle !== null' :key='props.subtitle')
|
|
27
|
+
FormattedText(:value='props.subtitle' :variables='props.variables')
|
|
28
|
+
div.min-w-0(v-if='$slots.default' :class='props.ui?.body')
|
|
29
|
+
slot
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang='ts'>
|
|
33
|
+
const props = withDefaults(defineProps<{
|
|
34
|
+
ui?: Partial<Record<'header' | 'title' | 'subtitle' | 'body', string>>,
|
|
35
|
+
title?: string | null,
|
|
36
|
+
subtitle?: string | null,
|
|
37
|
+
width?: number | string,
|
|
38
|
+
name?: string,
|
|
39
|
+
showBrand?: boolean,
|
|
40
|
+
padded?: boolean,
|
|
41
|
+
variables?: Record<string, unknown>
|
|
42
|
+
}>(), {
|
|
43
|
+
title: null,
|
|
44
|
+
subtitle: null,
|
|
45
|
+
width: '48.5rem',
|
|
46
|
+
name: '',
|
|
47
|
+
showBrand: true,
|
|
48
|
+
padded: true,
|
|
49
|
+
variables: () => ({})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
useHead({ title: () => [props.title, ['deplois', props.name].filter(Boolean).join(' ')].filter(Boolean).join(' - ') })
|
|
53
|
+
</script>
|
package/app/error.vue
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<template lang='pug'>
|
|
2
|
+
main.flex.min-h-svh.items-center.justify-center.bg-default.p-4.text-default
|
|
3
|
+
PageError(:status-code='props.error.statusCode || 500' @home="clearError({ redirect: '/' })" @retry='reloadNuxtApp({ force: true })')
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script setup lang='ts'>
|
|
7
|
+
import type { NuxtError } from '#app'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{ error: NuxtError }>()
|
|
10
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: 'English',
|
|
3
|
+
input: {} as Record<string, unknown>,
|
|
4
|
+
components: {
|
|
5
|
+
error: {
|
|
6
|
+
home: 'Go to home',
|
|
7
|
+
retry: 'Try again',
|
|
8
|
+
unauthorized: { title: 'Authentication required', description: 'Your login may have expired, or you may not have access to this page.' },
|
|
9
|
+
notFound: { title: 'Page not found', description: 'Check the address. This page may have moved or been deleted.' },
|
|
10
|
+
server: { title: 'Something went wrong', description: 'We could not process your request. Please try again shortly.' },
|
|
11
|
+
unknown: { title: 'Unable to complete your request', description: 'Return home and try again.' }
|
|
12
|
+
},
|
|
13
|
+
common: {
|
|
14
|
+
loading: 'Loading…',
|
|
15
|
+
close: 'Close',
|
|
16
|
+
cancel: 'Cancel',
|
|
17
|
+
submit: 'Submit',
|
|
18
|
+
apply: 'Apply',
|
|
19
|
+
empty: 'No data to display.',
|
|
20
|
+
select: 'Select an option.'
|
|
21
|
+
},
|
|
22
|
+
dashboard: {
|
|
23
|
+
openMenu: 'Open menu',
|
|
24
|
+
closeMenu: 'Close menu',
|
|
25
|
+
workspace: 'Workspace',
|
|
26
|
+
navigation: 'Main navigation'
|
|
27
|
+
},
|
|
28
|
+
table: {
|
|
29
|
+
loading: 'Loading table…'
|
|
30
|
+
},
|
|
31
|
+
calendar: {
|
|
32
|
+
label: 'Choose a date'
|
|
33
|
+
},
|
|
34
|
+
breadcrumb: {
|
|
35
|
+
label: 'Breadcrumb'
|
|
36
|
+
},
|
|
37
|
+
chart: {
|
|
38
|
+
label: 'Chart'
|
|
39
|
+
},
|
|
40
|
+
editor: {
|
|
41
|
+
loading: 'Loading editor…',
|
|
42
|
+
label: 'Content',
|
|
43
|
+
toolbar: 'Text formatting',
|
|
44
|
+
placeholder: 'Enter content.',
|
|
45
|
+
linkAddress: 'Link address',
|
|
46
|
+
unlink: 'Remove link',
|
|
47
|
+
invalidLink: 'Enter a valid HTTP, HTTPS or email link.',
|
|
48
|
+
bold: 'Bold',
|
|
49
|
+
italic: 'Italic',
|
|
50
|
+
underline: 'Underline',
|
|
51
|
+
bulletList: 'Bullet list',
|
|
52
|
+
heading: 'Heading',
|
|
53
|
+
strike: 'Strikethrough',
|
|
54
|
+
orderedList: 'Numbered list',
|
|
55
|
+
blockquote: 'Quote',
|
|
56
|
+
codeBlock: 'Code block',
|
|
57
|
+
left: 'Align left',
|
|
58
|
+
center: 'Align center',
|
|
59
|
+
right: 'Align right',
|
|
60
|
+
link: 'Link',
|
|
61
|
+
clear: 'Clear formatting',
|
|
62
|
+
undo: 'Undo',
|
|
63
|
+
redo: 'Redo'
|
|
64
|
+
},
|
|
65
|
+
file: {
|
|
66
|
+
select: 'Choose files',
|
|
67
|
+
description: 'Choose files or drop them here.',
|
|
68
|
+
remove: 'Remove ${name}',
|
|
69
|
+
unsupported: 'This file type is not supported. ${name}',
|
|
70
|
+
tooLarge: 'The maximum file size is ${size} bytes. ${name}',
|
|
71
|
+
readError: 'Could not read the file. ${name}'
|
|
72
|
+
},
|
|
73
|
+
terms: {
|
|
74
|
+
label: 'I agree to all terms.',
|
|
75
|
+
link: 'View details',
|
|
76
|
+
required: 'Required',
|
|
77
|
+
optional: 'Optional',
|
|
78
|
+
toggle: 'Expand or collapse terms'
|
|
79
|
+
},
|
|
80
|
+
toast: {
|
|
81
|
+
dismiss: 'Dismiss notification'
|
|
82
|
+
},
|
|
83
|
+
modal: {
|
|
84
|
+
failed: 'Failed to submit. Please try again.'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import korean from './ko-kr'
|
|
2
|
+
import english from './en-us'
|
|
3
|
+
import japanese from './ja-jp'
|
|
4
|
+
import { ko, en, ja } from '@nuxt/ui/locale'
|
|
5
|
+
|
|
6
|
+
export const items = {
|
|
7
|
+
'ko-kr': korean,
|
|
8
|
+
'en-us': english,
|
|
9
|
+
'ja-jp': japanese
|
|
10
|
+
}
|
|
11
|
+
export const locales = {
|
|
12
|
+
'ko-kr': ko,
|
|
13
|
+
'en-us': en,
|
|
14
|
+
'ja-jp': ja
|
|
15
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: '日本語',
|
|
3
|
+
input: {} as Record<string, unknown>,
|
|
4
|
+
components: {
|
|
5
|
+
error: {
|
|
6
|
+
home: 'ホームに戻る',
|
|
7
|
+
retry: 'もう一度試す',
|
|
8
|
+
unauthorized: { title: '認証が必要です', description: 'ログインの有効期限が切れているか、このページへのアクセス権限がありません。' },
|
|
9
|
+
notFound: { title: 'ページが見つかりません', description: 'アドレスを確認してください。ページが移動または削除された可能性があります。' },
|
|
10
|
+
server: { title: '問題が発生しました', description: 'リクエストを処理できませんでした。しばらくしてからもう一度お試しください。' },
|
|
11
|
+
unknown: { title: 'リクエストを完了できませんでした', description: 'ホームに戻ってもう一度お試しください。' }
|
|
12
|
+
},
|
|
13
|
+
common: {
|
|
14
|
+
loading: '読み込み中です。',
|
|
15
|
+
close: '閉じる',
|
|
16
|
+
cancel: 'キャンセル',
|
|
17
|
+
submit: '送信',
|
|
18
|
+
apply: '適用',
|
|
19
|
+
empty: '表示するデータがありません。',
|
|
20
|
+
select: '選択してください。'
|
|
21
|
+
},
|
|
22
|
+
dashboard: {
|
|
23
|
+
openMenu: 'メニューを開く',
|
|
24
|
+
closeMenu: 'メニューを閉じる',
|
|
25
|
+
workspace: 'ワークスペース',
|
|
26
|
+
navigation: 'メインメニュー'
|
|
27
|
+
},
|
|
28
|
+
table: {
|
|
29
|
+
loading: '表を読み込み中です。'
|
|
30
|
+
},
|
|
31
|
+
calendar: {
|
|
32
|
+
label: '日付を選択'
|
|
33
|
+
},
|
|
34
|
+
breadcrumb: {
|
|
35
|
+
label: '現在の位置'
|
|
36
|
+
},
|
|
37
|
+
chart: {
|
|
38
|
+
label: 'チャート'
|
|
39
|
+
},
|
|
40
|
+
editor: {
|
|
41
|
+
loading: 'エディターを読み込み中です。',
|
|
42
|
+
label: '内容',
|
|
43
|
+
toolbar: 'テキスト書式',
|
|
44
|
+
placeholder: '内容を入力してください。',
|
|
45
|
+
linkAddress: 'リンク先',
|
|
46
|
+
unlink: 'リンク解除',
|
|
47
|
+
invalidLink: '有効なHTTP、HTTPSまたはメールリンクを入力してください。',
|
|
48
|
+
bold: '太字',
|
|
49
|
+
italic: '斜体',
|
|
50
|
+
underline: '下線',
|
|
51
|
+
bulletList: '箇条書き',
|
|
52
|
+
heading: '見出し',
|
|
53
|
+
strike: '取り消し線',
|
|
54
|
+
orderedList: '番号付きリスト',
|
|
55
|
+
blockquote: '引用',
|
|
56
|
+
codeBlock: 'コードブロック',
|
|
57
|
+
left: '左揃え',
|
|
58
|
+
center: '中央揃え',
|
|
59
|
+
right: '右揃え',
|
|
60
|
+
link: 'リンク',
|
|
61
|
+
clear: '書式をクリア',
|
|
62
|
+
undo: '元に戻す',
|
|
63
|
+
redo: 'やり直す'
|
|
64
|
+
},
|
|
65
|
+
file: {
|
|
66
|
+
select: 'ファイルを選択',
|
|
67
|
+
description: 'ファイルを選択するかここにドロップしてください。',
|
|
68
|
+
remove: '${name}を削除',
|
|
69
|
+
unsupported: '対応していないファイル形式です。${name}',
|
|
70
|
+
tooLarge: 'ファイルの最大サイズは${size}バイトです。${name}',
|
|
71
|
+
readError: 'ファイルを読み込めません。${name}'
|
|
72
|
+
},
|
|
73
|
+
terms: {
|
|
74
|
+
label: 'すべての利用規約に同意します。',
|
|
75
|
+
link: '詳細を見る',
|
|
76
|
+
required: '必須',
|
|
77
|
+
optional: '任意',
|
|
78
|
+
toggle: '規約を開く / 閉じる'
|
|
79
|
+
},
|
|
80
|
+
toast: {
|
|
81
|
+
dismiss: '通知を閉じる'
|
|
82
|
+
},
|
|
83
|
+
modal: {
|
|
84
|
+
failed: '送信できませんでした。もう一度お試しください。'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: '한국어',
|
|
3
|
+
input: {} as Record<string, unknown>,
|
|
4
|
+
components: {
|
|
5
|
+
error: {
|
|
6
|
+
home: '홈으로 이동',
|
|
7
|
+
retry: '다시 시도',
|
|
8
|
+
unauthorized: { title: '인증이 필요해요', description: '로그인이 만료되었거나 이 페이지에 접근할 권한이 없어요.' },
|
|
9
|
+
notFound: { title: '페이지를 찾을 수 없어요', description: '주소가 올바른지 확인해 주세요. 페이지가 이동되거나 삭제되었을 수 있어요.' },
|
|
10
|
+
server: { title: '잠시 문제가 발생했어요', description: '요청을 처리하지 못했어요. 잠시 후 다시 시도해 주세요.' },
|
|
11
|
+
unknown: { title: '요청을 완료하지 못했어요', description: '홈으로 돌아가 다시 시도해 주세요.' }
|
|
12
|
+
},
|
|
13
|
+
common: {
|
|
14
|
+
loading: '불러오는 중입니다.',
|
|
15
|
+
close: '닫기',
|
|
16
|
+
cancel: '취소',
|
|
17
|
+
submit: '제출',
|
|
18
|
+
apply: '적용',
|
|
19
|
+
empty: '표시할 데이터가 없습니다.',
|
|
20
|
+
select: '선택하세요.'
|
|
21
|
+
},
|
|
22
|
+
dashboard: {
|
|
23
|
+
openMenu: '메뉴 열기',
|
|
24
|
+
closeMenu: '메뉴 닫기',
|
|
25
|
+
workspace: '워크스페이스',
|
|
26
|
+
navigation: '주 메뉴'
|
|
27
|
+
},
|
|
28
|
+
table: {
|
|
29
|
+
loading: '표를 불러오는 중입니다.'
|
|
30
|
+
},
|
|
31
|
+
calendar: {
|
|
32
|
+
label: '날짜 선택'
|
|
33
|
+
},
|
|
34
|
+
breadcrumb: {
|
|
35
|
+
label: '현재 위치'
|
|
36
|
+
},
|
|
37
|
+
chart: {
|
|
38
|
+
label: '차트'
|
|
39
|
+
},
|
|
40
|
+
editor: {
|
|
41
|
+
loading: '에디터를 불러오는 중입니다.',
|
|
42
|
+
label: '내용',
|
|
43
|
+
toolbar: '텍스트 서식',
|
|
44
|
+
placeholder: '내용을 입력하세요.',
|
|
45
|
+
linkAddress: '링크 주소',
|
|
46
|
+
unlink: '링크 해제',
|
|
47
|
+
invalidLink: '올바른 HTTP, HTTPS 또는 이메일 링크를 입력하세요.',
|
|
48
|
+
bold: '굵게',
|
|
49
|
+
italic: '기울임',
|
|
50
|
+
underline: '밑줄',
|
|
51
|
+
bulletList: '목록',
|
|
52
|
+
heading: '제목',
|
|
53
|
+
strike: '취소선',
|
|
54
|
+
orderedList: '번호 목록',
|
|
55
|
+
blockquote: '인용',
|
|
56
|
+
codeBlock: '코드 블록',
|
|
57
|
+
left: '왼쪽 정렬',
|
|
58
|
+
center: '가운데 정렬',
|
|
59
|
+
right: '오른쪽 정렬',
|
|
60
|
+
link: '링크',
|
|
61
|
+
clear: '서식 지우기',
|
|
62
|
+
undo: '실행 취소',
|
|
63
|
+
redo: '다시 실행'
|
|
64
|
+
},
|
|
65
|
+
file: {
|
|
66
|
+
select: '파일 선택',
|
|
67
|
+
description: '파일을 선택하거나 여기에 끌어다 놓으세요.',
|
|
68
|
+
remove: '${name} 삭제',
|
|
69
|
+
unsupported: '지원하지 않는 파일 형식입니다. ${name}',
|
|
70
|
+
tooLarge: '최대 파일 크기는 ${size}바이트입니다. ${name}',
|
|
71
|
+
readError: '파일을 읽을 수 없습니다. ${name}'
|
|
72
|
+
},
|
|
73
|
+
terms: {
|
|
74
|
+
label: '모든 이용 약관에 동의합니다.',
|
|
75
|
+
link: '자세히 보기',
|
|
76
|
+
required: '필수',
|
|
77
|
+
optional: '선택',
|
|
78
|
+
toggle: '약관 펼치기 / 접기'
|
|
79
|
+
},
|
|
80
|
+
toast: {
|
|
81
|
+
dismiss: '알림 닫기'
|
|
82
|
+
},
|
|
83
|
+
modal: {
|
|
84
|
+
failed: '제출하지 못했습니다. 다시 시도해 주세요.'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { reactive } from 'vue'
|
|
2
|
+
import { defineNuxtPlugin } from '#app'
|
|
3
|
+
|
|
4
|
+
export interface ModalFile {
|
|
5
|
+
name: string
|
|
6
|
+
size: number | string
|
|
7
|
+
type: string
|
|
8
|
+
data: string
|
|
9
|
+
}
|
|
10
|
+
export type ModalValue = string | boolean | Array<ModalFile>
|
|
11
|
+
export interface ModalForm {
|
|
12
|
+
type: 'input' | 'select' | 'file' | 'toggle'
|
|
13
|
+
title: string
|
|
14
|
+
name: string
|
|
15
|
+
placeholder?: string
|
|
16
|
+
options?: Array<{ name: string, value: string }>
|
|
17
|
+
value?: ModalValue
|
|
18
|
+
}
|
|
19
|
+
export type ModalCallback = (data?: Record<string, ModalValue>) => unknown | Promise<unknown>
|
|
20
|
+
export interface ModalItem {
|
|
21
|
+
srl: number
|
|
22
|
+
type: 'alert' | 'form'
|
|
23
|
+
title: string
|
|
24
|
+
content: string
|
|
25
|
+
forms?: Array<ModalForm>
|
|
26
|
+
isForced: boolean
|
|
27
|
+
callback?: ModalCallback
|
|
28
|
+
buttonText?: { cancel: string, continue?: string }
|
|
29
|
+
isActive: boolean
|
|
30
|
+
isSubmitting: boolean
|
|
31
|
+
error: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Modal {
|
|
35
|
+
public modals: Array<ModalItem> = reactive([])
|
|
36
|
+
private sequence: number = 0
|
|
37
|
+
|
|
38
|
+
constructor(private readonly failureMessage?: () => string) {}
|
|
39
|
+
|
|
40
|
+
public add(type: 'alert' | 'form', title: string, content: string, forms?: Array<ModalForm>, isForced: boolean = false, callback?: ModalCallback, buttonText?: ModalItem['buttonText']): number {
|
|
41
|
+
const names = new Set<string>()
|
|
42
|
+
const fields = forms?.map(form => {
|
|
43
|
+
if (!form.name || names.has(form.name)) throw new Error('Form field names must be unique and non-empty.')
|
|
44
|
+
if (form.type === 'select' && !form.options) throw new Error('Select options are required.')
|
|
45
|
+
names.add(form.name)
|
|
46
|
+
return {
|
|
47
|
+
...form,
|
|
48
|
+
options: form.options?.map(option => ({ ...option })),
|
|
49
|
+
value: Array.isArray(form.value) ? form.value.map(file => ({ ...file }))
|
|
50
|
+
: form.value ?? (form.type === 'toggle' ? false : form.type === 'file' ? [] : '')
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
const srl = this.sequence++
|
|
54
|
+
this.modals.push({
|
|
55
|
+
srl: srl,
|
|
56
|
+
type: type,
|
|
57
|
+
title: title,
|
|
58
|
+
content: content,
|
|
59
|
+
forms: fields,
|
|
60
|
+
isForced: isForced,
|
|
61
|
+
callback: callback,
|
|
62
|
+
buttonText: buttonText,
|
|
63
|
+
isActive: true,
|
|
64
|
+
isSubmitting: false,
|
|
65
|
+
error: ''
|
|
66
|
+
})
|
|
67
|
+
return srl
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public remove(srl: number, force: boolean = false): void {
|
|
71
|
+
const index = this.modals.findIndex(modal => modal.srl === srl)
|
|
72
|
+
const modal = this.modals[index]
|
|
73
|
+
if (!modal || modal.isSubmitting || (modal.isForced && !force)) return
|
|
74
|
+
this.modals.splice(index, 1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async confirm(srl: number): Promise<void> {
|
|
78
|
+
const modal = this.modals.find(modal => modal.srl === srl)
|
|
79
|
+
if (!modal || modal.isSubmitting) return
|
|
80
|
+
modal.isSubmitting = true
|
|
81
|
+
modal.error = ''
|
|
82
|
+
try {
|
|
83
|
+
const data = modal.forms ? Object.fromEntries(modal.forms.map(form => [form.name, form.value ?? ''])) : undefined
|
|
84
|
+
await modal.callback?.(data)
|
|
85
|
+
modal.isSubmitting = false
|
|
86
|
+
this.remove(srl, true)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
modal.error = error instanceof Error ? error.message : this.failureMessage?.() ?? 'Failed to submit. Please try again.'
|
|
89
|
+
} finally {
|
|
90
|
+
modal.isSubmitting = false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public clear(): void {
|
|
95
|
+
this.modals.splice(0)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default defineNuxtPlugin(nuxtApp => {
|
|
100
|
+
const modal = new Modal(() => nuxtApp.$languages.items[nuxtApp.$languages.value].components.modal.failed)
|
|
101
|
+
if (import.meta.hot) import.meta.hot.dispose(() => modal.clear())
|
|
102
|
+
return { provide: { modal: modal } }
|
|
103
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { vMaska } from 'maska/vue'
|
|
2
|
+
|
|
3
|
+
class Loader {
|
|
4
|
+
public contents: Array<string> = reactive([])
|
|
5
|
+
addLoad(content: string): () => void {
|
|
6
|
+
this.contents.push(content)
|
|
7
|
+
|
|
8
|
+
let removed = false
|
|
9
|
+
return () => {
|
|
10
|
+
if(removed) return
|
|
11
|
+
removed = true
|
|
12
|
+
const index = this.contents.indexOf(content)
|
|
13
|
+
if(index !== -1) this.contents.splice(index, 1)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default defineNuxtPlugin((nuxtApp): {
|
|
19
|
+
provide: {
|
|
20
|
+
loadingView: Loader,
|
|
21
|
+
|
|
22
|
+
copy: (text: string) => void,
|
|
23
|
+
solveButton: ($event: Event) => void,
|
|
24
|
+
JSON: {
|
|
25
|
+
encode: (object: object) => string,
|
|
26
|
+
decode: (text: string) => object
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} => {
|
|
30
|
+
nuxtApp.vueApp.directive('maska', vMaska)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
provide: {
|
|
34
|
+
loadingView: reactive(new Loader()),
|
|
35
|
+
copy: function (text: string) {
|
|
36
|
+
navigator.clipboard.writeText(text)
|
|
37
|
+
},
|
|
38
|
+
solveButton: ($event: Event) => {
|
|
39
|
+
const target = $event.target
|
|
40
|
+
const element = target instanceof Element ? target.closest('button') : null
|
|
41
|
+
element?.dispatchEvent(new Event('loading-end'))
|
|
42
|
+
},
|
|
43
|
+
JSON: {
|
|
44
|
+
encode: (object: object) => { return JSON.stringify(object) },
|
|
45
|
+
decode: (text: string) => { return JSON.parse(text) }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})
|