@finema/core 2.14.0 → 2.15.0

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 (30) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -1
  3. package/dist/runtime/components/FlexDeck/Base.vue +1 -1
  4. package/dist/runtime/components/Form/Fields.vue +23 -15
  5. package/dist/runtime/components/Form/InputRadio/index.vue +56 -0
  6. package/dist/runtime/components/Form/InputRadio/index.vue.d.ts +11 -0
  7. package/dist/runtime/components/Form/InputRadio/types.d.ts +19 -0
  8. package/dist/runtime/components/Form/InputRadio/types.js +0 -0
  9. package/dist/runtime/components/Form/InputUploadDropzoneAuto/EmptyState.vue +1 -1
  10. package/dist/runtime/components/Form/InputUploadDropzoneAuto/FailedState.vue +1 -1
  11. package/dist/runtime/components/Form/InputUploadDropzoneAuto/useUploadState.js +8 -11
  12. package/dist/runtime/components/Form/InputWYSIWYG/UploadImageForm.vue +38 -0
  13. package/dist/runtime/components/Form/InputWYSIWYG/UploadImageForm.vue.d.ts +11 -0
  14. package/dist/runtime/components/Form/InputWYSIWYG/index.vue +281 -0
  15. package/dist/runtime/components/Form/InputWYSIWYG/index.vue.d.ts +6 -0
  16. package/dist/runtime/components/Form/InputWYSIWYG/types.d.ts +52 -0
  17. package/dist/runtime/components/Form/InputWYSIWYG/types.js +0 -0
  18. package/dist/runtime/components/Form/types.d.ts +3 -1
  19. package/dist/runtime/components/Image.vue +1 -1
  20. package/dist/runtime/components/Table/Base.vue +1 -1
  21. package/dist/runtime/components/Table/index.vue +1 -1
  22. package/dist/runtime/styles/main.css +1 -1
  23. package/dist/runtime/theme/index.d.ts +2 -0
  24. package/dist/runtime/theme/index.js +2 -0
  25. package/dist/runtime/theme/radioGroup.d.ts +6 -0
  26. package/dist/runtime/theme/radioGroup.js +6 -0
  27. package/dist/runtime/theme/uploadFileDropzone.js +4 -4
  28. package/dist/runtime/theme/wysiwyg.d.ts +53 -0
  29. package/dist/runtime/theme/wysiwyg.js +53 -0
  30. package/package.json +3 -2
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "configKey": "core",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -3,7 +3,7 @@ import defu from 'defu';
3
3
  import * as theme from '../dist/runtime/theme/index.js';
4
4
 
5
5
  const name = "@finema/core";
6
- const version = "2.14.0";
6
+ const version = "2.15.0";
7
7
 
8
8
  const nuxtAppOptions = {
9
9
  head: {
@@ -40,7 +40,7 @@
40
40
  <div class="flex h-60 items-center justify-center">
41
41
  <Icon
42
42
  name="i-svg-spinners:180-ring-with-bg"
43
- class="text-primary size-8"
43
+ class="size-8 text-primary"
44
44
  />
45
45
  </div>
46
46
  </slot>
@@ -1,19 +1,19 @@
1
1
  <template>
2
- <div
2
+ <div
3
3
  :class="[theme.base({
4
4
  class: [$props.class, ui?.base]
5
- })]"
6
- >
7
- <component
8
- :is="componentMap[option.type]?.component"
9
- v-for="option in options.filter((item) => !item.isHide)"
10
- :key="option.props.name"
11
- :class="option.class"
12
- :form="form"
13
- v-bind="{ ...getFieldBinding(option), ...componentMap[option.type]?.props }"
14
- v-on="option.on ?? {}"
15
- />
16
- </div>
5
+ })]"
6
+ >
7
+ <component
8
+ :is="componentMap[option.type]?.component"
9
+ v-for="option in options.filter((item) => !item.isHide)"
10
+ :key="option.props.name"
11
+ :class="option.class"
12
+ :form="form"
13
+ v-bind="{ ...getFieldBinding(option), ...componentMap[option.type]?.props }"
14
+ v-on="option.on ?? {}"
15
+ />
16
+ </div>
17
17
  </template>
18
18
 
19
19
  <script setup>
@@ -25,9 +25,11 @@ import FormInputToggle from "./InputToggle/index.vue";
25
25
  import FormInputCheckbox from "./InputCheckbox/index.vue";
26
26
  import FormInputSelect from "./InputSelect/index.vue";
27
27
  import FormInputSelectMultiple from "./InputSelectMultiple/index.vue";
28
+ import FormInputRadio from "./InputRadio/index.vue";
28
29
  import FormInputDateTime from "./InputDateTime/index.vue";
29
30
  import FormInputDateTimeRange from "./InputDateTimeRange/index.vue";
30
31
  import FormInputUploadDropzoneAuto from "./InputUploadDropzoneAuto/index.vue";
32
+ import FormInputWYSIWYG from "./InputWYSIWYG/index.vue";
31
33
  import { INPUT_TYPES } from "#core/components/Form/types";
32
34
  import { formTheme } from "#core/theme/form";
33
35
  import { useUiConfig } from "#core/composables/useConfig";
@@ -84,7 +86,10 @@ const componentMap = {
84
86
  }
85
87
  },
86
88
  [INPUT_TYPES.STATIC]: void 0,
