@dargmuesli/nuxt-vio 2.0.1 → 3.0.0-beta.10

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 (79) hide show
  1. package/app.config.ts +83 -40
  2. package/components/vio/_/VioApp.vue +92 -0
  3. package/components/{VioError.vue → vio/_/VioError.vue} +1 -1
  4. package/components/{VioLink.vue → vio/_/VioLink.vue} +2 -2
  5. package/components/vio/button/VioButtonColored.vue +52 -0
  6. package/components/vio/card/VioCard.vue +19 -0
  7. package/components/vio/card/state/VioCardState.vue +20 -0
  8. package/components/vio/card/state/VioCardStateAlert.vue +14 -0
  9. package/components/vio/form/VioForm.vue +84 -0
  10. package/components/vio/form/VioFormCheckbox.vue +27 -0
  11. package/components/vio/form/input/VioFormInput.vue +192 -0
  12. package/components/vio/form/input/VioFormInputIconWrapper.vue +7 -0
  13. package/components/vio/form/input/VioFormInputUrl.vue +54 -0
  14. package/components/vio/form/input/state/VioFormInputState.vue +5 -0
  15. package/components/vio/form/input/state/VioFormInputStateError.vue +32 -0
  16. package/components/vio/form/input/state/VioFormInputStateInfo.vue +32 -0
  17. package/components/vio/icon/IconArrowRight.vue +31 -0
  18. package/components/vio/icon/IconCalendar.vue +31 -0
  19. package/components/vio/icon/IconChatOutline.vue +27 -0
  20. package/components/vio/icon/IconChatSolid.vue +26 -0
  21. package/components/vio/icon/IconCheckCircle.vue +29 -0
  22. package/components/vio/icon/IconContainer.vue +15 -0
  23. package/components/vio/icon/IconDownload.vue +31 -0
  24. package/components/vio/icon/IconExclamationCircle.vue +29 -0
  25. package/components/vio/icon/IconHome.vue +31 -0
  26. package/components/vio/icon/IconHourglass.vue +32 -0
  27. package/components/vio/icon/IconLightbulb.vue +27 -0
  28. package/components/vio/icon/IconLogo.vue +17 -0
  29. package/components/vio/icon/IconMixcloud.vue +23 -0
  30. package/components/vio/icon/IconMusic.vue +27 -0
  31. package/components/vio/icon/IconPlay.vue +25 -0
  32. package/components/vio/icon/IconShare.vue +27 -0
  33. package/components/{VioLayout.vue → vio/layout/VioLayout.vue} +1 -1
  34. package/components/vio/layout/VioLayoutBreadcrumbs.vue +83 -0
  35. package/components/vio/layout/VioLayoutFooter.vue +40 -0
  36. package/components/vio/layout/VioLayoutFooterCategory.vue +17 -0
  37. package/components/vio/layout/VioLayoutHeader.vue +98 -0
  38. package/components/vio/layout/VioLayoutSpanList.vue +20 -0
  39. package/components/vio/loader/indicator/VioLoaderIndicator.vue +14 -0
  40. package/components/vio/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
  41. package/components/vio/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
  42. package/components/{VioLegalNotice.vue → vio/page/VioPageLegalNotice.vue} +10 -8
  43. package/components/{VioPrivacyPolicy.vue → vio/page/VioPagePrivacyPolicy.vue} +19 -12
  44. package/composables/useAppLayout.ts +12 -18
  45. package/composables/useDateTime.ts +17 -0
  46. package/composables/useFavicons.ts +5 -33
  47. package/composables/useFireError.ts +17 -0
  48. package/composables/useGetServiceHref.ts +21 -0
  49. package/composables/useHeadDefault.ts +21 -0
  50. package/composables/usePolyfills.ts +23 -0
  51. package/composables/useStrapiFetch.ts +10 -0
  52. package/error.vue +5 -2
  53. package/locales/de.json +7 -1
  54. package/locales/en.json +7 -1
  55. package/nuxt.config.ts +56 -10
  56. package/package.json +37 -25
  57. package/pages/legal-notice.vue +1 -1
  58. package/pages/privacy-policy.vue +1 -1
  59. package/plugins/dayjs.ts +34 -0
  60. package/plugins/gtag.client.ts +3 -0
  61. package/plugins/i18n.ts +5 -0
  62. package/plugins/marked.ts +9 -0
  63. package/server/middleware/headers.ts +41 -10
  64. package/server/tsconfig.json +1 -1
  65. package/server/utils/util.ts +2 -0
  66. package/store/auth.ts +32 -0
  67. package/tailwind.config.ts +131 -10
  68. package/types/api.d.ts +9 -0
  69. package/types/fetch.d.ts +8 -0
  70. package/types/modules/gql.d.ts +6 -0
  71. package/types/modules/graphql.d.ts +6 -0
  72. package/utils/constants.ts +10 -1
  73. package/utils/form.ts +19 -0
  74. package/utils/networking.ts +117 -0
  75. package/utils/text.ts +20 -0
  76. package/LICENSE +0 -674
  77. package/components/VioApp.vue +0 -59
  78. /package/components/{VioButton.vue → vio/button/VioButton.vue} +0 -0
  79. /package/components/{VioHr.vue → vio/layout/VioLayoutHr.vue} +0 -0
