@konoma-development/vue-components 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.
Files changed (55) hide show
  1. package/.nuxtrc +1 -0
  2. package/.playground/app.vue +64 -0
  3. package/.playground/eslint.config.ts +3 -0
  4. package/.playground/nuxt.config.ts +9 -0
  5. package/.tool-versions +1 -0
  6. package/.vscode/extensions.json +9 -0
  7. package/.vscode/settings.json +12 -0
  8. package/README.md +70 -0
  9. package/app.config.ts +13 -0
  10. package/components/KonomaTheme.vue +92 -0
  11. package/components/defaults/button.ts +19 -0
  12. package/components/defaults/checkbox.ts +13 -0
  13. package/components/defaults/checkboxList.ts +9 -0
  14. package/components/defaults/columnChooser.ts +8 -0
  15. package/components/defaults/input.ts +16 -0
  16. package/components/defaults/pagination.ts +10 -0
  17. package/components/defaults/radioButtonGroup.ts +13 -0
  18. package/components/defaults/select.ts +20 -0
  19. package/components/defaults/table.ts +16 -0
  20. package/components/defaults/tabs.ts +10 -0
  21. package/components/defaults/tag.ts +7 -0
  22. package/components/defaults/tagList.ts +14 -0
  23. package/components/defaults/textarea.ts +16 -0
  24. package/components/form/KonomaCheckbox.vue +68 -0
  25. package/components/form/KonomaCheckboxList.vue +42 -0
  26. package/components/form/KonomaForm.vue +71 -0
  27. package/components/form/KonomaFormField.vue +36 -0
  28. package/components/form/KonomaInput.vue +92 -0
  29. package/components/form/KonomaPhoneInput.vue +9 -0
  30. package/components/form/KonomaRadioButtonGroup.vue +41 -0
  31. package/components/form/KonomaSelect.vue +9 -0
  32. package/components/form/KonomaTagList.vue +55 -0
  33. package/components/form/KonomaTextarea.vue +81 -0
  34. package/components/form/injectionKeys.ts +8 -0
  35. package/components/table/KonomaColumnChooser.vue +64 -0
  36. package/components/table/KonomaColumnChooserEntry.vue +18 -0
  37. package/components/table/KonomaPagination.vue +81 -0
  38. package/components/table/KonomaTable.vue +355 -0
  39. package/components/table/KonomaTableActionEntry.vue +27 -0
  40. package/components/table/KonomaTableActions.vue +32 -0
  41. package/components/ui/KonomaButton.vue +109 -0
  42. package/components/ui/KonomaIcon.vue +13 -0
  43. package/components/ui/KonomaLoadingIndicator.vue +14 -0
  44. package/components/ui/KonomaModal.vue +120 -0
  45. package/components/ui/KonomaTabs.vue +70 -0
  46. package/components/ui/KonomaTag.vue +49 -0
  47. package/composables/useKonomaTheme.ts +5 -0
  48. package/eslint.config.ts +43 -0
  49. package/index.d.ts +33 -0
  50. package/nuxt.config.ts +20 -0
  51. package/package.json +60 -0
  52. package/tsconfig.json +11 -0
  53. package/types/form.ts +149 -0
  54. package/types/table.ts +33 -0
  55. package/unocss.config.ts +83 -0
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <!-- TODO: The cast for errors[props.name] should not be neccessary -->
3
+ <component
4
+ :is="component"
5
+ v-bind="$props"
6
+ :error="errors && props.name ? errors[props.name as keyof typeof errors] : undefined"
7
+ />
8
+ </template>
9
+
10
+ <script lang="ts" setup generic="DataType extends FormDataType">
11
+ import type { FormDataType, FormFieldProps } from '../../types/form';
12
+ import type KonomaCheckbox from './KonomaCheckbox.vue';
13
+ import type KonomaCheckboxList from './KonomaCheckboxList.vue';
14
+ import type KonomaInput from './KonomaInput.vue';
15
+ import type KonomaPhoneInput from './KonomaPhoneInput.vue';
16
+ import type KonomaRadioButtonGroup from './KonomaRadioButtonGroup.vue';
17
+ import type KonomaSelect from './KonomaSelect.vue';
18
+ import type KonomaTagList from './KonomaTagList.vue';
19
+ import type KonomaTextarea from './KonomaTextarea.vue';
20
+ import { formInjectionKeys } from './injectionKeys';
21
+
22
+ const props = defineProps<FormFieldProps<DataType> & { component: keyof FormFieldComponents }>()
23
+
24
+ interface FormFieldComponents {
25
+ checkbox: typeof KonomaCheckbox
26
+ checkboxList: typeof KonomaCheckboxList
27
+ input: typeof KonomaInput
28
+ radioButtonGroup: typeof KonomaRadioButtonGroup
29
+ select: typeof KonomaSelect
30
+ textarea: typeof KonomaTextarea
31
+ tagList: typeof KonomaTagList
32
+ phoneInput: typeof KonomaPhoneInput
33
+ };
34
+
35
+ const errors = inject(formInjectionKeys<DataType>().errors)
36
+ </script>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <label :class="wrapperClasses">
3
+ <span v-if="$slots.label" :class="labelClasses">
4
+ <slot name="label" /><template v-if="required">*</template>
5
+ </span>
6
+ <div class="relative">
7
+ <div v-if="mask">
8
+ <!-- TODO: masked input -->
9
+ </div>
10
+ <input
11
+ v-else
12
+ :class="combinedClasses"
13
+ :placeholder="placeholder"
14
+ :value="value"
15
+ :step="step"
16
+ :name="name?.toString()"
17
+ v-bind="$attrs"
18
+ @input="(e) => $emit('input', (e.target as HTMLInputElement).value, e)"
19
+ @click="$emit('click', $event)"
20
+ @blur="$emit('blur', $event)"
21
+ @keydown="$emit('keyDown', $event)"
22
+ >
23
+ <KonomaIcon
24
+ v-if="iconLeftPath || iconLeftName"
25
+ :name="iconLeftName"
26
+ :path="iconLeftPath"
27
+ :class="iconLeftClasses"
28
+ @click="$emit('iconLeftClick')"
29
+ />
30
+ <div v-if="iconRightPath || iconRightName || textRight" :class="wrapperRightClasses">
31
+ <span v-if="textRight">{{ textRight }}</span>
32
+ <KonomaIcon
33
+ v-if="iconRightPath || iconRightName"
34
+ :name="iconRightName"
35
+ :path="iconRightPath"
36
+ :class="iconRightClasses"
37
+ @click="$emit('iconRightClick', $event)"
38
+ />
39
+ </div>
40
+ </div>
41
+ <template v-if="error && error.length > 0">
42
+ <span v-for="(e, i) in error" :key="i" :class="errorClasses">
43
+ {{ e }}
44
+ </span>
45
+ </template>
46
+ </label>
47
+ </template>
48
+
49
+ <script lang="ts" setup generic="DataType extends FormDataType">
50
+ import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
51
+ import { baseClasses } from '../defaults/input';
52
+ import KonomaIcon from '../ui/KonomaIcon.vue';
53
+
54
+ const props = withDefaults(defineProps<FormFieldProps<DataType> & { class?: string }>(), {
55
+ classes: baseClasses.classes,
56
+ wrapperClasses: baseClasses.wrapperClasses,
57
+ labelClasses: baseClasses.labelClasses,
58
+ iconLeftClasses: baseClasses.iconLeftClasses,
59
+ iconRightClasses: baseClasses.iconRightClasses,
60
+ errorClasses: baseClasses.errorClasses,
61
+ classesError: baseClasses.classesError,
62
+ classesNeutral: baseClasses.classesNeutral,
63
+ additionalClassesIconLeft: baseClasses.additionalClassesIconLeft,
64
+ additionalClassesIconRight: baseClasses.additionalClassesIconRight,
65
+ wrapperRightClasses: baseClasses.wrapperRightClasses,
66
+ })
67
+
68
+ defineEmits<FormFieldEmits>()
69
+
70
+ const combinedClasses = computed(() => {
71
+ const classesFull = [props.classes];
72
+ if (props.iconLeftPath || props.iconLeftName) {
73
+ classesFull.push(props.additionalClassesIconLeft);
74
+ }
75
+ if (props.iconRightPath || props.iconRightName || props.textRight) {
76
+ classesFull.push(props.additionalClassesIconRight);
77
+ }
78
+ if (props.centered) {
79
+ classesFull.push('text-center');
80
+ }
81
+ if (props.error && props.error.length > 0) {
82
+ classesFull.push(props.classesError);
83
+ } else {
84
+ classesFull.push(props.classesNeutral);
85
+ }
86
+ if (props.class) {
87
+ classesFull.push(props.class);
88
+ }
89
+
90
+ return classesFull;
91
+ });
92
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div />
3
+ </template>
4
+
5
+ <script lang="ts" setup generic="DataType extends FormDataType">
6
+ import type { FormDataType } from '../../types/form';
7
+
8
+ // TODO: Old implementation needs select component and masked input component
9
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div :class="wrapperClasses">
3
+ <span :class="labelClasses">
4
+ <slot name="label" /><template v-if="required">*</template>
5
+ </span>
6
+ <div :class="['flex gap-5', arrangement === 'vertical' ? 'flex-col' : 'flex-row'].join(' ')">
7
+ <label v-for="(option, i) in options" :key="i" :class="labelWrapperClasses">
8
+ <div :class="[controlClasses, value?.toString() === option.value.toString() ? classesFilled : classesEmpty].join(' ')">
9
+ <div :class="classes" />
10
+ <input class="appearance-none" type="radio" :name="name?.toString()" :value="option.value.toString()" :checked="value?.toString() === option.value.toString()" v-bind="$attrs" @change="$emit('change', option.value, $event)">
11
+ </div>
12
+ <span :class="optionClasses">{{ option.label }}</span>
13
+ </label>
14
+ </div>
15
+ <template v-if="error && error.length > 0">
16
+ <span v-for="(e, i) in error" :key="i" :class="errorClasses">
17
+ {{ e }}
18
+ </span>
19
+ </template>
20
+ </div>
21
+ </template>
22
+
23
+ <script lang="ts" setup generic="DataType extends FormDataType">
24
+ import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
25
+ import { baseClasses } from '../defaults/radioButtonGroup';
26
+
27
+ withDefaults(defineProps<FormFieldProps<DataType> & { class?: string }>(), {
28
+ classes: baseClasses.classes,
29
+ controlClasses: baseClasses.controlClasses,
30
+ optionClasses: baseClasses.optionClasses,
31
+ labelClasses: baseClasses.labelClasses,
32
+ classesFilled: baseClasses.classesFilled,
33
+ classesEmpty: baseClasses.classesEmpty,
34
+ wrapperClasses: baseClasses.wrapperClasses,
35
+ labelWrapperClasses: baseClasses.labelWrapperClasses,
36
+ errorClasses: baseClasses.errorClasses,
37
+ arrangement: 'horizontal',
38
+ })
39
+
40
+ defineEmits<FormFieldEmits>()
41
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div />
3
+ </template>
4
+
5
+ <script lang="ts" setup generic="DataType extends FormDataType">
6
+ import type { FormDataType } from '../../types/form';
7
+
8
+ // TODO: Find appropriate component
9
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <label :class="wrapperClasses">
3
+ <span :class="labelClasses">
4
+ <slot name="label" /><template v-if="required">*</template>
5
+ </span>
6
+ <div :class="combinedClasses">
7
+ <KonomaTag
8
+ v-for="(value, i) in values"
9
+ :key="i"
10
+ :title="value.toString()"
11
+ icon-right-name="heroicons:x-mark-16-solid"
12
+ @click="$emit('change', value)"
13
+ />
14
+ <KonomaTag
15
+ :title="addTagTitle"
16
+ icon-left-name="heroicons:plus-16-solid"
17
+ wrapper-classes="flex flex-row h-6 cursor-pointer items-center justify-center gap-1 rounded-krc-tag-list-add border border-secondary-300 px-3 py-1 bg-white"
18
+ @click="$emit('change', '')"
19
+ />
20
+ </div>
21
+ <template v-if="error && error.length > 0">
22
+ <span v-for="(e, i) in error" :key="i" :class="errorClasses">
23
+ {{ e }}
24
+ </span>
25
+ </template>
26
+ </label>
27
+ </template>
28
+
29
+ <script lang="ts" setup generic="DataType extends FormDataType">
30
+ import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
31
+ import { baseClasses } from '../defaults/tagList';
32
+ import KonomaTag from '../ui/KonomaTag.vue';
33
+
34
+ const props = withDefaults(defineProps<FormFieldProps<DataType>>(), {
35
+ wrapperClasses: baseClasses.wrapperClasses,
36
+ labelClasses: baseClasses.labelClasses,
37
+ errorClasses: baseClasses.errorClasses,
38
+ classesError: baseClasses.classesError,
39
+ classesNeutral: baseClasses.classesNeutral,
40
+ addTagTitle: '',
41
+ })
42
+
43
+ defineEmits<FormFieldEmits>()
44
+
45
+ const combinedClasses = computed(() => {
46
+ const classesFull = [props.classes];
47
+ if (props.error && props.error.length > 0) {
48
+ classesFull.push(props.classesError);
49
+ } else {
50
+ classesFull.push(props.classesNeutral);
51
+ }
52
+ classesFull.push(props.class);
53
+ return classesFull;
54
+ })
55
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <label :class="wrapperClasses">
3
+ <div :class="labelWrapperClasses">
4
+ <span :class="labelClasses">
5
+ <slot name="label" /><template v-if="required">*</template>
6
+ </span>
7
+ <span v-if="maxLength" :class="hintClasses">{{ maxLengthLabel }}</span>
8
+ </div>
9
+ <div class="relative">
10
+ <!-- TODO: Resizable -->
11
+ <textarea
12
+ ref="textareaRef"
13
+ :maxlength="maxLength"
14
+ :value="text"
15
+ :disabled="disabled"
16
+ :name="name?.toString()"
17
+ v-bind="$attrs"
18
+ :class="controlClasses"
19
+ @input="$event => $emit('change', ($event.target as HTMLTextAreaElement).value, $event)"
20
+ @focus="isFocused = true"
21
+ @blur="isFocused = false"
22
+ />
23
+ </div>
24
+ </label>
25
+ </template>
26
+
27
+ <script lang="ts" setup generic="DataType extends FormDataType">
28
+ import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
29
+ import { baseClasses } from '../defaults/textarea';
30
+
31
+ const props = withDefaults(defineProps<FormFieldProps<DataType>>(), {
32
+ classes: baseClasses.classes,
33
+ classesNeutral: baseClasses.classesNeutral,
34
+ classesError: baseClasses.classesError,
35
+ errorClasses: baseClasses.errorClasses,
36
+ labelClasses: baseClasses.labelClasses,
37
+ hintClasses: baseClasses.hintClasses,
38
+ wrapperClasses: baseClasses.wrapperClasses,
39
+ resizeClasses: baseClasses.resizeClasses,
40
+ resizeIconClasses: baseClasses.resizeIconClasses,
41
+ controlClasses: baseClasses.controlClasses,
42
+ labelWrapperClasses: baseClasses.labelWrapperClasses,
43
+ classesDisabled: baseClasses.classesDisabled,
44
+ initialHeight: 100,
45
+ maxLength: 150,
46
+ })
47
+
48
+ defineEmits<FormFieldEmits>()
49
+ // const height = ref(props.initialHeight)
50
+ const isFocused = ref(false)
51
+ const textareaRef = useTemplateRef('textareaRef')
52
+
53
+ // const combinedClasses = computed(() => {
54
+ // const classesFull = [];
55
+
56
+ // if (props.disabled) {
57
+ // classesFull.push(props.classesDisabled);
58
+ // } else {
59
+ // classesFull.push(props.classes);
60
+ // }
61
+ // if (props.error && props.error.length > 0) {
62
+ // classesFull.push(props.classesError);
63
+ // } else {
64
+ // classesFull.push(props.classesNeutral);
65
+ // }
66
+ // return classesFull;
67
+ // })
68
+
69
+ const text = computed(() => {
70
+ let t = props.value?.toString() || '';
71
+ if (!isFocused.value) {
72
+ if (!props.replacements) {
73
+ return t;
74
+ }
75
+ Object.entries(props.replacements).forEach(([key, value]) => {
76
+ t = t.replace(new RegExp(`{${key}}`, 'g'), value);
77
+ });
78
+ }
79
+ return t;
80
+ })
81
+ </script>
@@ -0,0 +1,8 @@
1
+ import type { InjectionKey } from 'vue';
2
+
3
+ export function formInjectionKeys<DataType>() {
4
+ return {
5
+ errors: Symbol('errors') as InjectionKey<Record<keyof DataType, string[]>>,
6
+ updateErrors: Symbol('updateErrors') as InjectionKey<(newErrors: Partial<Record<keyof DataType, string[]>>) => void>,
7
+ }
8
+ }
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <Dropdown>
3
+ <div :class="wrapperClasses">
4
+ <div :class="headerClasses">
5
+ {{ columnsLabel }}
6
+ </div>
7
+ <div :class="columnsWrapperClasses">
8
+ <KonomaColumnChooserEntry
9
+ v-for="col, i in columns.filter(col => col.title)"
10
+ :id="col.id"
11
+ :key="i"
12
+ :title="col.title?.toString() || ''"
13
+ :visible-columns="visibleColumns"
14
+ :entry-classes="entryClasses"
15
+ :visible-column-classes="visibleColumnClasses"
16
+ :hidden-column-classes="hiddenColumnClasses"
17
+ @update:columns="update"
18
+ />
19
+ </div>
20
+ </div>
21
+ </Dropdown>
22
+ </template>
23
+
24
+ <script lang="ts" setup generic="DataType">
25
+ import type { TableColumn } from '../../types/table';
26
+ import { Dropdown } from 'floating-vue';
27
+ import { baseClasses } from '../defaults/columnChooser';
28
+ import KonomaColumnChooserEntry from './KonomaColumnChooserEntry.vue';
29
+
30
+ const props = withDefaults(defineProps<{
31
+ columns: TableColumn<DataType>[]
32
+ wrapperClasses?: string
33
+ headerClasses?: string
34
+ columnsWrapperClasses?: string
35
+ visibleColumnClasses?: string
36
+ hiddenColumnClasses?: string
37
+ entryClasses?: string
38
+ columnsLabel?: string
39
+ }>(), {
40
+ wrapperClasses: baseClasses.wrapperClasses,
41
+ headerClasses: baseClasses.headerClasses,
42
+ columnsWrapperClasses: baseClasses.columnsWrapperClasses,
43
+ entryClasses: baseClasses.entryClasses,
44
+ visibleColumnClasses: baseClasses.visibleColumnClasses,
45
+ hiddenColumnClasses: baseClasses.hiddenColumnClasses,
46
+ })
47
+
48
+ const emit = defineEmits<{
49
+ (e: 'update:columns', column: TableColumn<DataType>): void
50
+ (e: 'getFloatingProps'): Record<string, unknown>
51
+ }>()
52
+
53
+ const visibleColumns = computed(() => {
54
+ return props.columns.filter(column => !column.hidden).map(column => column.id);
55
+ });
56
+
57
+ function update(id: keyof DataType) {
58
+ const columnToUpdate = props.columns.find(column => column.id === id);
59
+ if (!columnToUpdate) {
60
+ return;
61
+ }
62
+ emit('update:columns', { ...columnToUpdate, hidden: !columnToUpdate?.hidden });
63
+ }
64
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <span :class="[entryClasses, visibleColumns.includes(id) ? visibleColumnClasses : hiddenColumnClasses].join(' ')" @click="$emit('update:columns', id)" />
3
+ </template>
4
+
5
+ <script lang="ts" setup generic="DataType">
6
+ defineProps<{
7
+ title: string
8
+ id: keyof DataType
9
+ entryClasses: string
10
+ visibleColumnClasses: string
11
+ hiddenColumnClasses: string
12
+ visibleColumns: (keyof DataType)[]
13
+ }>()
14
+
15
+ defineEmits<{
16
+ (e: 'update:columns', id: keyof DataType): void
17
+ }>()
18
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div v-if="showButtons">
3
+ <div :class="resultsClasses">
4
+ <span :class="resultsTextClasses">
5
+ {{ xToY }}
6
+ </span>
7
+ </div>
8
+ <div v-if="totalPages > 1" :class="controlClasses">
9
+ <KonomaIcon :class="previousPageActive ? activeIconClasses : inactiveIconClasses" name="lucide:chevron-first" @click="previousPageActive ? emit('onFirstPage') : undefined" />
10
+ <KonomaIcon :class="previousPageActive ? activeIconClasses : inactiveIconClasses" name="lucide:chevron-left" @click="previousPageActive ? emit('onPreviousPage') : undefined" />
11
+ <div class="w-16">
12
+ <KonomaInput centered :value="pageInternal" @change="pageInternal = +$event" @key-down="onKeyDown($event)" />
13
+ </div>
14
+ <KonomaIcon :class="nextPageActive ? activeIconClasses : inactiveIconClasses" name="lucide:chevron-right" @click="nextPageActive ? emit('onNextPage') : undefined" />
15
+ <KonomaIcon :class="nextPageActive ? activeIconClasses : inactiveIconClasses" name="lucide:chevron-last" @click="nextPageActive ? emit('onLastPage') : undefined" />
16
+ </div>
17
+ </div>
18
+ <div v-else :class="wrapperClasses">
19
+ <div :class="resultsClasses">
20
+ <span :class="resultsTextClasses">
21
+ {{ xToY }}
22
+ </span>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script lang="ts" setup>
28
+ import { baseClasses } from '../defaults/pagination';
29
+ import KonomaInput from '../form/KonomaInput.vue';
30
+ import KonomaIcon from '../ui/KonomaIcon.vue';
31
+
32
+ const props = withDefaults(defineProps<{
33
+ currentLoaded: number
34
+ currentStart: number
35
+ currentEnd: number
36
+ currentTotal: number
37
+ currentPage: number
38
+ totalPages: number
39
+ inactiveIconClasses?: string
40
+ activeIconClasses?: string
41
+ wrapperClasses?: string
42
+ resultsClasses?: string
43
+ resultsTextClasses?: string
44
+ controlClasses?: string
45
+ xToY: string
46
+ showButtons: boolean
47
+ }>(), {
48
+ wrapperClasses: baseClasses.wrapperClasses,
49
+ resultsClasses: baseClasses.resultsClasses,
50
+ resultsTextClasses: baseClasses.resultsTextClasses,
51
+ activeIconClasses: baseClasses.activeIconClasses,
52
+ inactiveIconClasses: baseClasses.inactiveIconClasses,
53
+ controlClasses: baseClasses.controlClasses,
54
+ })
55
+
56
+ const emit = defineEmits<{
57
+ (e: 'onFirstPage'): void
58
+ (e: 'onPreviousPage'): void
59
+ (e: 'onNextPage'): void
60
+ (e: 'onLastPage'): void
61
+ (e: 'toPage', page: number): void
62
+ }>()
63
+
64
+ const previousPageActive = computed(() => {
65
+ return props.currentPage !== 1;
66
+ });
67
+
68
+ const nextPageActive = computed(() => {
69
+ return props.currentPage !== props.totalPages;
70
+ });
71
+
72
+ const pageInternal = ref(props.currentPage);
73
+
74
+ function onKeyDown(event: KeyboardEvent) {
75
+ switch (event.key) {
76
+ case 'Enter':
77
+ emit('toPage', pageInternal.value);
78
+ break;
79
+ }
80
+ }
81
+ </script>