@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.
- package/.nuxtrc +1 -0
- package/.playground/app.vue +64 -0
- package/.playground/eslint.config.ts +3 -0
- package/.playground/nuxt.config.ts +9 -0
- package/.tool-versions +1 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +12 -0
- package/README.md +70 -0
- package/app.config.ts +13 -0
- package/components/KonomaTheme.vue +92 -0
- package/components/defaults/button.ts +19 -0
- package/components/defaults/checkbox.ts +13 -0
- package/components/defaults/checkboxList.ts +9 -0
- package/components/defaults/columnChooser.ts +8 -0
- package/components/defaults/input.ts +16 -0
- package/components/defaults/pagination.ts +10 -0
- package/components/defaults/radioButtonGroup.ts +13 -0
- package/components/defaults/select.ts +20 -0
- package/components/defaults/table.ts +16 -0
- package/components/defaults/tabs.ts +10 -0
- package/components/defaults/tag.ts +7 -0
- package/components/defaults/tagList.ts +14 -0
- package/components/defaults/textarea.ts +16 -0
- package/components/form/KonomaCheckbox.vue +68 -0
- package/components/form/KonomaCheckboxList.vue +42 -0
- package/components/form/KonomaForm.vue +71 -0
- package/components/form/KonomaFormField.vue +36 -0
- package/components/form/KonomaInput.vue +92 -0
- package/components/form/KonomaPhoneInput.vue +9 -0
- package/components/form/KonomaRadioButtonGroup.vue +41 -0
- package/components/form/KonomaSelect.vue +9 -0
- package/components/form/KonomaTagList.vue +55 -0
- package/components/form/KonomaTextarea.vue +81 -0
- package/components/form/injectionKeys.ts +8 -0
- package/components/table/KonomaColumnChooser.vue +64 -0
- package/components/table/KonomaColumnChooserEntry.vue +18 -0
- package/components/table/KonomaPagination.vue +81 -0
- package/components/table/KonomaTable.vue +355 -0
- package/components/table/KonomaTableActionEntry.vue +27 -0
- package/components/table/KonomaTableActions.vue +32 -0
- package/components/ui/KonomaButton.vue +109 -0
- package/components/ui/KonomaIcon.vue +13 -0
- package/components/ui/KonomaLoadingIndicator.vue +14 -0
- package/components/ui/KonomaModal.vue +120 -0
- package/components/ui/KonomaTabs.vue +70 -0
- package/components/ui/KonomaTag.vue +49 -0
- package/composables/useKonomaTheme.ts +5 -0
- package/eslint.config.ts +43 -0
- package/index.d.ts +33 -0
- package/nuxt.config.ts +20 -0
- package/package.json +60 -0
- package/tsconfig.json +11 -0
- package/types/form.ts +149 -0
- package/types/table.ts +33 -0
- package/unocss.config.ts +83 -0
package/.nuxtrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
typescript.includeWorkspace=true
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col gap-2">
|
|
3
|
+
<KonomaTheme :unocss-config="unocss" />
|
|
4
|
+
<KonomaButton variant="primary" label="Change color" @click="switchColor" />
|
|
5
|
+
<KonomaTag title="Test Tag" />
|
|
6
|
+
<KonomaTabs
|
|
7
|
+
:active="activeTab"
|
|
8
|
+
:tabs="[
|
|
9
|
+
{
|
|
10
|
+
count: 5,
|
|
11
|
+
id: 0,
|
|
12
|
+
label: 'First',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
count: 10,
|
|
16
|
+
id: 1,
|
|
17
|
+
label: 'Second',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
count: 15,
|
|
21
|
+
id: 2,
|
|
22
|
+
label: 'Third',
|
|
23
|
+
},
|
|
24
|
+
]"
|
|
25
|
+
@click="(tab) => activeTab = tab"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import KonomaTheme from '../components/KonomaTheme.vue';
|
|
32
|
+
import KonomaButton from '../components/ui/KonomaButton.vue';
|
|
33
|
+
import KonomaTabs from '../components/ui/KonomaTabs.vue';
|
|
34
|
+
import KonomaTag from '../components/ui/KonomaTag.vue';
|
|
35
|
+
import unocss from '../unocss.config';
|
|
36
|
+
|
|
37
|
+
const activeTab = ref(0);
|
|
38
|
+
|
|
39
|
+
function switchColor() {
|
|
40
|
+
console.warn('Color changed!');
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
:root {
|
|
46
|
+
--kvc-table-header-backgroundColor: rgb(228 229 226);
|
|
47
|
+
--kvc-table-borderRadius: 1rem;
|
|
48
|
+
--kvc-select-borderRadius: 1rem;
|
|
49
|
+
--kvc-button-borderRadius: 0.375rem;
|
|
50
|
+
--kvc-modal-borderRadius: 0.5rem;
|
|
51
|
+
--kvc-tag-borderRadius: ;
|
|
52
|
+
--kvc-tagList-borderRadius: ;
|
|
53
|
+
--kvc-tagListAdd-borderRadius: ;
|
|
54
|
+
--kvc-input-borderRadius: 0.375rem;
|
|
55
|
+
--kvc-phoneInput-borderRadius: 0.375rem;
|
|
56
|
+
--kvc-checkbox-borderRadius: ;
|
|
57
|
+
--kvc-textarea-borderRadius: 0.375rem;
|
|
58
|
+
--kvc-radiobuttonGroup-borderRadius: 100%;
|
|
59
|
+
--kvc-table-borderRadius: 0.5rem;
|
|
60
|
+
--kvc-tableColumnChooser-borderRadius: 0.375rem;
|
|
61
|
+
--kvc-tableActions-borderRadius: 0.375rem;
|
|
62
|
+
--kvc-table-header: rgb(228 229 226);
|
|
63
|
+
}
|
|
64
|
+
</style>
|
package/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs 22.14.0
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Konoma Vue Components
|
|
2
|
+
|
|
3
|
+
A collection of Vue 3 components and composables for building web applications, designed to be used as a Nuxt layer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Konoma Vue Components](#konoma-vue-components)
|
|
10
|
+
- [Table of Contents](#table-of-contents)
|
|
11
|
+
- [Related Resources](#related-resources)
|
|
12
|
+
- [Deployment](#deployment)
|
|
13
|
+
- [Runtime](#runtime)
|
|
14
|
+
- [Development](#development)
|
|
15
|
+
- [Prerequisites](#prerequisites)
|
|
16
|
+
- [Configuration](#configuration)
|
|
17
|
+
- [Running the Application](#running-the-application)
|
|
18
|
+
- [Code Style \& Linting](#code-style--linting)
|
|
19
|
+
- [Git](#git)
|
|
20
|
+
- [Important Concepts](#important-concepts)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Related Resources
|
|
25
|
+
|
|
26
|
+
## Deployment
|
|
27
|
+
|
|
28
|
+
- Push changes to develop branch
|
|
29
|
+
- Create new release in GitHub
|
|
30
|
+
- A pipeline will be triggered that builds and publishes the code to the npm registry
|
|
31
|
+
- Use as a Nuxt layer in your Nuxt projects
|
|
32
|
+
|
|
33
|
+
## Runtime
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
### Prerequisites
|
|
38
|
+
|
|
39
|
+
To run the application locally, you need to have the following installed:
|
|
40
|
+
|
|
41
|
+
- [Node.js](https://nodejs.org/) (version 22 or higher)
|
|
42
|
+
- [Yarn](https://yarnpkg.com/) (version 4 or higher)
|
|
43
|
+
|
|
44
|
+
### Configuration
|
|
45
|
+
|
|
46
|
+
### Running the Application
|
|
47
|
+
|
|
48
|
+
Explain how to run the application locally. Include commands and any additional steps needed to start the development server.
|
|
49
|
+
|
|
50
|
+
To run the application, use the following command:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
yarn dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
|
|
57
|
+
|
|
58
|
+
### Code Style & Linting
|
|
59
|
+
|
|
60
|
+
- We use ESLint for linting and formatting.
|
|
61
|
+
- If possible, use the corresponding extensions in your IDE to automatically lint and format the code on save.
|
|
62
|
+
- Run `yarn lint` to check for linting issues and format the code.
|
|
63
|
+
|
|
64
|
+
### Git
|
|
65
|
+
|
|
66
|
+
- Generally all development happens on the `develop` branch.
|
|
67
|
+
- If we introduce breaking changes, create a new branch from `develop` and name it according to the old version (e.g. `legacy/0.1.*`).
|
|
68
|
+
- This way we are able to still support older versions if needed.
|
|
69
|
+
|
|
70
|
+
## Important Concepts
|
package/app.config.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import type { UserConfig } from 'unocss';
|
|
7
|
+
import { createGenerator } from 'unocss';
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
unocssConfig: UserConfig<object>
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const requiredThemeShades = [
|
|
14
|
+
'50',
|
|
15
|
+
'100',
|
|
16
|
+
'150',
|
|
17
|
+
'200',
|
|
18
|
+
'250',
|
|
19
|
+
'300',
|
|
20
|
+
'350',
|
|
21
|
+
'400',
|
|
22
|
+
'450',
|
|
23
|
+
'500',
|
|
24
|
+
'550',
|
|
25
|
+
'600',
|
|
26
|
+
'650',
|
|
27
|
+
'700',
|
|
28
|
+
'750',
|
|
29
|
+
'800',
|
|
30
|
+
'850',
|
|
31
|
+
'900',
|
|
32
|
+
'950',
|
|
33
|
+
'DEFAULT',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
const appConfig = useAppConfig();
|
|
37
|
+
const requiredThemeColors = Object.keys(appConfig['konoma-theme']);
|
|
38
|
+
|
|
39
|
+
interface UnocssThemeColors {
|
|
40
|
+
[key: string]: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type AppConfigThemeKey = keyof (typeof appConfig)['konoma-theme'];
|
|
44
|
+
|
|
45
|
+
async function initTheme(): Promise<Record<AppConfigThemeKey, Record<keyof typeof requiredThemeShades, string>>> {
|
|
46
|
+
const ctx = await createGenerator(props.unocssConfig);
|
|
47
|
+
const unocssThemeColors = (ctx.config.theme as { colors: UnocssThemeColors }).colors;
|
|
48
|
+
|
|
49
|
+
const result = Object.fromEntries(requiredThemeColors.map(requiredThemeColorName => [requiredThemeColorName, {}])) as Record<
|
|
50
|
+
AppConfigThemeKey,
|
|
51
|
+
Record<keyof typeof requiredThemeShades, string>
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
for (const requiredThemeColorName of requiredThemeColors) {
|
|
55
|
+
const effectiveColorName = appConfig['konoma-theme'][requiredThemeColorName as AppConfigThemeKey];
|
|
56
|
+
const unocssColorObject = unocssThemeColors[effectiveColorName];
|
|
57
|
+
if (!unocssColorObject) {
|
|
58
|
+
throw new Error(`Theme color "${effectiveColorName}" not found in UnoCSS config`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof unocssColorObject === 'string') {
|
|
62
|
+
throw new TypeError(`Theme color "${effectiveColorName}" is not an object`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const unocssColorKeys = Object.keys(unocssColorObject);
|
|
66
|
+
const missingShades = requiredThemeShades.filter(shade => !unocssColorKeys.includes(shade));
|
|
67
|
+
if (missingShades.length) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Theme color "${effectiveColorName}" is missing "${missingShades.join('", "')}" ${missingShades.length > 1 ? 'shades' : 'shade'}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
result[requiredThemeColorName as AppConfigThemeKey] = unocssColorObject as Record<keyof typeof requiredThemeShades, string>;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const theme = await initTheme();
|
|
78
|
+
|
|
79
|
+
useHead(() => ({
|
|
80
|
+
bodyAttrs: {
|
|
81
|
+
style: Object.entries(theme).reduce(
|
|
82
|
+
(acc, [colorName, colorShades]) => {
|
|
83
|
+
Object.entries(colorShades).forEach(([shade, value]) => {
|
|
84
|
+
acc[`--konoma-${colorName}-${shade}`] = value;
|
|
85
|
+
});
|
|
86
|
+
return acc;
|
|
87
|
+
},
|
|
88
|
+
{} as Record<string, string>,
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const baseClasses = {
|
|
2
|
+
classesBase:
|
|
3
|
+
'w-full rounded-kvc-button py-2.5 px-3.5 h-10 text-sm font-semibold shadow-sm inline-flex justify-center items-center flex flex-row gap-2 cursor-pointer disabled:!cursor-not-allowed disabled:!opacity-50 disabled:!shadow-none disabled:!ring-0 disabled:!bg-secondary-200 disabled:!text-secondary-500',
|
|
4
|
+
classesPrimary:
|
|
5
|
+
'bg-primary-600 border border-primary-600 text-white hover:bg-primary-700 focus:ring-2 focus:ring-primary-600 focus:ring-offset-2',
|
|
6
|
+
classesSecondary: 'bg-white border border-secondary-200 hover:bg-secondary-100 focus:ring-2 focus:ring-primary-600 focus:ring-offset-2',
|
|
7
|
+
classesActiveSecondary:
|
|
8
|
+
'bg-secondary-100 border border-secondary-100 hover:bg-secondary-200 text-secondary-900 focus:ring-2 focus:ring-primary-600 focus:ring-offset-2',
|
|
9
|
+
classesError: 'bg-error-500 border border-error-600 text-white hover:bg-error-700 focus:ring-2 focus:ring-error-500 focus:ring-offset-2',
|
|
10
|
+
classesAlert: 'bg-alert-500 border border-alert-600 text-white hover:bg-alert-700 focus:ring-2 focus:ring-alert-500 focus:ring-offset-2',
|
|
11
|
+
loadingClassesBase: 'h-6 w-6 animate-spin',
|
|
12
|
+
loadingClassesPrimary: 'text-primary-600 fill-white',
|
|
13
|
+
loadingClassesSecondary: 'text-secondary-200',
|
|
14
|
+
loadingClassesActiveSecondary: 'text-secondary-200',
|
|
15
|
+
loadingClassesError: 'text-error-200',
|
|
16
|
+
loadingClassesAlert: 'text-alert-200',
|
|
17
|
+
iconLeftClasses: 'h-6 w-6',
|
|
18
|
+
iconRightClasses: 'h-6 w-6',
|
|
19
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
classesFilled:
|
|
5
|
+
'flex h-4 w-4 flex-row items-center justify-center rounded-kvc-checkbox border border-primary-600 bg-primary-600 shadow-sm outline-offset-2 group-hover:border-primary-700 group-hover:bg-primary-700',
|
|
6
|
+
classesEmpty:
|
|
7
|
+
'flex flex-row items-center justify-center h-4 w-4 rounded-kvc-checkbox border border-secondary-300 bg-white shadow-sm outline-offset-2 outline-primary-800 group-hover:border-secondary-400 group-active:outline-solid',
|
|
8
|
+
classesError: 'h-4 w-4 rounded-kvc-checkbox border border-error-500 bg-error-100 shadow-sm outline-offset-2',
|
|
9
|
+
labelClassesFilled: 'ml-2 text-sm font-medium text-secondary-900',
|
|
10
|
+
labelClassesError: 'ml-2 text-sm font-medium text-error-600',
|
|
11
|
+
labelClassesEmpty: 'ml-2 text-sm font-medium text-secondary-900',
|
|
12
|
+
iconClassesFilled: 'h-3 w-3 text-white',
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
labelClasses: 'text-sm font-medium text-secondary-900',
|
|
5
|
+
wrapperClasses: 'flex gap-4',
|
|
6
|
+
classes: 'flex flex-row gap-5',
|
|
7
|
+
classesError: '!text-error-500',
|
|
8
|
+
errorClasses: 'text-sm text-error-500',
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const baseClasses = {
|
|
2
|
+
wrapperClasses: 'flex flex-col rounded-kvc-table-column-chooser bg-white px-4 py-3 shadow-sm z-1',
|
|
3
|
+
headerClasses: 'h-8',
|
|
4
|
+
columnsWrapperClasses: 'ml-2 mt-3 flex max-h-100 flex-col gap-1 overflow-y-auto',
|
|
5
|
+
entryClasses: 'cursor-pointer px-4 py-2 text-sm font-medium text-secondary-600',
|
|
6
|
+
visibleColumnClasses: 'bg-primary-100',
|
|
7
|
+
hiddenColumnClasses: 'bg-white',
|
|
8
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
classes:
|
|
5
|
+
'w-full h-10 rounded-kvc-input px-3 py-2 outline-hidden placeholder:text-secondary-500 placeholder:text-sm text-secondary-900 text-sm disabled:pointer-events-none not-disabled:bg-white disabled:bg-secondary-50',
|
|
6
|
+
wrapperClasses: 'group flex flex-col gap-1',
|
|
7
|
+
labelClasses: 'flex flex-row justify-start text-sm font-medium text-secondary-900',
|
|
8
|
+
iconLeftClasses: 'absolute bottom-0 left-3 top-0 my-auto h-5 w-5 text-secondary-300',
|
|
9
|
+
iconRightClasses: 'h-5 w-5',
|
|
10
|
+
wrapperRightClasses: 'absolute bottom-0 right-3 top-0 my-auto flex flex-row items-center gap-2 text-secondary-300 text-sm',
|
|
11
|
+
errorClasses: 'text-sm text-error-500',
|
|
12
|
+
classesError: 'ring-error-500 ring-2',
|
|
13
|
+
classesNeutral: 'border-secondary-300 border focus:ring-2 hover:border-secondary-400 focus:ring-primary-900',
|
|
14
|
+
additionalClassesIconLeft: 'pl-10',
|
|
15
|
+
additionalClassesIconRight: 'pr-12',
|
|
16
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PaginationClasses } from '../../types/table';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: PaginationClasses = {
|
|
4
|
+
activeIconClasses: 'w-8 p-2 cursor-pointer rounded-full bg-white',
|
|
5
|
+
inactiveIconClasses: 'w-8 p-2 cursor-not-allowed text-secondary-500',
|
|
6
|
+
wrapperClasses: 'flex flex-row justify-between p-4',
|
|
7
|
+
resultsClasses: 'flex flex-row gap-1 text-sm',
|
|
8
|
+
resultsTextClasses: 'font-medium',
|
|
9
|
+
controlClasses: 'flex flex-row items-center gap-4 pb-2',
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
classes: 'absolute left-[5px] top-[5px] h-1.5 w-1.5 rounded-full bg-white',
|
|
5
|
+
controlClasses: 'h-4 w-4 min-w-4 rounded-krc-radiobutton-group relative',
|
|
6
|
+
wrapperClasses: 'flex flex-col gap-2',
|
|
7
|
+
classesFilled: 'bg-primary-600',
|
|
8
|
+
labelClasses: 'text-sm font-medium text-secondary-900',
|
|
9
|
+
labelWrapperClasses: 'flex flex-row items-center gap-2',
|
|
10
|
+
classesEmpty: 'border border-secondary-300 bg-white',
|
|
11
|
+
errorClasses: 'text-sm text-error-500',
|
|
12
|
+
optionClasses: 'text-sm',
|
|
13
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]: string } = {
|
|
4
|
+
classes: 'rounded-kvc-select h-10 py-0 border shadow-none',
|
|
5
|
+
errorClasses: 'text-sm text-error-500',
|
|
6
|
+
labelClasses: 'text-sm font-medium text-secondary-900',
|
|
7
|
+
labelWrapperClasses: 'group flex flex-col gap-1',
|
|
8
|
+
classesNeutral: 'border-secondary-300 group-hover:border-secondary-400',
|
|
9
|
+
classesError: 'ring-error-500 ring-2',
|
|
10
|
+
focusClasses: 'ring-2 ring-primary-900',
|
|
11
|
+
controlClasses: 'h-8 rounded-kvc-select py-0 text-secondary-900 text-sm border-none outline-hidden',
|
|
12
|
+
optionClasses: 'bg-white text-sm',
|
|
13
|
+
optionSelectedClasses: 'bg-primary-500 text-sm',
|
|
14
|
+
optionFocusedClasses: 'bg-primary-200 text-sm',
|
|
15
|
+
valueClasses: 'text-secondary-900 text-sm',
|
|
16
|
+
placeholderClasses: 'text-secondary-500 text-sm',
|
|
17
|
+
wrapperClasses: 'w-full h-auto',
|
|
18
|
+
indicatorClasses: 'text-secondary-500',
|
|
19
|
+
valueContainerClasses: '',
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const baseClasses = {
|
|
2
|
+
wrapperClasses: 'relative h-full',
|
|
3
|
+
tableClasses:
|
|
4
|
+
'max-h-full max-w-full overflow-x-auto overflow-y-auto rounded-kvc-table outline-1 outline-solid outline-secondary-300 divide-y divide-secondary-200',
|
|
5
|
+
headerClasses:
|
|
6
|
+
'flex flex-col items-start truncate bg-kvc-table-header justify-center px-4 py-3 text-xs font-medium first:rounded-tl-kvc-table gap-2',
|
|
7
|
+
columnsWrapperClasses: 'flex flex-row overflow-x-auto overflow-y-auto h-full',
|
|
8
|
+
columnsLeftClasses: 'flex flex-row h-fit overflow-x-auto border-r first:rounded-tl-kvc-table last:rounded-tr-kvc-table',
|
|
9
|
+
columnsCenterClasses: 'flex grow flex-row h-fit overflow-x-auto first:rounded-tl-kvc-table last:rounded-tr-kvc-table',
|
|
10
|
+
columnsRightClasses: 'flex flex-row h-fit overflow-x-auto border-l first:rounded-tl-kvc-table last:rounded-tr-kvc-table',
|
|
11
|
+
noDataClasses: 'flex h-16 items-center justify-start pl-16 rounded-b-kvc-table bg-white text-secondary-500 w-full',
|
|
12
|
+
rowClasses: 'group relative flex flex-row justify-between bg-white last:rounded-b-kvc-table hover:bg-primary-100 h-fit',
|
|
13
|
+
rowLeftWrapperClasses: 'bg-white group-hover:bg-primary-100 h-fit',
|
|
14
|
+
rowCenterWrapperClasses: 'bg-white first:grow group-hover:bg-primary-100 h-fit',
|
|
15
|
+
rowRightWrapperClasses: 'bg-white group-hover:bg-primary-100 h-fit',
|
|
16
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const baseClasses = {
|
|
2
|
+
wrapperClasses: 'flex flex-row gap-10 pt-4 border-b border-secondary-200',
|
|
3
|
+
tabActiveClasses: '',
|
|
4
|
+
tabInactiveClasses: 'text-secondary-600',
|
|
5
|
+
countActiveClasses: 'bg-tertiary-300',
|
|
6
|
+
countInactiveClasses: 'bg-secondary-200',
|
|
7
|
+
tabBaseClasses: 'h-12 px-1 text-sm font-semibold text-primary-900 cursor-pointer flex flex-row gap-2 items-center relative',
|
|
8
|
+
countBaseClasses: 'rounded-full px-3',
|
|
9
|
+
activeMarkerClasses: 'absolute -bottom-0.5 left-0 h-0.5 w-full bg-primary-600',
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const baseClasses = {
|
|
2
|
+
wrapperClasses:
|
|
3
|
+
'flex flex-row h-6 cursor-pointer items-center justify-center gap-1 rounded-kvc-tag border border-secondary-300 px-3 py-1 bg-primary-100 text-secondary-900',
|
|
4
|
+
titleClasses: 'text-center text-xs font-medium',
|
|
5
|
+
iconLeftClasses: 'h-4 w-4',
|
|
6
|
+
iconRightClasses: 'h-4 w-4',
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
classes: 'w-full flex flex-row gap-2 flex-wrap items-center rounded-kvc-tag-list p-4 bg-white',
|
|
5
|
+
wrapperClasses: 'group flex flex-col gap-1',
|
|
6
|
+
labelClasses: 'flex flex-row justify-start text-sm font-medium text-secondary-900',
|
|
7
|
+
iconLeftClasses: 'absolute bottom-0 left-3 top-0 my-auto h-5 w-5 text-secondary-300',
|
|
8
|
+
iconRightClasses: 'absolute bottom-0 right-3 top-0 my-auto h-5 w-5 text-secondary-300',
|
|
9
|
+
errorClasses: 'text-sm text-error-500',
|
|
10
|
+
classesError: 'ring-error-500 ring-2',
|
|
11
|
+
classesNeutral: 'border-secondary-300 border',
|
|
12
|
+
additionalClassesIconLeft: 'pl-10',
|
|
13
|
+
additionalClassesIconRight: 'pr-10',
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Classes } from '../../types/form';
|
|
2
|
+
|
|
3
|
+
export const baseClasses: { [key in keyof Classes]?: string } = {
|
|
4
|
+
classes: 'w-full bg-white text-sm cursor-text rounded-kvc-textarea p-4 text-secondary-900',
|
|
5
|
+
classesNeutral: 'border border-secondary-300 has-focus:ring-2 hover:border-secondary-400 has-[:focus]:ring-primary-900',
|
|
6
|
+
classesError: 'ring-error-500 ring-2',
|
|
7
|
+
errorClasses: 'text-sm text-error-500',
|
|
8
|
+
labelClasses: 'flex flex-row justify-start text-sm text-secondary-900 font-medium',
|
|
9
|
+
hintClasses: 'text-sm text-secondary-500',
|
|
10
|
+
wrapperClasses: 'flex flex-col gap-1',
|
|
11
|
+
resizeClasses: 'absolute bottom-4 right-4 h-4 w-4 cursor-row-resize text-secondary-300',
|
|
12
|
+
resizeIconClasses: 'h-4 w-4',
|
|
13
|
+
controlClasses: 'h-full w-full resize-none focus-visible:outline-hidden disabled:pointer-events-none disabled:bg-primary-50',
|
|
14
|
+
labelWrapperClasses: 'flex flex-row justify-between',
|
|
15
|
+
classesDisabled: 'w-full text-sm rounded-kvc-textarea p-4 pointer-events-none bg-primary-50',
|
|
16
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label v-if="value || indeterminate" :class="`group flex flex-row items-center ${$attrs.class}`">
|
|
3
|
+
<input type="hidden" :value="1">
|
|
4
|
+
<input
|
|
5
|
+
:value="1"
|
|
6
|
+
:indeterminate="indeterminate"
|
|
7
|
+
type="checkbox"
|
|
8
|
+
:name="name?.toString()"
|
|
9
|
+
class="h-0 w-0 appearance-none"
|
|
10
|
+
:checked="!!value"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
v-bind="$attrs"
|
|
13
|
+
@change="$emit('change', ($event.target as HTMLInputElement).checked, $event)"
|
|
14
|
+
>
|
|
15
|
+
<div :class="classesFilled">
|
|
16
|
+
<KonomaIcon :name="indeterminate ? 'heroicons:minus' : 'heroicons:check-16-solid'" :class="iconClassesFilled" />
|
|
17
|
+
</div>
|
|
18
|
+
<span :class="labelClassesFilled">
|
|
19
|
+
<slot name="label" />
|
|
20
|
+
</span>
|
|
21
|
+
</label>
|
|
22
|
+
<label v-else-if="error?.length" :class="`group flex flex-row items-center ${$attrs.class}`">
|
|
23
|
+
<input type="hidden" :name="name?.toString()" :value="0">
|
|
24
|
+
<input
|
|
25
|
+
type="checkbox"
|
|
26
|
+
:name="name?.toString()"
|
|
27
|
+
:checked="!!value"
|
|
28
|
+
class="h-0 w-0 appearance-none"
|
|
29
|
+
:disabled="disabled"
|
|
30
|
+
@change="$emit('change', ($event.target as HTMLInputElement).checked, $event)"
|
|
31
|
+
>
|
|
32
|
+
<div :class="classesError" />
|
|
33
|
+
<span :class="labelClassesError"><slot name="label" /></span>
|
|
34
|
+
</label>
|
|
35
|
+
<label v-else :class="`group flex flex-row items-center ${$attrs.class}`">
|
|
36
|
+
<!-- This allows us to check for errors in CheckboxList -->
|
|
37
|
+
<input type="hidden" :name="name?.toString()" :value="0">
|
|
38
|
+
<input
|
|
39
|
+
type="checkbox"
|
|
40
|
+
:name="name?.toString()"
|
|
41
|
+
:value="0"
|
|
42
|
+
class="h-0 w-0 appearance-none"
|
|
43
|
+
:checked="!!value"
|
|
44
|
+
:disabled="disabled"
|
|
45
|
+
@change="$emit('change', ($event.target as HTMLInputElement).checked, $event)"
|
|
46
|
+
>
|
|
47
|
+
<div :class="classesEmpty" />
|
|
48
|
+
<span :class="labelClassesEmpty"><slot name="label" /></span>
|
|
49
|
+
</label>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script lang="ts" setup generic="DataType extends FormDataType">
|
|
53
|
+
import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
|
|
54
|
+
import { baseClasses } from '../defaults/checkbox';
|
|
55
|
+
import KonomaIcon from '../ui/KonomaIcon.vue';
|
|
56
|
+
|
|
57
|
+
withDefaults(defineProps<FormFieldProps<DataType>>(), {
|
|
58
|
+
classesFilled: baseClasses.classesFilled,
|
|
59
|
+
classesError: baseClasses.classesError,
|
|
60
|
+
classesEmpty: baseClasses.classesEmpty,
|
|
61
|
+
labelClassesFilled: baseClasses.labelClassesFilled,
|
|
62
|
+
labelClassesError: baseClasses.labelClassesError,
|
|
63
|
+
labelClassesEmpty: baseClasses.labelClassesEmpty,
|
|
64
|
+
iconClassesFilled: baseClasses.iconClassesFilled,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
defineEmits<FormFieldEmits>()
|
|
68
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="[wrapperClasses, positionClasses[labelPosition]].join(' ')">
|
|
3
|
+
<span :class="labelClasses">
|
|
4
|
+
<slot name="label" /><template v-if="required">*</template>
|
|
5
|
+
</span>
|
|
6
|
+
<div :class="[classes, error?.length && { classesError }].join(' ')">
|
|
7
|
+
<KonomaCheckbox
|
|
8
|
+
v-for="(option, i) in options"
|
|
9
|
+
:key="i"
|
|
10
|
+
:name="name?.toString()"
|
|
11
|
+
:label="option.label.toString()"
|
|
12
|
+
:error="error"
|
|
13
|
+
v-bind="{ ...$attrs, ...childClasses }"
|
|
14
|
+
:value="values?.includes(option.value)"
|
|
15
|
+
@change=" (_, e) => $emit('change', option.value, e)"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<template v-if="error && error.length > 0">
|
|
19
|
+
<span v-for="(e, i) in error" :key="i" :class="errorClasses">
|
|
20
|
+
{{ e }}
|
|
21
|
+
</span>
|
|
22
|
+
</template>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script lang="ts" setup generic="DataType extends FormDataType">
|
|
27
|
+
import type { FormDataType, FormFieldEmits, FormFieldProps } from '../../types/form';
|
|
28
|
+
import { positionClasses } from '../../types/form';
|
|
29
|
+
import { baseClasses } from '../defaults/checkboxList';
|
|
30
|
+
import KonomaCheckbox from './KonomaCheckbox.vue';
|
|
31
|
+
|
|
32
|
+
withDefaults(defineProps<FormFieldProps<DataType>>(), {
|
|
33
|
+
labelClasses: baseClasses.labelClasses,
|
|
34
|
+
wrapperClasses: baseClasses.wrapperClasses,
|
|
35
|
+
classes: baseClasses.classes,
|
|
36
|
+
classesError: baseClasses.classesError,
|
|
37
|
+
errorClasses: baseClasses.errorClasses,
|
|
38
|
+
labelPosition: 'top',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
defineEmits<FormFieldEmits>()
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<form @submit="submit">
|
|
3
|
+
<slot />
|
|
4
|
+
</form>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script lang="ts" setup generic="DataType extends {
|
|
8
|
+
[key: string]: string | number | boolean | null
|
|
9
|
+
}"
|
|
10
|
+
>
|
|
11
|
+
import { formInjectionKeys } from './injectionKeys';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
validators: Record<keyof DataType, ((value: string | number | boolean | null) => string)[]>
|
|
15
|
+
data: DataType
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(e: 'onValidation', errors: Partial<Record<keyof DataType, string[]>>): void
|
|
20
|
+
(e: 'onSubmit'): void | Promise<void>
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const errors = ref<Partial<Record<keyof DataType, string[]>>>({})
|
|
24
|
+
const submitAttempted = ref(false)
|
|
25
|
+
|
|
26
|
+
function validate() {
|
|
27
|
+
let invalid = false;
|
|
28
|
+
const newErrors: Record<keyof DataType, string[]> = {} as Record<keyof DataType, string[]>;
|
|
29
|
+
|
|
30
|
+
Object.entries(props.data).forEach(([name, value]) => {
|
|
31
|
+
if (!props.validators[name as keyof DataType]) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
newErrors[name as keyof DataType] = props.validators[name as keyof DataType]
|
|
35
|
+
.map((validator) => {
|
|
36
|
+
const validation = validator(value);
|
|
37
|
+
if (validation) {
|
|
38
|
+
invalid = true;
|
|
39
|
+
}
|
|
40
|
+
return validation;
|
|
41
|
+
})
|
|
42
|
+
.filter(v => v);
|
|
43
|
+
});
|
|
44
|
+
errors.value = newErrors;
|
|
45
|
+
emit('onValidation', newErrors);
|
|
46
|
+
if (process.env.NODE_ENV === 'development') {
|
|
47
|
+
console.warn('Form errors', newErrors);
|
|
48
|
+
}
|
|
49
|
+
return invalid;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function submit(event: Event) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const invalid = validate();
|
|
55
|
+
|
|
56
|
+
if (invalid) {
|
|
57
|
+
submitAttempted.value = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
submitAttempted.value = false;
|
|
61
|
+
await emit('onSubmit');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function updateErrors(newErrors: Partial<Record<keyof DataType, string[]>>) {
|
|
65
|
+
errors.value = newErrors;
|
|
66
|
+
emit('onValidation', newErrors);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
provide(formInjectionKeys().errors, errors.value);
|
|
70
|
+
provide(formInjectionKeys().updateErrors, updateErrors);
|
|
71
|
+
</script>
|