package/app.config.ts CHANGED
@@ -1,57 +1,100 @@
1
- import { useSeoMeta } from '@unhead/vue'
1
+ import { useServerSeoMeta } from '@unhead/vue'
2
2
 
3
3
  export default defineAppConfig({
4
- legalNotice: undefined,
5
- privacyPolicy: undefined,
6
- seoMeta: {
7
- twitterSite: '@dargmuesli',
4
+ vio: {
5
+ pages: undefined,
6
+ seoMeta: undefined,
7
+ server: {
8
+ middleware: {
9
+ headers: {
10
+ csp: {
11
+ default: {
12
+ 'Cross-Origin-Opener-Policy': 'same-origin',
13
+ // 'Cross-Origin-Embedder-Policy', 'require-corp') // https://stackoverflow.com/questions/71904052/getting-notsameoriginafterdefaultedtosameoriginbycoep-error-with-helmet
14
+ 'Cross-Origin-Resource-Policy': 'same-origin',
15
+ // 'Expect-CT', 'max-age=0') // deprecated (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT)
16
+ NEL: '\'{"report_to":"default","max_age":31536000,"include_subdomains":true}\'',
17
+ 'Origin-Agent-Cluster': '?1',
18
+ 'Permissions-Policy': '',
19
+ 'Referrer-Policy': 'no-referrer',
20
+ 'Report-To':
21
+ '\'{"group":"default":"max_age":31536000:"endpoints":[{"url":"https://dargmuesli.report-uri.com/a/d/g"}]:"include_subdomains":true}\'',
22
+ 'X-Content-Type-Options': 'nosniff',
23
+ 'X-DNS-Prefetch-Control': 'off',
24
+ 'X-Download-Options': 'noopen',
25
+ 'X-Frame-Options': 'SAMEORIGIN',
26
+ 'X-Permitted-Cross-Domain-Policies': 'none',
27
+ 'X-XSS-Protection': '1; mode=block', // TODO: set back to `0` once CSP does not use `unsafe-*` anymore (https://github.com/maevsi/maevsi/issues/1047)
28
+ },
29
+ production: {
30
+ 'Strict-Transport-Security':
31
+ 'max-age=31536000; includeSubDomains; preload',
32
+ },
33
+ },
34
+ },
35
+ },
36
+ },
37
+ themeColor: undefined,
8
38
  },
9
- themeColor: '#202020',
10
39
  })
11
40
 
12
41
  declare module 'nuxt/schema' {
13
42
  interface AppConfigInput {
14
- legalNotice?: {
15
- contact: {
16
- email: string
17
- }
18
- responsibility: {
19
- address: {
20
- city: string
21
- name: string
22
- street: string
23
- }
24
- }
25
- tmg: {
26
- address: {
27
- city: string
28
- name: string
29
- street: string
43
+ vio: {
44
+ pages?: {
45
+ legalNotice?: {
46
+ contact: {
47
+ email: string
48
+ }
49
+ responsibility: {
50
+ address: {
51
+ city: string
52
+ name: string
53
+ street: string
54
+ }
55
+ }
56
+ tmg: {
57
+ address: {
58
+ city: string
59
+ name: string
60
+ street: string
61
+ }
62
+ }
30
63
  }
31
- }
32
- }
33
- privacyPolicy?: {
34
- hostingCdn?: {
35
- external: {
36
- address: {
37
- city: string
38
- name: string
39
- street: string
64
+ privacyPolicy?: {
65
+ hostingCdn?: {
66
+ external: {
67
+ address: {
68
+ city: string
69
+ name: string
70
+ street: string
71
+ }
72
+ }
73
+ }
74
+ mandatoryInfo?: {
75
+ responsible: {
76
+ address: {
77
+ city: string
78
+ email: string
79
+ name: string
80
+ street: string
81
+ }
82
+ }
40
83
  }
41
84
  }
42
85
  }
43
- mandatoryInfo?: {
44
- responsible: {
45
- address: {
46
- city: string
47
- email: string
48
- name: string
49
- street: string
86
+ seoMeta?: Parameters<typeof useServerSeoMeta>[0]
87
+ server?: {
88
+ middleware: {
89
+ headers: {
90
+ csp: {
91
+ default: Record<string, string>
92
+ production: Record<string, string>
93
+ }
50
94
  }
51
95
  }
52
96
  }
97
+ themeColor?: string
53
98
  }
54
- seoMeta?: Parameters<typeof useSeoMeta>[0]
55
- themeColor?: string
56
99
  }
57
100
  }
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div :data-is-loading="isLoading" data-testid="is-loading">
3
+ <NuxtLayout>
4
+ <!-- `NuxtLayout` can't have mulitple child nodes (https://github.com/nuxt/nuxt/issues/21759) -->
5
+ <div>
6
+ <NuxtPage :site-description="siteDescriptionProp" />
7
+ <CookieControl :locale="locale" />
8
+ </div>
9
+ </NuxtLayout>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import type { Locale } from '@dargmuesli/nuxt-cookie-control/dist/runtime/types'
15
+ import type { WritableComputedRef } from 'vue'
16
+
17
+ export interface Props {
18
+ ogImageAlt: string
19
+ ogImageComponent?: string
20
+ siteDescription: string
21
+ }
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ ogImageComponent: undefined,
24
+ })
25
+ const ogImageAltProp = toRef(() => props.ogImageAlt)
26
+ const ogImageComponentProp = toRef(() => props.ogImageComponent)
27
+ const siteDescriptionProp = toRef(() => props.siteDescription)
28
+
29
+ const { $dayjs } = useNuxtApp()
30
+ const i18n = useI18n()
31
+ const cookieControl = useCookieControl()
32
+ const siteConfig = useSiteConfig()
33
+
34
+ const { loadingIds, indicateLoadingDone } = useLoadingDoneIndicator('app')
35
+
36
+ // data
37
+ const locale = i18n.locale as WritableComputedRef<Locale>
38
+
39
+ // methods
40
+ const init = () => {
41
+ if (process.client) {
42
+ const cookieTimezone = useCookie(TIMEZONE_COOKIE_NAME, {
43
+ // default: () => undefined, // setting `default` on the client side only does not write the cookie
44
+ httpOnly: false,
45
+ sameSite: 'strict',
46
+ secure: true,
47
+ })
48
+ // @ts-ignore `tz` should be part of `$dayjs` (https://github.com/iamkun/dayjs/issues/2106)
49
+ cookieTimezone.value = $dayjs.tz.guess()
50
+ }
51
+ }
52
+
53
+ // computations
54
+ const isLoading = computed(() => !!loadingIds.value.length)
55
+
56
+ // lifecycle
57
+ onMounted(() => indicateLoadingDone())
58
+ watch(
59
+ () => cookieControl.cookiesEnabledIds.value,
60
+ (current, previous) => {
61
+ if (
62
+ (!previous?.includes('ga') && current?.includes('ga')) ||
63
+ (previous?.includes('ga') && !current?.includes('ga'))
64
+ ) {
65
+ window.location.reload()
66
+ }
67
+ },
68
+ { deep: true },
69
+ )
70
+
71
+ // initialization
72
+ updateSiteConfig({
73
+ description: siteDescriptionProp.value,
74
+ })
75
+ defineOgImage({
76
+ alt: ogImageAltProp.value,
77
+ component: ogImageComponentProp.value,
78
+ description: siteDescriptionProp.value,
79
+ })
80
+ useAppLayout()
81
+ useFavicons()
82
+ usePolyfills()
83
+ useSchemaOrg([
84
+ defineWebSite({
85
+ description: siteDescriptionProp,
86
+ inLanguage: locale,
87
+ name: siteConfig.name,
88
+ }),
89
+ defineWebPage(),
90
+ ])
91
+ init()
92
+ </script>
@@ -3,7 +3,7 @@
3
3
  <div>
4
4
  {{ description }}
5
5
  </div>
6
- <div v-if="stack && !runtimeConfig.public.isInProduction" v-html="stack" />
6
+ <pre v-if="stack && !runtimeConfig.public.isInProduction" v-html="stack" />
7
7
  </template>
8
8
 
9
9
  <script setup lang="ts">
@@ -12,7 +12,7 @@
12
12
  >
13
13
  <slot />
14
14
  </a>
15
- <NuxtLink
15
+ <NuxtLinkLocale
16
16
  v-else
17
17
  :aria-label="ariaLabel"
18
18
  :class="classes"
@@ -20,7 +20,7 @@
20
20
  @click="emit('click')"
21
21
  >
22
22
  <slot />
23
- </NuxtLink>
23
+ </NuxtLinkLocale>
24
24
  </template>
25
25
 
26
26
  <script setup lang="ts">
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <VioButton
3
+ :is-to-relative="isToRelative"
4
+ :aria-label="ariaLabel"
5
+ class="rounded-md border px-4 py-2 font-medium"
6
+ :class="
7
+ [
8
+ ...(isPrimary
9
+ ? [
10
+ 'border-transparent bg-gray-800 text-text-bright hover:bg-black dark:bg-yellow-500 dark:text-gray-800 dark:hover:bg-yellow-600',
11
+ ]
12
+ : [
13
+ 'border-gray-300 text-text-dark hover:bg-black/5 dark:border-gray-600 dark:text-text-bright dark:hover:bg-black/30',
14
+ ]),
15
+ ].join(' ')
16
+ "
17
+ :disabled="disabled"
18
+ :to="to"
19
+ :type="type"
20
+ @click="emit('click')"
21
+ >
22
+ <slot />
23
+ <template #prefix>
24
+ <slot name="prefix" />
25
+ </template>
26
+ <template #suffix>
27
+ <slot name="suffix" />
28
+ </template>
29
+ </VioButton>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ export interface Props {
34
+ ariaLabel: string
35
+ disabled?: boolean
36
+ isPrimary?: boolean
37
+ isToRelative?: boolean
38
+ to?: string
39
+ type?: 'button' | 'reset' | 'submit'
40
+ }
41
+ withDefaults(defineProps<Props>(), {
42
+ disabled: false,
43
+ isPrimary: true,
44
+ isToRelative: false,
45
+ to: undefined,
46
+ type: 'button',
47
+ })
48
+
49
+ const emit = defineEmits<{
50
+ click: []
51
+ }>()
52
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div
3
+ class="overflow-hidden rounded-lg"
4
+ :class="[backgroundColor, ...(isHigh ? ['px-4 py-6'] : ['p-4'])]"
5
+ >
6
+ <slot />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ export interface Props {
12
+ backgroundColor?: string
13
+ isHigh?: boolean
14
+ }
15
+ withDefaults(defineProps<Props>(), {
16
+ backgroundColor: 'bg-background-brighten dark:bg-background-darken',
17
+ isHigh: false,
18
+ })
19
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <VioCard
3
+ :background-color="backgroundColor"
4
+ class="border-0 text-center font-medium text-white"
5
+ :class="{ 'rounded-none': isEdgy }"
6
+ >
7
+ <slot />
8
+ </VioCard>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ export interface Props {
13
+ backgroundColor?: string
14
+ isEdgy?: boolean
15
+ }
16
+ withDefaults(defineProps<Props>(), {
17
+ backgroundColor: undefined,
18
+ isEdgy: false,
19
+ })
20
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <VioCardState background-color="bg-red-600" :is-edgy="isEdgy" role="alert">
3
+ <slot />
4
+ </VioCardState>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ export interface Props {
9
+ isEdgy?: boolean
10
+ }
11
+ withDefaults(defineProps<Props>(), {
12
+ isEdgy: false,
13
+ })
14
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <form
3
+ v-if="form"
4
+ :class="[
5
+ { 'animate-shake rounded-lg border border-red-500': errors?.length },
6
+ formClass,
7
+ ]"
8
+ novalidate
9
+ @submit="(e) => emit('submit', e)"
10
+ >
11
+ <VioCard class="flex flex-col" is-high>
12
+ <div class="flex flex-col min-h-0 overflow-y-auto gap-6">
13
+ <slot />
14
+ <div class="flex flex-col items-center justify-between">
15
+ <VioButtonColored
16
+ :aria-label="submitName || t('submit')"
17
+ :class="{
18
+ 'animate-shake': form.$error,
19
+ }"
20
+ type="submit"
21
+ @click="emit('click')"
22
+ >
23
+ {{ submitName || t('submit') }}
24
+ <template #prefix>
25
+ <slot name="submit-icon" />
26
+ </template>
27
+ </VioButtonColored>
28
+ <VioFormInputStateError v-if="form.$error" class="mt-2">
29
+ {{ t('globalValidationFailed') }}
30
+ </VioFormInputStateError>
31
+ </div>
32
+ <VioCardStateAlert v-if="errorMessages?.length" class="my-4">
33
+ <VioLayoutSpanList :span="errorMessages" />
34
+ </VioCardStateAlert>
35
+ <div v-if="$slots.assistance" class="flex justify-center">
36
+ <slot name="assistance" />
37
+ </div>
38
+ </div>
39
+ </VioCard>
40
+ </form>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import type { BaseValidation } from '@vuelidate/core'
45
+
46
+ import type { BackendError } from '../../../types/api'
47
+
48
+ export interface Props {
49
+ errors?: BackendError[]
50
+ errorsPgIds?: Record<string, string>
51
+ form: BaseValidation
52
+ formClass?: string
53
+ isFormSent?: boolean
54
+ submitName?: string
55
+ }
56
+ const props = withDefaults(defineProps<Props>(), {
57
+ errors: undefined,
58
+ errorsPgIds: undefined,
59
+ formClass: undefined,
60
+ isFormSent: false,
61
+ submitName: undefined,
62
+ })
63
+
64
+ const emit = defineEmits<{
65
+ click: []
66
+ submit: [event: Event]
67
+ }>()
68
+
69
+ const { t } = useI18n()
70
+
71
+ // computations
72
+ const errorMessages = computed(() =>
73
+ props.errors
74
+ ? getCombinedErrorMessages(props.errors, props.errorsPgIds)
75
+ : undefined,
76
+ )
77
+ </script>
78
+
79
+ <i18n lang="yaml">
80
+ de:
81
+ submit: Absenden
82
+ en:
83
+ submit: Submit
84
+ </i18n>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div>
3
+ <input
4
+ :id="`input-${formKey}`"
5
+ class="rounded"
6
+ type="checkbox"
7
+ :checked="value"
8
+ @change="emit('change', ($event.target as HTMLInputElement).checked)"
9
+ />
10
+ <label class="pl-2" :for="`input-${formKey}`"><slot /></label>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ export interface Props {
16
+ formKey?: string
17
+ value?: boolean
18
+ }
19
+ withDefaults(defineProps<Props>(), {
20
+ formKey: undefined,
21
+ value: undefined,
22
+ })
23
+
24
+ const emit = defineEmits<{
25
+ change: [change: boolean]
26
+ }>()
27
+ </script>
@@ -0,0 +1,192 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ :class="{
5
+ 'form-input-success': success,
6
+ 'form-input-warning': warning,
7
+ 'form-input-error': value?.$error,
8
+ }"
9
+ class="flex-wrap md:flex md:items-center"
10
+ >
11
+ <div class="mb-1 md:mb-0 md:w-1/3 md:pr-4 md:text-right">
12
+ <label
13
+ class="inline-flex items-baseline gap-2 font-semibold md:flex-col md:items-end md:gap-0"
14
+ :class="{
15
+ 'form-input-success': success,
16
+ 'form-input-warning': warning,
17
+ 'form-input-error': value?.$error,
18
+ }"
19
+ :for="idLabel"
20
+ >
21
+ <span>{{ title }}</span>
22
+ <span
23
+ class="text-xs font-medium text-gray-500 dark:text-gray-400 md:text-right"
24
+ >
25
+ <span v-if="isRequired">
26
+ {{ t('required') }}
27
+ </span>
28
+ <span v-if="isOptional">
29
+ {{ t('optional') }}
30
+ </span>
31
+ </span>
32
+ </label>
33
+ </div>
34
+ <div class="flex md:mt-1 md:w-2/3">
35
+ <div class="relative min-w-0 grow">
36
+ <slot v-if="$slots.default" />
37
+ <input
38
+ v-else
39
+ :id="idLabel"
40
+ class="form-input"
41
+ :class="{
42
+ 'rounded-r-none': $slots.icon,
43
+ }"
44
+ :disabled="isDisabled"
45
+ :placeholder="placeholder"
46
+ :readonly="isReadonly"
47
+ :type="type"
48
+ :value="valueFormatter(value?.$model as string)"
49
+ @input="emit('input', ($event.target as HTMLInputElement)?.value)"
50
+ @click="emit('click')"
51
+ />
52
+ <div v-if="validationProperty && isValidatable">
53
+ <VioFormInputIconWrapper v-if="validationProperty.$pending">
54
+ <IconHourglass
55
+ class="text-blue-600"
56
+ :title="t('globalStatusLoading')"
57
+ />
58
+ </VioFormInputIconWrapper>
59
+ <VioFormInputIconWrapper
60
+ v-else-if="
61
+ validationProperty.$model && !validationProperty.$invalid
62
+ "
63
+ >
64
+ <IconCheckCircle class="text-green-600" :title="t('valid')" />
65
+ </VioFormInputIconWrapper>
66
+ <VioFormInputIconWrapper
67
+ v-else-if="
68
+ validationProperty.$model && validationProperty.$invalid
69
+ "
70
+ >
71
+ <IconExclamationCircle
72
+ class="text-red-600"
73
+ :title="t('validNot')"
74
+ />
75
+ </VioFormInputIconWrapper>
76
+ </div>
77
+ </div>
78
+ <span
79
+ v-if="$slots.icon"
80
+ class="inline-flex cursor-pointer items-center rounded-r-md border border-l-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-600"
81
+ @click="emit('icon')"
82
+ >
83
+ <slot name="icon" />
84
+ </span>
85
+ </div>
86
+ <div class="md:w-1/3" />
87
+ <div class="md:w-2/3">
88
+ <slot name="stateSuccess" />
89
+ </div>
90
+ <div class="md:w-1/3" />
91
+ <div class="md:w-2/3">
92
+ <slot name="stateInfo" />
93
+ <VioFormInputStateInfo v-if="value?.$pending">
94
+ {{ t('globalStatusLoading') }}
95
+ </VioFormInputStateInfo>
96
+ </div>
97
+ <div class="md:w-1/3" />
98
+ <div class="md:w-2/3">
99
+ <slot name="stateWarning" />
100
+ </div>
101
+ <div class="md:w-1/3" />
102
+ <div class="md:w-2/3">
103
+ <slot name="stateError" />
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </template>
108
+
109
+ <script setup lang="ts">
110
+ import type { BaseValidation } from '@vuelidate/core'
111
+ import { consola } from 'consola'
112
+
113
+ export interface Props {
114
+ isDisabled?: boolean
115
+ isOptional?: boolean
116
+ isReadonly?: boolean
117
+ isRequired?: boolean
118
+ isValidatable?: boolean
119
+ idLabel?: string
120
+ placeholder?: string
121
+ success?: boolean
122
+ title?: string
123
+ type?: string
124
+ validationProperty?: BaseValidation
125
+ value?: BaseValidation
126
+ valueFormatter?: (x?: string) => typeof x | undefined
127
+ warning?: boolean
128
+ }
129
+ const props = withDefaults(defineProps<Props>(), {
130
+ isDisabled: false,
131
+ isOptional: false,
132
+ isReadonly: false,
133
+ isRequired: false,
134
+ isValidatable: false,
135
+ idLabel: undefined,
136
+ placeholder: undefined,
137
+ success: false,
138
+ title: undefined,
139
+ type: undefined,
140
+ validationProperty: undefined,
141
+ value: undefined,
142
+ valueFormatter: (x?: string) => x,
143
+ warning: false,
144
+ })
145
+ const typeProp = toRef(() => props.type)
146
+
147
+ const emit = defineEmits<{
148
+ icon: []
149
+ input: [input: string]
150
+ click: []
151
+ }>()
152
+
153
+ const { t } = useI18n()
154
+
155
+ // initialization
156
+ if (
157
+ !props.placeholder &&
158
+ typeProp.value &&
159
+ ![
160
+ 'checkbox',
161
+ 'datetime-local',
162
+ 'number',
163
+ 'select',
164
+ 'textarea',
165
+ 'tiptap',
166
+ 'radio',
167
+ ].includes(typeProp.value)
168
+ ) {
169
+ consola.warn(`placeholder is missing for ${props.idLabel}!`)
170
+ }
171
+
172
+ if (
173
+ !props.value &&
174
+ typeProp.value &&
175
+ !['checkbox', 'select'].includes(typeProp.value)
176
+ ) {
177
+ consola.warn(`value is missing for ${props.idLabel}!`)
178
+ }
179
+ </script>
180
+
181
+ <i18n lang="yaml">
182
+ de:
183
+ optional: optional
184
+ required: Pflichtfeld
185
+ valid: Gültig
186
+ validNot: Ungültig
187
+ en:
188
+ optional: optional
189
+ required: required
190
+ valid: valid
191
+ validNot: invalid
192
+ </i18n>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <span class="absolute right-0 top-1/2 -translate-y-1/2 px-3">
3
+ <div class="rounded-full bg-gray-50">
4
+ <slot />
5
+ </div>
6
+ </span>
7
+ </template>