87
- [INPUT_TYPES.RADIO]: void 0,
89
+ [INPUT_TYPES.RADIO]: {
90
+ component: FormInputRadio,
91
+ props: {}
92
+ },
88
93
  [INPUT_TYPES.DATE_TIME]: {
89
94
  component: FormInputDateTime,
90
95
  props: {}
@@ -115,7 +120,10 @@ const componentMap = {
115
120
  },
116
121
  [INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE]: void 0,
117
122
  [INPUT_TYPES.UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE]: void 0,
118
- [INPUT_TYPES.WYSIWYG]: void 0,
123
+ [INPUT_TYPES.WYSIWYG]: {
124
+ component: FormInputWYSIWYG,
125
+ props: {}
126
+ },
119
127
  [INPUT_TYPES.TAGS]: void 0
120
128
  };
121
129
  const theme = computed(() => useUiConfig(formTheme, "form")({
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <RadioGroup
4
+ :model-value="value"
5
+ :items="options"
6
+ :variant="variant"
7
+ :orientation="orientation"
8
+ :indicator="indicator"
9
+ :legend="legend"
10
+ :disabled="wrapperProps.disabled"
11
+ :required="wrapperProps.required"
12
+ :name="wrapperProps.name"
13
+ value-key="value"
14
+ label-key="label"
15
+ description-key="description"
16
+ :ui="ui"
17
+ @update:model-value="onChange"
18
+ />
19
+ </FieldWrapper>
20
+ </template>
21
+
22
+ <script setup>
23
+ import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
24
+ import { useFieldHOC } from "#core/composables/useForm";
25
+ const emits = defineEmits(["change"]);
26
+ const props = defineProps({
27
+ variant: { type: String, required: false, default: "list" },
28
+ orientation: { type: String, required: false, default: "vertical" },
29
+ indicator: { type: String, required: false, default: "start" },
30
+ options: { type: Array, required: true },
31
+ legend: { type: String, required: false },
32
+ form: { type: Object, required: false },
33
+ name: { type: String, required: true },
34
+ errorMessage: { type: String, required: false },
35
+ label: { type: null, required: false },
36
+ description: { type: String, required: false },
37
+ hint: { type: String, required: false },
38
+ rules: { type: null, required: false },
39
+ autoFocus: { type: Boolean, required: false },
40
+ placeholder: { type: String, required: false },
41
+ disabled: { type: Boolean, required: false },
42
+ readonly: { type: Boolean, required: false },
43
+ required: { type: Boolean, required: false },
44
+ help: { type: String, required: false },
45
+ ui: { type: null, required: false }
46
+ });
47
+ const {
48
+ value,
49
+ wrapperProps,
50
+ handleChange
51
+ } = useFieldHOC(props);
52
+ const onChange = (newValue) => {
53
+ handleChange(newValue);
54
+ emits("change", newValue);
55
+ };
56
+ </script>
@@ -0,0 +1,11 @@
1
+ import type { IRadioFieldProps } from '#core/components/Form/InputRadio/types';
2
+ declare const _default: import("vue").DefineComponent<IRadioFieldProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
3
+ change: (value: any) => any;
4
+ }, string, import("vue").PublicProps, Readonly<IRadioFieldProps> & Readonly<{
5
+ onChange?: ((value: any) => any) | undefined;
6
+ }>, {
7
+ orientation: "horizontal" | "vertical";
8
+ variant: "list" | "card" | "table";
9
+ indicator: "start" | "end" | "hidden";
10
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import type { IFieldProps, IFormFieldBase, INPUT_TYPES } from '#core/components/Form/types';
2
+ import type { IOption } from '#core/types/common';
3
+ export type RadioOption = IOption & {
4
+ label?: string;
5
+ value?: any;
6
+ description?: string;
7
+ disabled?: boolean;
8
+ class?: any;
9
+ };
10
+ export interface IRadioFieldProps extends IFieldProps {
11
+ variant?: 'list' | 'card' | 'table';
12
+ orientation?: 'horizontal' | 'vertical';
13
+ indicator?: 'start' | 'end' | 'hidden';
14
+ options: RadioOption[];
15
+ legend?: string;
16
+ }
17
+ export type IRadioField = IFormFieldBase<INPUT_TYPES.RADIO, IRadioFieldProps, {
18
+ change?: (value: any) => void;
19
+ }>;
@@ -6,7 +6,7 @@
6
6
  />
7
7
  <div :class="theme.labelWrapper()">
8
8
  <p
9
- class="text-primary cursor-pointer font-bold"
9
+ class="cursor-pointer font-bold text-primary"
10
10
  @click="$emit('openFile')"
11
11
  >
12
12
  {{ selectFileLabel }}
@@ -11,7 +11,7 @@
11
11
  <h1 class="truncate font-bold">
12
12
  {{ selectedFile.name }}
13
13
  </h1>
14
- <p class="text-error truncate font-light">
14
+ <p class="truncate font-light text-error">
15
15
  {{ uploadFailedLabel }}
16
16
  </p>
17
17
  <Button
@@ -32,8 +32,8 @@ export const useUploadState = (props, emits, onChange, setErrors, value, accepte
32
32
  };
33
33
  const upload = useUploadLoader(request);
34
34
  const validateFile = (file) => {
35
- const acceptedTypes = fileAllocate.acceptFile.value;
36
- if (acceptedTypes) {
35
+ if (props.accept && fileAllocate.acceptFile.value) {
36
+ const acceptedTypes = fileAllocate.acceptFile.value;
37
37
  const acceptedTypesList = acceptedTypes.split(",").map((type) => type.trim());
38
38
  const fileExtension = file.name.toLowerCase().split(".").pop();
39
39
  const isValidFileType = acceptedTypesList.some((acceptedType) => {
@@ -113,6 +113,11 @@ export const useUploadState = (props, emits, onChange, setErrors, value, accepte
113
113
  processFile(file);
114
114
  }
115
115
  };
116
+ fileDialog.onChange((files) => {
117
+ if (files?.length) {
118
+ processFile(files[0]);
119
+ }
120
+ });
116
121
  const handleOpenFile = () => {
117
122
  if (wrapperProps.value.disabled || wrapperProps.value.readonly) return;
118
123
  fileDialog.open();
@@ -136,14 +141,6 @@ export const useUploadState = (props, emits, onChange, setErrors, value, accepte
136
141
  value: value.value
137
142
  });
138
143
  };
139
- watch(
140
- () => fileDialog.files.value,
141
- (files) => {
142
- if (files?.length) {
143
- processFile(files[0]);
144
- }
145
- }
146
- );
147
144
  watch(
148
145
  () => upload.status.value.isSuccess,
149
146
  (isSuccess2) => {
@@ -163,7 +160,7 @@ export const useUploadState = (props, emits, onChange, setErrors, value, accepte
163
160
  () => upload.status.value.isError,
164
161
  (isError2) => {
165
162
  if (isError2) {
166
- setErrors(StringHelper.getError(upload.status.value.errorData));
163
+ setErrors("\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14: " + StringHelper.getError(upload.status.value.errorData));
167
164
  }
168
165
  }
169
166
  );
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div class="upload-image-form">
3
+ <input
4
+ ref="fileInput"
5
+ type="file"
6
+ accept="image/*"
7
+ @change="handleFileSelect"
8
+ />
9
+ <button
10
+ type="button"
11
+ @click="handleUpload"
12
+ >
13
+ Upload
14
+ </button>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ import { ref } from "vue";
20
+ const props = defineProps({
21
+ options: { type: Object, required: false }
22
+ });
23
+ const emit = defineEmits(["submit"]);
24
+ const fileInput = ref();
25
+ const selectedFile = ref(null);
26
+ const handleFileSelect = (event) => {
27
+ const target = event.target;
28
+ const file = target.files?.[0];
29
+ if (file) {
30
+ selectedFile.value = file;
31
+ }
32
+ };
33
+ const handleUpload = async () => {
34
+ if (!selectedFile.value) return;
35
+ const url = URL.createObjectURL(selectedFile.value);
36
+ emit("submit", url);
37
+ };
38
+ </script>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ options?: {
3
+ requestOptions?: any;
4
+ };
5
+ }
6
+ declare const _default: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ submit: (url: string) => any;
8
+ }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
9
+ onSubmit?: ((url: string) => any) | undefined;
10
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ export default _default;
@@ -0,0 +1,281 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div :class="ui.container()">
4
+ <div
5
+ v-if="showToolbar"
6
+ :class="ui.toolbar()"
7
+ >
8
+ <div
9
+ v-for="(items, index) in menuItems"
10
+ :key="index"
11
+ :class="ui.toolbarGroup()"
12
+ >
13
+ <button
14
+ v-for="item in items"
15
+ :key="item.name"
16
+ :class="[ui.menuItem(), { [ui.menuItemActive()]: item.isActive?.() }]"
17
+ type="button"
18
+ :title="item.title"
19
+ @click="item.action"
20
+ >
21
+ <Icon
22
+ :name="item.icon"
23
+ :class="ui.icon()"
24
+ />
25
+ </button>
26
+ </div>
27
+ </div>
28
+ <ClientOnly>
29
+ <EditorContent
30
+ :editor="editor"
31
+ :class="ui.editorContent()"
32
+ />
33
+ <template #fallback>
34
+ <div
35
+ class="min-h-[200px]"
36
+ v-html="value"
37
+ />
38
+ </template>
39
+ </ClientOnly>
40
+ </div>
41
+ </FieldWrapper>
42
+ </template>
43
+
44
+ <script setup>
45
+ import { computed, watch } from "vue";
46
+ import { EditorContent, useEditor } from "@tiptap/vue-3";
47
+ import StarterKit from "@tiptap/starter-kit";
48
+ import Underline from "@tiptap/extension-underline";
49
+ import TextAlign from "@tiptap/extension-text-align";
50
+ import Link from "@tiptap/extension-link";
51
+ import Image from "@tiptap/extension-image";
52
+ import Youtube from "@tiptap/extension-youtube";
53
+ import { useFieldHOC } from "#core/composables/useForm";
54
+ import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
55
+ import { wysiwygTheme } from "#core/theme/wysiwyg";
56
+ import { useUiConfig } from "#core/composables/useConfig";
57
+ const props = defineProps({
58
+ editable: { type: Boolean, required: false },
59
+ autofocus: { type: Boolean, required: false },
60
+ content: { type: String, required: false },
61
+ toolbar: { type: Object, required: false },
62
+ minHeight: { type: [String, Number], required: false },
63
+ maxHeight: { type: [String, Number], required: false },
64
+ size: { type: String, required: false, default: "md" },
65
+ color: { type: String, required: false, default: "gray" },
66
+ imageUpload: { type: Object, required: false },
67
+ linkOptions: { type: Object, required: false },
68
+ containerUi: { type: null, required: false },
69
+ image: { type: Object, required: false },
70
+ form: { type: Object, required: false },
71
+ name: { type: String, required: true },
72
+ errorMessage: { type: String, required: false },
73
+ label: { type: null, required: false },
74
+ description: { type: String, required: false },
75
+ hint: { type: String, required: false },
76
+ rules: { type: null, required: false },
77
+ autoFocus: { type: Boolean, required: false },
78
+ placeholder: { type: String, required: false },
79
+ disabled: { type: Boolean, required: false },
80
+ readonly: { type: Boolean, required: false },
81
+ required: { type: Boolean, required: false },
82
+ help: { type: String, required: false },
83
+ ui: { type: null, required: false }
84
+ });
85
+ const {
86
+ value,
87
+ wrapperProps
88
+ } = useFieldHOC(props);
89
+ const ui = computed(() => useUiConfig(wysiwygTheme, "wysiwyg")({
90
+ size: props.size,
91
+ color: props.color
92
+ }));
93
+ const showToolbar = computed(() => {
94
+ if (!props.toolbar) return true;
95
+ return Object.values(props.toolbar).some(Boolean);
96
+ });
97
+ const editor = useEditor({
98
+ content: value.value,
99
+ extensions: [
100
+ StarterKit,
101
+ Underline,
102
+ TextAlign.configure({
103
+ types: ["heading", "paragraph"]
104
+ }),
105
+ Link.configure({
106
+ openOnClick: false
107
+ }),
108
+ Image,
109
+ Youtube
110
+ ],
111
+ editorProps: {
112
+ attributes: {
113
+ class: "prose px-4 py-2 focus:outline-none min-h-[200px]"
114
+ }
115
+ },
116
+ onUpdate: ({
117
+ editor: editor2
118
+ }) => {
119
+ value.value = editor2.getHTML();
120
+ }
121
+ });
122
+ watch(value, (newValue) => {
123
+ if (editor.value && newValue !== editor.value.getHTML()) {
124
+ editor.value.commands.setContent(newValue);
125
+ }
126
+ });
127
+ const toolbarConfig = {
128
+ format: [
129
+ {
130
+ key: "bold",
131
+ name: "bold",
132
+ icon: "ph:text-b-bold",
133
+ action: () => editor.value?.chain().focus().toggleBold().run(),
134
+ isActive: () => editor.value?.isActive("bold") || false,
135
+ title: "\u0E15\u0E31\u0E27\u0E2B\u0E19\u0E32"
136
+ },
137
+ {
138
+ key: "italic",
139
+ name: "italic",
140
+ icon: "ph:text-italic",
141
+ action: () => editor.value?.chain().focus().toggleItalic().run(),
142
+ isActive: () => editor.value?.isActive("italic") || false,
143
+ title: "\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E35\u0E22\u0E07"
144
+ },
145
+ {
146
+ key: "underline",
147
+ name: "underline",
148
+ icon: "ph:text-underline",
149
+ action: () => editor.value?.chain().focus().toggleUnderline().run(),
150
+ isActive: () => editor.value?.isActive("underline") || false,
151
+ title: "\u0E02\u0E35\u0E14\u0E40\u0E2A\u0E49\u0E19\u0E43\u0E15\u0E49"
152
+ }
153
+ ],
154
+ list: [
155
+ {
156
+ key: "bulletList",
157
+ name: "bullet-list",
158
+ icon: "ph:list-bullets",
159
+ action: () => editor.value?.chain().focus().toggleBulletList().run(),
160
+ isActive: () => editor.value?.isActive("bulletList") || false,
161
+ title: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E2A\u0E31\u0E0D\u0E25\u0E31\u0E01\u0E29\u0E13\u0E4C"
162
+ },
163
+ {
164
+ key: "orderedList",
165
+ name: "ordered-list",
166
+ icon: "ph:list-numbers",
167
+ action: () => editor.value?.chain().focus().toggleOrderedList().run(),
168
+ isActive: () => editor.value?.isActive("orderedList") || false,
169
+ title: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E25\u0E33\u0E14\u0E31\u0E1A"
170
+ }
171
+ ],
172
+ textAlign: [
173
+ {
174
+ key: "textAlign",
175
+ name: "align-left",
176
+ icon: "ph:text-align-left",
177
+ action: () => editor.value?.chain().focus().setTextAlign("left").run(),
178
+ isActive: () => editor.value?.isActive({
179
+ textAlign: "left"
180
+ }) || false,
181
+ title: "\u0E08\u0E31\u0E14\u0E0A\u0E34\u0E14\u0E0B\u0E49\u0E32\u0E22"
182
+ },
183
+ {
184
+ key: "textAlign",
185
+ name: "align-center",
186
+ icon: "ph:text-align-center",
187
+ action: () => editor.value?.chain().focus().setTextAlign("center").run(),
188
+ isActive: () => editor.value?.isActive({
189
+ textAlign: "center"
190
+ }) || false,
191
+ title: "\u0E08\u0E31\u0E14\u0E01\u0E36\u0E48\u0E07\u0E01\u0E25\u0E32\u0E07"
192
+ },
193
+ {
194
+ key: "textAlign",
195
+ name: "align-right",
196
+ icon: "ph:text-align-right",
197
+ action: () => editor.value?.chain().focus().setTextAlign("right").run(),
198
+ isActive: () => editor.value?.isActive({
199
+ textAlign: "right"
200
+ }) || false,
201
+ title: "\u0E08\u0E31\u0E14\u0E0A\u0E34\u0E14\u0E02\u0E27\u0E32"
202
+ }
203
+ ],
204
+ media: [
205
+ {
206
+ key: "link",
207
+ name: "link",
208
+ icon: "ph:link-simple",
209
+ action: () => {
210
+ const url = window.prompt("URL");
211
+ if (url) {
212
+ editor.value?.chain().focus().setLink({
213
+ href: url
214
+ }).run();
215
+ }
216
+ },
217
+ isActive: () => editor.value?.isActive("link") || false,
218
+ title: "\u0E41\u0E17\u0E23\u0E01\u0E25\u0E34\u0E07\u0E01\u0E4C"
219
+ },
220
+ {
221
+ key: "image",
222
+ name: "image",
223
+ icon: "ph:image",
224
+ action: () => {
225
+ const url = window.prompt("URL \u0E23\u0E39\u0E1B\u0E20\u0E32\u0E1E");
226
+ if (url) {
227
+ editor.value?.chain().focus().setImage({
228
+ src: url
229
+ }).run();
230
+ }
231
+ },
232
+ title: "\u0E41\u0E17\u0E23\u0E01\u0E23\u0E39\u0E1B\u0E20\u0E32\u0E1E"
233
+ },
234
+ {
235
+ key: "youtube",
236
+ name: "video",
237
+ icon: "ph:video-camera",
238
+ action: () => {
239
+ const url = window.prompt("URL \u0E27\u0E34\u0E14\u0E35\u0E42\u0E2D");
240
+ if (url) {
241
+ editor.value?.chain().focus().setYoutubeVideo({
242
+ src: url
243
+ }).run();
244
+ }
245
+ },
246
+ title: "\u0E41\u0E17\u0E23\u0E01\u0E27\u0E34\u0E14\u0E35\u0E42\u0E2D"
247
+ }
248
+ ],
249
+ history: [
250
+ {
251
+ key: "undo",
252
+ name: "undo",
253
+ icon: "ph:arrow-counter-clockwise",
254
+ action: () => editor.value?.chain().focus().undo().run(),
255
+ title: "\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33"
256
+ },
257
+ {
258
+ key: "redo",
259
+ name: "redo",
260
+ icon: "ph:arrow-clockwise",
261
+ action: () => editor.value?.chain().focus().redo().run(),
262
+ title: "\u0E17\u0E33\u0E43\u0E2B\u0E21\u0E48"
263
+ }
264
+ ]
265
+ };
266
+ const menuItems = computed(() => {
267
+ const items = [];
268
+ Object.entries(toolbarConfig).forEach(([groupKey, groupItems]) => {
269
+ const enabledItems = groupItems.filter((item) => {
270
+ if (groupKey === "textAlign") {
271
+ return props.toolbar?.textAlign;
272
+ }
273
+ return props.toolbar?.[item.key];
274
+ });
275
+ if (enabledItems.length > 0) {
276
+ items.push(enabledItems);
277
+ }
278
+ });
279
+ return items;
280
+ });
281
+ </script>
@@ -0,0 +1,6 @@
1
+ import type { IWYSIWYGFieldProps } from '#core/components/Form/InputWYSIWYG/types';
2
+ declare const _default: import("vue").DefineComponent<IWYSIWYGFieldProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<IWYSIWYGFieldProps> & Readonly<{}>, {
3
+ size: "xs" | "sm" | "md" | "lg" | "xl";
4
+ color: "primary" | "gray";
5
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ export default _default;
@@ -0,0 +1,52 @@
1
+ import type { Editor } from '@tiptap/vue-3';
2
+ import type { IFieldProps, IFormFieldBase, INPUT_TYPES } from '#core/components/Form/types';
3
+ export interface IWYSIWYGFieldProps extends IFieldProps {
4
+ editable?: boolean;
5
+ autofocus?: boolean;
6
+ content?: string;
7
+ toolbar?: {
8
+ bold?: boolean;
9
+ italic?: boolean;
10
+ underline?: boolean;
11
+ strike?: boolean;
12
+ code?: boolean;
13
+ heading?: boolean | number[];
14
+ paragraph?: boolean;
15
+ bulletList?: boolean;
16
+ orderedList?: boolean;
17
+ blockquote?: boolean;
18
+ codeBlock?: boolean;
19
+ horizontalRule?: boolean;
20
+ link?: boolean;
21
+ image?: boolean;
22
+ youtube?: boolean;
23
+ textAlign?: boolean;
24
+ undo?: boolean;
25
+ redo?: boolean;
26
+ };
27
+ minHeight?: string | number;
28
+ maxHeight?: string | number;
29
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
30
+ color?: 'primary' | 'gray';
31
+ imageUpload?: {
32
+ enabled?: boolean;
33
+ uploadUrl?: string;
34
+ maxSize?: number;
35
+ accept?: string[];
36
+ headers?: Record<string, string>;
37
+ };
38
+ linkOptions?: {
39
+ openOnClick?: boolean;
40
+ HTMLAttributes?: Record<string, any>;
41
+ };
42
+ containerUi?: any;
43
+ image?: {
44
+ requestOptions?: any;
45
+ };
46
+ }
47
+ export type IWYSIWYGField = IFormFieldBase<INPUT_TYPES.WYSIWYG, IWYSIWYGFieldProps, {
48
+ change?: (content: string) => void;
49
+ update?: (content: string, editor: Editor) => void;
50
+ focus?: (editor: Editor) => void;
51
+ blur?: (editor: Editor) => void;
52
+ }>;
@@ -10,6 +10,8 @@ import type { ICheckboxField } from '#core/components/Form/InputCheckbox/types';
10
10
  import type { ISelectMultipleField } from '#core/components/Form/InputSelectMultiple/types';
11
11
  import type { INumberField } from '#core/components/Form/InputNumber/types';
12
12
  import type { IDateTimeField } from '#core/components/Form/InputDateTime/date_time_field.types';
13
+ import type { IRadioField } from '#core/components/Form/InputRadio/types';
14
+ import type { IWYSIWYGField } from '#core/components/Form/InputWYSIWYG/types';
13
15
  export declare const enum INPUT_TYPES {
14
16
  TEXT = "TEXT",
15
17
  NUMBER = "NUMBER",
@@ -62,7 +64,7 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
62
64
  props: P;
63
65
  on?: O;
64
66
  }
65
- export type IFormField = ITextField | INumberField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IDateTimeField | IDateTimeRangeField | IUploadDropzoneAutoField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
67
+ export type IFormField = ITextField | INumberField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IRadioField | IDateTimeField | IDateTimeRangeField | IUploadDropzoneAutoField | IWYSIWYGField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
66
68
  export interface IFileValue {
67
69
  url: string;
68
70
  path?: string;
@@ -20,7 +20,7 @@
20
20
  <p class="text-error-400">
21
21
  <Icon
22
22
  name="i-heroicons:exclamation-circle-solid"
23
- class="text-error-400 size-8"
23
+ class="size-8 text-error-400"
24
24
  />
25
25
  </p>
26
26
  </div>
@@ -18,7 +18,7 @@
18
18
  <div class="flex h-60 items-center justify-center">
19
19
  <Icon
20
20
  name="i-svg-spinners:180-ring-with-bg"
21
- class="text-primary size-8"
21
+ class="size-8 text-primary"
22
22
  />
23
23
  </div>
24
24
  </template>
@@ -53,7 +53,7 @@
53
53
  name="error"
54
54
  >
55
55
  <div
56
- class="text-error-400 flex h-[200px] items-center justify-center text-2xl"
56
+ class="flex h-[200px] items-center justify-center text-2xl text-error-400"
57
57
  >
58
58
  {{ StringHelper.getError(options.status.errorData) }}
59
59
  </div>
@@ -1 +1 @@
1
- @import "tailwindcss";@import "@nuxt/ui";@theme static{--font-sans:"Noto Sans Thai","Noto Sans Thai Looped","Public Sans",sans-serif;--color-main:#232c5a;--color-main-50:#f4f4f7;--color-main-100:#e9eaef;--color-main-200:#c8cad6;--color-main-300:#a7abbd;--color-main-400:#656b8c;--color-main-500:#232c5a;--color-main-600:#202851;--color-main-700:#151a36;--color-main-800:#101429;--color-main-900:#0b0d1b;--color-main-950:#0b0d1b;--color-secondary:#ee8b36;--color-secondary-50:#fdf1e7;--color-secondary-100:#f9d6b8;--color-secondary-200:#f5bb89;--color-secondary-300:#f1a05a;--color-secondary-400:#ed852b;--color-secondary-500:#d46b12;--color-secondary-600:#a5540e;--color-secondary-700:#763c0a;--color-secondary-800:#472406;--color-secondary-900:#180c02;--color-info:#0d8cee;--color-info-50:#f3f9fe;--color-info-100:#e7f4fd;--color-info-200:#ebf6ff;--color-info-300:#9ed1f8;--color-info-400:#56aff3;--color-info-500:#0d8cee;--color-info-600:#0c7ed6;--color-info-700:#08548f;--color-info-800:#063f6b;--color-info-900:#042a47;--color-error:#f25555;--color-error-50:#fef7f7;--color-error-100:#feeeee;--color-error-200:#ffdfdf;--color-error-300:#fabbbb;--color-error-400:#f68888;--color-error-500:#f25555;--color-error-600:#da4d4d;--color-error-700:#913333;--color-error-800:#6d2626;--color-error-900:#491a1a;--color-success:#3fb061;--color-success-50:#f5fbf7;--color-success-100:#ecf7ef;--color-success-200:#daeee0;--color-success-300:#b2dfc0;--color-success-400:#79c890;--color-success-500:#3fb061;--color-success-600:#399e57;--color-success-700:#266a3a;--color-success-800:#1c4f2c;--color-success-900:#13351d;--color-warning:#ff9a35;--color-warning-50:#fffaf5;--color-warning-100:#fff5eb;--color-warning-200:#fef1cc;--color-warning-300:#ffd7ae;--color-warning-400:#ffb872;--color-warning-500:#ff9a35;--color-warning-600:#e68b30;--color-warning-700:#995c20;--color-warning-800:#734518;--color-warning-900:#4d2e10}::-webkit-scrollbar{-webkit-appearance:none;height:10px;width:10px}::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.3);border-radius:4px;box-shadow:0 0 1px hsla(0,0%,100%,.5)}:root{--dp-font-family:inherit!important}.dp__theme_light{--dp-primary-color:var(--color-main)!important;--dp-primary-disabled-color:var(--color-main-200)!important}#__nuxt,body,html{@apply w-full h-full}
1
+ @import "tailwindcss";@import "@nuxt/ui";@plugin "@tailwindcss/typography";@source inline("prose");@theme static{--font-sans:"Noto Sans Thai","Noto Sans Thai Looped","Public Sans",sans-serif;--color-main:#232c5a;--color-main-50:#f4f4f7;--color-main-100:#e9eaef;--color-main-200:#c8cad6;--color-main-300:#a7abbd;--color-main-400:#656b8c;--color-main-500:#232c5a;--color-main-600:#202851;--color-main-700:#151a36;--color-main-800:#101429;--color-main-900:#0b0d1b;--color-main-950:#0b0d1b;--color-secondary:#ee8b36;--color-secondary-50:#fdf1e7;--color-secondary-100:#f9d6b8;--color-secondary-200:#f5bb89;--color-secondary-300:#f1a05a;--color-secondary-400:#ed852b;--color-secondary-500:#d46b12;--color-secondary-600:#a5540e;--color-secondary-700:#763c0a;--color-secondary-800:#472406;--color-secondary-900:#180c02;--color-info:#0d8cee;--color-info-50:#f3f9fe;--color-info-100:#e7f4fd;--color-info-200:#ebf6ff;--color-info-300:#9ed1f8;--color-info-400:#56aff3;--color-info-500:#0d8cee;--color-info-600:#0c7ed6;--color-info-700:#08548f;--color-info-800:#063f6b;--color-info-900:#042a47;--color-error:#f25555;--color-error-50:#fef7f7;--color-error-100:#feeeee;--color-error-200:#ffdfdf;--color-error-300:#fabbbb;--color-error-400:#f68888;--color-error-500:#f25555;--color-error-600:#da4d4d;--color-error-700:#913333;--color-error-800:#6d2626;--color-error-900:#491a1a;--color-success:#3fb061;--color-success-50:#f5fbf7;--color-success-100:#ecf7ef;--color-success-200:#daeee0;--color-success-300:#b2dfc0;--color-success-400:#79c890;--color-success-500:#3fb061;--color-success-600:#399e57;--color-success-700:#266a3a;--color-success-800:#1c4f2c;--color-success-900:#13351d;--color-warning:#ff9a35;--color-warning-50:#fffaf5;--color-warning-100:#fff5eb;--color-warning-200:#fef1cc;--color-warning-300:#ffd7ae;--color-warning-400:#ffb872;--color-warning-500:#ff9a35;--color-warning-600:#e68b30;--color-warning-700:#995c20;--color-warning-800:#734518;--color-warning-900:#4d2e10}::-webkit-scrollbar{-webkit-appearance:none;height:10px;width:10px}::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.3);border-radius:4px;box-shadow:0 0 1px hsla(0,0%,100%,.5)}:root{--dp-font-family:inherit!important}.dp__theme_light{--dp-primary-color:var(--color-main)!important;--dp-primary-disabled-color:var(--color-main-200)!important}#__nuxt,body,html{@apply w-full h-full}
@@ -11,3 +11,5 @@ export { inputNumberTheme as inputNumber } from './inputNumber.js';
11
11
  export { iconsTheme as icons } from './icons.js';
12
12
  export { uploadFileDropzoneTheme as uploadFileDropzone } from './uploadFileDropzone.js';
13
13
  export { dateTimeTheme as dateTime } from './dateTime.js';
14
+ export { radioGroupTheme as radioGroup } from './radioGroup.js';
15
+ export { wysiwygTheme as wysiwyg } from './wysiwyg.js';
@@ -11,3 +11,5 @@ export { inputNumberTheme as inputNumber } from "./inputNumber.js";
11
11
  export { iconsTheme as icons } from "./icons.js";
12
12
  export { uploadFileDropzoneTheme as uploadFileDropzone } from "./uploadFileDropzone.js";
13
13
  export { dateTimeTheme as dateTime } from "./dateTime.js";
14
+ export { radioGroupTheme as radioGroup } from "./radioGroup.js";
15
+ export { wysiwygTheme as wysiwyg } from "./wysiwyg.js";
@@ -0,0 +1,6 @@
1
+ export declare const radioGroupTheme: {
2
+ slots: {
3
+ item: string;
4
+ label: string;
5
+ };
6
+ };
@@ -0,0 +1,6 @@
1
+ export const radioGroupTheme = {
2
+ slots: {
3
+ item: "cursor-pointer",
4
+ label: "cursor-pointer"
5
+ }
6
+ };
@@ -1,6 +1,6 @@
1
1
  export const uploadFileDropzoneTheme = {
2
2
  slots: {
3
- base: "relative w-full text-base p-4 transition rounded-lg flex items-center justify-center ring-1 bg-white ring-gray-300",
3
+ base: "relative w-full text-base p-4 transition rounded-md flex items-center justify-center ring-1 bg-white ring-gray-300",
4
4
  wrapper: "flex flex-col items-center w-full",
5
5
  disabled: "bg-gray-100 border-none grayscale cursor-not-allowed",
6
6
  failed: "border-error",
@@ -20,13 +20,13 @@ export const uploadFileDropzoneTheme = {
20
20
  onLoadingTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
21
21
  onLoadingLoadingIconClass: "size-10 text-primary animate-spin",
22
22
  // Preview state
23
- onPreviewWrapper: "flex items-center space-x-4 rounded-lg w-full",
24
- onPreviewPreviewImgWrapper: "flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden bg-gray-100",
23
+ onPreviewWrapper: "flex items-center space-x-4 rounded-md w-full",
24
+ onPreviewPreviewImgWrapper: "flex-shrink-0 w-16 h-16 rounded-md overflow-hidden bg-gray-100",
25
25
  onPreviewPreviewImgClass: "w-full h-full object-cover",
26
26
  onPreviewPreviewFileClass: "size-8 text-gray-400 m-auto mt-4",
27
27
  onPreviewTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
28
28
  // Failed state
29
- onFailedWrapper: "flex items-start space-x-4 w-full rounded-lg",
29
+ onFailedWrapper: "flex items-start space-x-4 w-full rounded-md",
30
30
  onFailedFailedImgWrapper: "flex-shrink-0",
31
31
  onFailedFailedIconClass: "size-12",
32
32
  onFailedTextWrapper: "flex-1 min-w-0 flex items-start justify-between",
@@ -0,0 +1,53 @@
1
+ export declare const wysiwygTheme: {
2
+ slots: {
3
+ container: string;
4
+ toolbar: string;
5
+ toolbarGroup: string;
6
+ menuItem: string;
7
+ menuItemActive: string;
8
+ icon: string;
9
+ editorContent: string;
10
+ };
11
+ variants: {
12
+ size: {
13
+ xs: {
14
+ button: string;
15
+ icon: string;
16
+ select: string;
17
+ };
18
+ sm: {
19
+ button: string;
20
+ icon: string;
21
+ select: string;
22
+ };
23
+ md: {
24
+ button: string;
25
+ icon: string;
26
+ select: string;
27
+ };
28
+ lg: {
29
+ button: string;
30
+ icon: string;
31
+ select: string;
32
+ };
33
+ xl: {
34
+ button: string;
35
+ icon: string;
36
+ select: string;
37
+ };
38
+ };
39
+ color: {
40
+ primary: {
41
+ button: string;
42
+ };
43
+ gray: {
44
+ button: string;
45
+ };
46
+ };
47
+ };
48
+ compoundVariants: never[];
49
+ defaultVariants: {
50
+ size: string;
51
+ color: string;
52
+ };
53
+ };
@@ -0,0 +1,53 @@
1
+ export const wysiwygTheme = {
2
+ slots: {
3
+ container: "border border-gray-200 rounded focus:ring-primary-500 relative block w-full resize-none rounded-md border-0 bg-white p-0 pb-3 text-sm text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-75",
4
+ toolbar: "flex flex-wrap border py-2 px-2 gap-1 border-gray-300 bg-white rounded-t-md",
5
+ toolbarGroup: "flex items-center border-r border-gray-200 pr-2",
6
+ menuItem: "px-1 py-1 rounded-md hover:bg-gray-100 transition-colors flex justify-center items-center cursor-pointer flex-wrap",
7
+ menuItemActive: "bg-primary-100 text-primary-600",
8
+ icon: "size-5",
9
+ editorContent: ""
10
+ },
11
+ variants: {
12
+ size: {
13
+ xs: {
14
+ button: "px-1.5 py-0.5 text-xs",
15
+ icon: "h-3 w-3",
16
+ select: "px-1.5 py-0.5 text-xs"
17
+ },
18
+ sm: {
19
+ button: "px-2 py-1 text-sm",
20
+ icon: "h-3.5 w-3.5",
21
+ select: "px-2 py-1 text-xs"
22
+ },
23
+ md: {
24
+ button: "px-2 py-1 text-sm",
25
+ icon: "h-4 w-4",
26
+ select: "px-2 py-1 text-xs"
27
+ },
28
+ lg: {
29
+ button: "px-3 py-1.5 text-base",
30
+ icon: "h-5 w-5",
31
+ select: "px-3 py-1.5 text-sm"
32
+ },
33
+ xl: {
34
+ button: "px-4 py-2 text-lg",
35
+ icon: "h-6 w-6",
36
+ select: "px-4 py-2 text-base"
37
+ }
38
+ },
39
+ color: {
40
+ primary: {
41
+ button: "hover:bg-blue-50 hover:border-blue-300"
42
+ },
43
+ gray: {
44
+ button: "hover:bg-gray-100 hover:border-gray-300"
45
+ }
46
+ }
47
+ },
48
+ compoundVariants: [],
49
+ defaultVariants: {
50
+ size: "md",
51
+ color: "gray"
52
+ }
53
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",
@@ -67,13 +67,14 @@
67
67
  "url-join": "^5.0.0"
68
68
  },
69
69
  "devDependencies": {
70
- "@hyoban/eslint-plugin-tailwindcss": "^4.0.0-alpha.12",
71
70
  "@eslint/js": "^9.26.0",
71
+ "@hyoban/eslint-plugin-tailwindcss": "^4.0.0-alpha.12",
72
72
  "@nuxt/devtools": "^2.4.1",
73
73
  "@nuxt/eslint-config": "^1.3.1",
74
74
  "@nuxt/module-builder": "^1.0.1",
75
75
  "@nuxt/schema": "^3.17.3",
76
76
  "@nuxt/test-utils": "^3.19.0",
77
+ "@tailwindcss/typography": "^0.5.0-alpha.3",
77
78
  "@types/node": "latest",
78
79
  "changelogen": "^0.5.7",
79
80
  "eslint": "^9.26.0",