@karibsen/eponyme 0.6.0 → 0.8.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.
- package/README.md +109 -45
- package/dist/eponyme.mjs +2 -3
- package/dist/module.d.mts +109 -13
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -6
- package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
- package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
- package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
- package/dist/runtime/components/fields/FieldRenderer.vue +22 -0
- package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
- package/dist/runtime/components/fields/PhoneField.vue +61 -0
- package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/RichTextField.vue +37 -1
- package/dist/runtime/components/fields/TagsField.d.vue.ts +21 -0
- package/dist/runtime/components/fields/TagsField.vue +55 -0
- package/dist/runtime/components/fields/TagsField.vue.d.ts +21 -0
- package/dist/runtime/components/ui/EPAlertDialog.d.vue.ts +37 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue +66 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue.d.ts +37 -0
- package/dist/runtime/components/ui/EPDialog.d.vue.ts +1 -0
- package/dist/runtime/components/ui/EPDialog.vue +6 -2
- package/dist/runtime/components/ui/EPDialog.vue.d.ts +1 -0
- package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
- package/dist/runtime/components/ui/EPTagsInput.d.vue.ts +27 -0
- package/dist/runtime/components/ui/EPTagsInput.vue +101 -0
- package/dist/runtime/components/ui/EPTagsInput.vue.d.ts +27 -0
- package/dist/runtime/composables/useEponymeCollection.d.ts +13 -1
- package/dist/runtime/composables/useEponymeCollection.js +29 -4
- package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
- package/dist/runtime/composables/useEponymeForm.js +1 -8
- package/dist/runtime/fields/index.d.ts +4 -0
- package/dist/runtime/fields/index.js +3 -1
- package/dist/runtime/fields/phone.d.ts +2 -0
- package/dist/runtime/fields/phone.js +3 -0
- package/dist/runtime/fields/tags.d.ts +6 -0
- package/dist/runtime/fields/tags.js +3 -0
- package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
- package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
- package/dist/runtime/pages/EponymeIndexPage.vue +0 -3
- package/dist/runtime/server/api/eponyme-collections/[path].get.js +40 -1
- package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
- package/dist/runtime/server/plugins/eponyme-sync.js +11 -1
- package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
- package/dist/runtime/server/services/eponyme-form-store.js +4 -3
- package/dist/runtime/server/services/eponyme-store.d.ts +289 -30
- package/dist/runtime/server/services/eponyme-store.js +405 -73
- package/dist/runtime/server/utils/eponyme-reindex.d.ts +13 -0
- package/dist/runtime/server/utils/eponyme-reindex.js +4 -0
- package/dist/runtime/types/eponyme-config.d.ts +0 -7
- package/dist/runtime/types/eponyme.d.ts +54 -6
- package/dist/runtime/types/field.d.ts +82 -4
- package/dist/runtime/types/index.d.ts +0 -1
- package/dist/runtime/types/index.js +0 -1
- package/dist/runtime/utils/eponyme-entry-index.d.ts +39 -0
- package/dist/runtime/utils/eponyme-entry-index.js +51 -0
- package/dist/runtime/utils/get-field-default-value.js +2 -0
- package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
- package/dist/runtime/utils/middle-ellipsis.js +8 -0
- package/dist/runtime/utils/normalize-eponyme-values.d.ts +13 -0
- package/dist/runtime/utils/normalize-eponyme-values.js +40 -0
- package/dist/runtime/utils/normalize-phone.d.ts +26 -0
- package/dist/runtime/utils/normalize-phone.js +17 -0
- package/dist/runtime/utils/normalize-tags.d.ts +10 -0
- package/dist/runtime/utils/normalize-tags.js +17 -0
- package/dist/runtime/utils/validate-eponyme-data.js +45 -3
- package/dist/runtime/utils/variables.d.ts +4 -0
- package/dist/runtime/utils/variables.js +8 -1
- package/package.json +4 -2
- package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
- package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
- package/dist/runtime/types/captcha.d.ts +0 -16
- package/dist/runtime/types/captcha.js +0 -0
- package/dist/runtime/utils/no-captcha.d.ts +0 -7
- package/dist/runtime/utils/no-captcha.js +0 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PhoneFieldOptions } from '../../types/field.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
modelValue?: unknown;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
countries?: PhoneFieldOptions['countries'];
|
|
10
|
+
defaultCountry?: PhoneFieldOptions['defaultCountry'];
|
|
11
|
+
detectCountry?: boolean;
|
|
12
|
+
autocomplete?: PhoneFieldOptions['autocomplete'];
|
|
13
|
+
errors?: string[];
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
"update:modelValue": (value: string) => any;
|
|
18
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
19
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import Image from "@tiptap/extension-image";
|
|
3
3
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
4
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
5
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
6
|
import StarterKit from "@tiptap/starter-kit";
|
|
5
7
|
import { EditorContent, Extension, useEditor } from "@tiptap/vue-3";
|
|
6
8
|
import { computed, ref, watch } from "vue";
|
|
7
9
|
import { useEponymeVariables } from "../../composables/useEponymeVariables";
|
|
10
|
+
import { findEponymeVariableRanges } from "../../utils/variables";
|
|
8
11
|
import EPDropdownMenu from "../ui/EPDropdownMenu.vue";
|
|
9
12
|
import EPFormField from "../ui/EPFormField.vue";
|
|
10
13
|
import EPLinkDialog from "../ui/EPLinkDialog.vue";
|
|
@@ -23,6 +26,38 @@ const DownloadableLinkAttribute = Extension.create({
|
|
|
23
26
|
}];
|
|
24
27
|
}
|
|
25
28
|
});
|
|
29
|
+
const variableHighlightKey = new PluginKey("eponymeVariableHighlight");
|
|
30
|
+
function createVariableDecorations(document) {
|
|
31
|
+
const decorations = [];
|
|
32
|
+
document.descendants((node, position) => {
|
|
33
|
+
if (!node.isText || !node.text) return;
|
|
34
|
+
for (const range of findEponymeVariableRanges(node.text)) {
|
|
35
|
+
decorations.push(Decoration.inline(
|
|
36
|
+
position + range.from,
|
|
37
|
+
position + range.to,
|
|
38
|
+
{ class: "eponyme-rich-text-variable" }
|
|
39
|
+
));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return DecorationSet.create(document, decorations);
|
|
43
|
+
}
|
|
44
|
+
const EponymeVariableHighlight = Extension.create({
|
|
45
|
+
name: "eponymeVariableHighlight",
|
|
46
|
+
addProseMirrorPlugins() {
|
|
47
|
+
return [
|
|
48
|
+
new Plugin({
|
|
49
|
+
key: variableHighlightKey,
|
|
50
|
+
state: {
|
|
51
|
+
init: (_, state) => createVariableDecorations(state.doc),
|
|
52
|
+
apply: (transaction, decorations) => transaction.docChanged ? createVariableDecorations(transaction.doc) : decorations
|
|
53
|
+
},
|
|
54
|
+
props: {
|
|
55
|
+
decorations: (state) => variableHighlightKey.getState(state)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
});
|
|
26
61
|
const props = defineProps({
|
|
27
62
|
id: { type: String, required: true },
|
|
28
63
|
modelValue: { type: null, required: false },
|
|
@@ -55,6 +90,7 @@ const editor = useEditor({
|
|
|
55
90
|
}
|
|
56
91
|
}),
|
|
57
92
|
DownloadableLinkAttribute,
|
|
93
|
+
EponymeVariableHighlight,
|
|
58
94
|
Placeholder.configure({ placeholder: props.placeholder }),
|
|
59
95
|
Image.configure({ HTMLAttributes: { class: "eponyme-rich-text-image" } })
|
|
60
96
|
],
|
|
@@ -244,5 +280,5 @@ const tools = computed(() => {
|
|
|
244
280
|
</template>
|
|
245
281
|
|
|
246
282
|
<style scoped>
|
|
247
|
-
.rich-text-tool{align-items:center;background:transparent;border:0;border-radius:.45rem;color:var(--ep-color-muted-ep,#8d8d8d);cursor:pointer;display:flex;font-family:inherit;font-size:.75rem;font-weight:650;height:2rem;justify-content:center;min-width:2rem;padding:0 .55rem;transition:color .15s ease,background-color .15s ease}.rich-text-tool.is-active,.rich-text-tool:hover{background:var(--ep-color-theme-ep,#1c1c1c);color:#fff}.rich-text-tool:disabled{cursor:not-allowed;opacity:.35}.eponyme-rich-text :deep(.tiptap){color:var(--ep-color-text-ep,#e7e7e7);font-size:.925rem;line-height:1.75;min-height:18rem;outline:none;padding:1.25rem}.eponyme-rich-text :deep(.tiptap>:first-child){margin-top:0}.eponyme-rich-text :deep(.tiptap>:last-child){margin-bottom:0}.eponyme-rich-text :deep(.tiptap h2){font-size:1.5rem;line-height:1.25;margin:1.5rem 0 .75rem}.eponyme-rich-text :deep(.tiptap h3){font-size:1.2rem;line-height:1.3;margin:1.25rem 0 .625rem}.eponyme-rich-text :deep(.tiptap blockquote),.eponyme-rich-text :deep(.tiptap ol),.eponyme-rich-text :deep(.tiptap p),.eponyme-rich-text :deep(.tiptap ul){margin:.75rem 0}.eponyme-rich-text :deep(.tiptap ol),.eponyme-rich-text :deep(.tiptap ul){padding-left:1.5rem}.eponyme-rich-text :deep(.tiptap blockquote){border-left:2px solid var(--ep-color-muted-ep,#8d8d8d);color:var(--ep-color-muted-ep,#8d8d8d);padding-left:1rem}.eponyme-rich-text :deep(.tiptap img){border-radius:.75rem;height:auto;max-width:100%}.eponyme-rich-text :deep(.tiptap img.ProseMirror-selectednode){outline:2px solid #fff;outline-offset:2px}.eponyme-rich-text :deep(.tiptap a){color:#fff;text-decoration:underline;text-underline-offset:3px}.eponyme-rich-text :deep(.tiptap p.is-editor-empty:first-child:before){color:var(--ep-color-muted-ep,#8d8d8d);content:attr(data-placeholder);float:left;height:0;pointer-events:none}
|
|
283
|
+
.rich-text-tool{align-items:center;background:transparent;border:0;border-radius:.45rem;color:var(--ep-color-muted-ep,#8d8d8d);cursor:pointer;display:flex;font-family:inherit;font-size:.75rem;font-weight:650;height:2rem;justify-content:center;min-width:2rem;padding:0 .55rem;transition:color .15s ease,background-color .15s ease}.rich-text-tool.is-active,.rich-text-tool:hover{background:var(--ep-color-theme-ep,#1c1c1c);color:#fff}.rich-text-tool:disabled{cursor:not-allowed;opacity:.35}.eponyme-rich-text :deep(.tiptap){color:var(--ep-color-text-ep,#e7e7e7);font-size:.925rem;line-height:1.75;min-height:18rem;outline:none;padding:1.25rem}.eponyme-rich-text :deep(.tiptap>:first-child){margin-top:0}.eponyme-rich-text :deep(.tiptap>:last-child){margin-bottom:0}.eponyme-rich-text :deep(.tiptap h2){font-size:1.5rem;line-height:1.25;margin:1.5rem 0 .75rem}.eponyme-rich-text :deep(.tiptap h3){font-size:1.2rem;line-height:1.3;margin:1.25rem 0 .625rem}.eponyme-rich-text :deep(.tiptap blockquote),.eponyme-rich-text :deep(.tiptap ol),.eponyme-rich-text :deep(.tiptap p),.eponyme-rich-text :deep(.tiptap ul){margin:.75rem 0}.eponyme-rich-text :deep(.tiptap ol),.eponyme-rich-text :deep(.tiptap ul){padding-left:1.5rem}.eponyme-rich-text :deep(.tiptap blockquote){border-left:2px solid var(--ep-color-muted-ep,#8d8d8d);color:var(--ep-color-muted-ep,#8d8d8d);padding-left:1rem}.eponyme-rich-text :deep(.tiptap img){border-radius:.75rem;height:auto;max-width:100%}.eponyme-rich-text :deep(.tiptap img.ProseMirror-selectednode){outline:2px solid #fff;outline-offset:2px}.eponyme-rich-text :deep(.tiptap a){color:#fff;text-decoration:underline;text-underline-offset:3px}.eponyme-rich-text :deep(.eponyme-rich-text-variable){color:#ff41c5;font-weight:650}@supports (((-webkit-background-clip:text) or (background-clip:text)) or (-webkit-background-clip:text)){.eponyme-rich-text :deep(.eponyme-rich-text-variable){background-clip:text;-webkit-background-clip:text;background-image:linear-gradient(90deg,#fe7af0,#ff41c5);background-position:0 50%;background-size:200% 100%;color:transparent;-webkit-text-fill-color:transparent}}@media (prefers-reduced-motion:no-preference){@supports (((-webkit-background-clip:text) or (background-clip:text)) or (-webkit-background-clip:text)){.eponyme-rich-text :deep(.eponyme-rich-text-variable){animation:eponyme-variable-gradient 2.5s ease-in-out infinite alternate}}}@keyframes eponyme-variable-gradient{to{background-position:100% 50%}}@media (forced-colors:active){.eponyme-rich-text :deep(.eponyme-rich-text-variable){background:none;color:linktext;text-decoration:underline;-webkit-text-fill-color:currentcolor}}.eponyme-rich-text :deep(.tiptap p.is-editor-empty:first-child:before){color:var(--ep-color-muted-ep,#8d8d8d);content:attr(data-placeholder);float:left;height:0;pointer-events:none}
|
|
248
284
|
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
id: string;
|
|
3
|
+
modelValue?: unknown;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
suggestions?: readonly string[];
|
|
8
|
+
allowCustom?: boolean;
|
|
9
|
+
minItems?: number;
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
errors?: string[];
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
"update:modelValue": (value: string[]) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import EPFormField from "../ui/EPFormField.vue";
|
|
4
|
+
import EPTagsInput from "../ui/EPTagsInput.vue";
|
|
5
|
+
import { normalizeEponymeTags } from "../../utils/normalize-tags";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
id: { type: String, required: true },
|
|
8
|
+
modelValue: { type: null, required: false },
|
|
9
|
+
label: { type: String, required: true },
|
|
10
|
+
description: { type: String, required: false },
|
|
11
|
+
required: { type: Boolean, required: false },
|
|
12
|
+
suggestions: { type: Array, required: false },
|
|
13
|
+
allowCustom: { type: Boolean, required: false },
|
|
14
|
+
minItems: { type: Number, required: false },
|
|
15
|
+
maxItems: { type: Number, required: false },
|
|
16
|
+
placeholder: { type: String, required: false },
|
|
17
|
+
errors: { type: Array, required: false, default: () => [] },
|
|
18
|
+
disabled: { type: Boolean, required: false }
|
|
19
|
+
});
|
|
20
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
21
|
+
const tags = computed(() => Array.isArray(props.modelValue) ? props.modelValue.filter((tag) => typeof tag === "string") : []);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<EPFormField
|
|
26
|
+
:id="id"
|
|
27
|
+
:label="label"
|
|
28
|
+
:description="description"
|
|
29
|
+
:required="required"
|
|
30
|
+
:errors="errors"
|
|
31
|
+
>
|
|
32
|
+
<ClientOnly>
|
|
33
|
+
<EPTagsInput
|
|
34
|
+
:model-value="tags"
|
|
35
|
+
:suggestions="suggestions"
|
|
36
|
+
:allow-custom="allowCustom"
|
|
37
|
+
:placeholder="placeholder"
|
|
38
|
+
:max="maxItems"
|
|
39
|
+
:required="required"
|
|
40
|
+
:disabled="disabled"
|
|
41
|
+
:invalid="Boolean(errors.length)"
|
|
42
|
+
@update:model-value="emit('update:modelValue', normalizeEponymeTags($event, { suggestions, allowCustom }))"
|
|
43
|
+
/>
|
|
44
|
+
<template #fallback>
|
|
45
|
+
<div class="ep:min-h-12 ep:rounded-xl ep:border ep:border-border-ep ep:bg-selected-ep" />
|
|
46
|
+
</template>
|
|
47
|
+
</ClientOnly>
|
|
48
|
+
<p
|
|
49
|
+
v-if="maxItems !== void 0"
|
|
50
|
+
class="ep:mt-1.5 ep:mb-0 ep:text-right ep:text-[11px] ep:text-muted-ep"
|
|
51
|
+
>
|
|
52
|
+
{{ tags.length }} / {{ maxItems }}
|
|
53
|
+
</p>
|
|
54
|
+
</EPFormField>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
id: string;
|
|
3
|
+
modelValue?: unknown;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
suggestions?: readonly string[];
|
|
8
|
+
allowCustom?: boolean;
|
|
9
|
+
minItems?: number;
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
errors?: string[];
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
"update:modelValue": (value: string[]) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
cancelLabel?: string;
|
|
6
|
+
confirmLabel?: string;
|
|
7
|
+
confirmVariant?: 'primary' | 'secondary' | 'danger';
|
|
8
|
+
confirmLoading?: boolean;
|
|
9
|
+
confirmDisabled?: boolean;
|
|
10
|
+
closeOnConfirm?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type __VLS_Slots = {
|
|
13
|
+
trigger?: () => unknown;
|
|
14
|
+
default?: () => unknown;
|
|
15
|
+
confirm?: (props: {
|
|
16
|
+
confirm: () => void;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
}) => unknown;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
cancel: () => any;
|
|
23
|
+
"update:open": (value: boolean) => any;
|
|
24
|
+
confirm: () => any;
|
|
25
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
26
|
+
onCancel?: (() => any) | undefined;
|
|
27
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
|
+
onConfirm?: (() => any) | undefined;
|
|
29
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
31
|
+
declare const _default: typeof __VLS_export;
|
|
32
|
+
export default _default;
|
|
33
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
34
|
+
new (): {
|
|
35
|
+
$slots: S;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import EPButton from "./EPButton.vue";
|
|
3
|
+
import EPDialog from "./EPDialog.vue";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
open: { type: Boolean, required: false },
|
|
6
|
+
label: { type: String, required: true },
|
|
7
|
+
description: { type: String, required: false },
|
|
8
|
+
cancelLabel: { type: String, required: false, default: "Cancel" },
|
|
9
|
+
confirmLabel: { type: String, required: false, default: "Confirm" },
|
|
10
|
+
confirmVariant: { type: String, required: false, default: "primary" },
|
|
11
|
+
confirmLoading: { type: Boolean, required: false },
|
|
12
|
+
confirmDisabled: { type: Boolean, required: false },
|
|
13
|
+
closeOnConfirm: { type: Boolean, required: false, default: true }
|
|
14
|
+
});
|
|
15
|
+
const emit = defineEmits(["update:open", "cancel", "confirm"]);
|
|
16
|
+
defineSlots();
|
|
17
|
+
function cancel() {
|
|
18
|
+
emit("cancel");
|
|
19
|
+
emit("update:open", false);
|
|
20
|
+
}
|
|
21
|
+
function confirm() {
|
|
22
|
+
emit("confirm");
|
|
23
|
+
if (props.closeOnConfirm) emit("update:open", false);
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<EPDialog
|
|
29
|
+
:open="open"
|
|
30
|
+
:title="label"
|
|
31
|
+
:description="description"
|
|
32
|
+
role="alertdialog"
|
|
33
|
+
@update:open="emit('update:open', $event)"
|
|
34
|
+
>
|
|
35
|
+
<template
|
|
36
|
+
v-if="$slots.trigger"
|
|
37
|
+
#trigger
|
|
38
|
+
>
|
|
39
|
+
<slot name="trigger" />
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<slot />
|
|
43
|
+
|
|
44
|
+
<div class="ep:mt-6 ep:flex ep:justify-end ep:gap-2">
|
|
45
|
+
<EPButton
|
|
46
|
+
:label="cancelLabel"
|
|
47
|
+
variant="ghost"
|
|
48
|
+
@click="cancel"
|
|
49
|
+
/>
|
|
50
|
+
<slot
|
|
51
|
+
name="confirm"
|
|
52
|
+
:confirm="confirm"
|
|
53
|
+
:loading="confirmLoading"
|
|
54
|
+
:disabled="confirmDisabled"
|
|
55
|
+
>
|
|
56
|
+
<EPButton
|
|
57
|
+
:label="confirmLabel"
|
|
58
|
+
:variant="confirmVariant"
|
|
59
|
+
:loading="confirmLoading"
|
|
60
|
+
:disabled="confirmDisabled"
|
|
61
|
+
@click="confirm"
|
|
62
|
+
/>
|
|
63
|
+
</slot>
|
|
64
|
+
</div>
|
|
65
|
+
</EPDialog>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
cancelLabel?: string;
|
|
6
|
+
confirmLabel?: string;
|
|
7
|
+
confirmVariant?: 'primary' | 'secondary' | 'danger';
|
|
8
|
+
confirmLoading?: boolean;
|
|
9
|
+
confirmDisabled?: boolean;
|
|
10
|
+
closeOnConfirm?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type __VLS_Slots = {
|
|
13
|
+
trigger?: () => unknown;
|
|
14
|
+
default?: () => unknown;
|
|
15
|
+
confirm?: (props: {
|
|
16
|
+
confirm: () => void;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
}) => unknown;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
cancel: () => any;
|
|
23
|
+
"update:open": (value: boolean) => any;
|
|
24
|
+
confirm: () => any;
|
|
25
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
26
|
+
onCancel?: (() => any) | undefined;
|
|
27
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
|
+
onConfirm?: (() => any) | undefined;
|
|
29
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
31
|
+
declare const _default: typeof __VLS_export;
|
|
32
|
+
export default _default;
|
|
33
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
34
|
+
new (): {
|
|
35
|
+
$slots: S;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -4,7 +4,8 @@ import EPButton from "./EPButton.vue";
|
|
|
4
4
|
defineProps({
|
|
5
5
|
open: { type: Boolean, required: false },
|
|
6
6
|
title: { type: String, required: true },
|
|
7
|
-
description: { type: String, required: false }
|
|
7
|
+
description: { type: String, required: false },
|
|
8
|
+
role: { type: String, required: false }
|
|
8
9
|
});
|
|
9
10
|
const emit = defineEmits(["update:open"]);
|
|
10
11
|
</script>
|
|
@@ -22,7 +23,10 @@ const emit = defineEmits(["update:open"]);
|
|
|
22
23
|
</DialogTrigger>
|
|
23
24
|
<DialogPortal>
|
|
24
25
|
<DialogOverlay class="eponyme-dialog-overlay ep:fixed ep:inset-0 ep:z-50 ep:bg-black/65 ep:backdrop-blur-sm" />
|
|
25
|
-
<DialogContent
|
|
26
|
+
<DialogContent
|
|
27
|
+
:role="role"
|
|
28
|
+
class="eponyme-dialog-content eponyme-portal ep:fixed ep:top-1/2 ep:left-1/2 ep:z-50 ep:w-[min(32rem,calc(100vw-2rem))] ep:rounded-2xl ep:border-0 ep:bg-theme-ep ep:p-6 ep:font-sans ep:text-text-ep ep:outline-none"
|
|
29
|
+
>
|
|
26
30
|
<DialogTitle class="ep:text-xl ep:font-semibold ep:text-white">
|
|
27
31
|
{{ title }}
|
|
28
32
|
</DialogTitle>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
+
import { middleEllipsis } from "../../utils/middle-ellipsis";
|
|
3
4
|
import EPButton from "./EPButton.vue";
|
|
4
5
|
import EPLinkDialog from "./EPLinkDialog.vue";
|
|
5
6
|
import EPTooltip from "./EPTooltip.vue";
|
|
@@ -51,7 +52,7 @@ function openLink() {
|
|
|
51
52
|
/>
|
|
52
53
|
<div class="ep:min-w-0 ep:flex-1">
|
|
53
54
|
<p class="ep:m-0 ep:truncate ep:text-sm ep:text-white">
|
|
54
|
-
{{ value.href
|
|
55
|
+
{{ value.href ? middleEllipsis(value.href, 40) : "No link configured" }}
|
|
55
56
|
</p>
|
|
56
57
|
<p class="ep:mt-0.5 ep:mb-0 ep:text-[11px] ep:text-muted-ep">
|
|
57
58
|
{{ value.type === "internal" ? "Internal" : "External" }} · {{ value.download ? "Download" : value.openInNewTab ? "New tab" : "Same tab" }}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chips plus a row of suggestion buttons, knowing nothing about Eponyme.
|
|
3
|
+
*
|
|
4
|
+
* The suggestions the developer configured are not a filtered autocomplete list: they're
|
|
5
|
+
* rendered as standing buttons below the input so an editor can see and click them directly,
|
|
6
|
+
* with no typing or listbox navigation required. `TagsInputRoot` still owns the chips, their
|
|
7
|
+
* keyboard handling and the delete buttons.
|
|
8
|
+
*/
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
modelValue?: string[];
|
|
11
|
+
/** Rendered as clickable buttons below the input, minus the ones already picked. */
|
|
12
|
+
suggestions?: readonly string[];
|
|
13
|
+
/** Whether a value outside `suggestions` may be added. */
|
|
14
|
+
allowCustom?: boolean;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
max?: number;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
"update:modelValue": (value: string[]) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
25
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
|
+
declare const _default: typeof __VLS_export;
|
|
27
|
+
export default _default;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
TagsInputItem,
|
|
5
|
+
TagsInputItemDelete,
|
|
6
|
+
TagsInputItemText,
|
|
7
|
+
TagsInputRoot
|
|
8
|
+
} from "reka-ui";
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
modelValue: { type: Array, required: false, default: () => [] },
|
|
11
|
+
suggestions: { type: Array, required: false, default: () => [] },
|
|
12
|
+
allowCustom: { type: Boolean, required: false },
|
|
13
|
+
placeholder: { type: String, required: false },
|
|
14
|
+
max: { type: Number, required: false },
|
|
15
|
+
required: { type: Boolean, required: false },
|
|
16
|
+
disabled: { type: Boolean, required: false },
|
|
17
|
+
invalid: { type: Boolean, required: false }
|
|
18
|
+
});
|
|
19
|
+
const emit = defineEmits(["update:modelValue"]);
|
|
20
|
+
const search = ref("");
|
|
21
|
+
const available = computed(() => props.suggestions.filter((suggestion) => !props.modelValue.some((tag) => same(tag, suggestion))));
|
|
22
|
+
const full = computed(() => props.max !== void 0 && props.modelValue.length >= props.max);
|
|
23
|
+
function same(left, right) {
|
|
24
|
+
return left.trim().toLocaleLowerCase() === right.trim().toLocaleLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function add(value) {
|
|
27
|
+
const tag = value.trim();
|
|
28
|
+
if (!tag || full.value) return;
|
|
29
|
+
const suggested = props.suggestions.find((suggestion) => same(suggestion, tag));
|
|
30
|
+
if (!suggested && !props.allowCustom) return;
|
|
31
|
+
if (props.modelValue.some((existing) => same(existing, tag))) return;
|
|
32
|
+
emit("update:modelValue", [...props.modelValue, suggested ?? tag]);
|
|
33
|
+
search.value = "";
|
|
34
|
+
}
|
|
35
|
+
function remove(tag) {
|
|
36
|
+
emit("update:modelValue", props.modelValue.filter((existing) => existing !== tag));
|
|
37
|
+
}
|
|
38
|
+
function onEnter(event) {
|
|
39
|
+
if (!search.value.trim()) return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
add(search.value);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div>
|
|
47
|
+
<TagsInputRoot
|
|
48
|
+
:model-value="modelValue"
|
|
49
|
+
:max="max"
|
|
50
|
+
:required="required"
|
|
51
|
+
:disabled="disabled"
|
|
52
|
+
:add-on-paste="false"
|
|
53
|
+
class="ep:flex ep:min-h-12 ep:w-full ep:flex-wrap ep:items-center ep:gap-1.5 ep:rounded-xl ep:border ep:border-border-ep ep:bg-selected-ep ep:px-2 ep:py-2 ep:transition ep:focus-within:border-muted-ep ep:focus-within:ring-2 ep:focus-within:ring-white/10 ep:aria-invalid:border-danger-ep"
|
|
54
|
+
:aria-invalid="invalid || void 0"
|
|
55
|
+
@remove-tag="remove"
|
|
56
|
+
>
|
|
57
|
+
<TagsInputItem
|
|
58
|
+
v-for="tag in modelValue"
|
|
59
|
+
:key="tag"
|
|
60
|
+
:value="tag"
|
|
61
|
+
class="ep:flex ep:items-center ep:gap-1 ep:rounded-lg ep:bg-white/10 ep:py-1 ep:pr-1 ep:pl-2.5 ep:text-sm ep:text-white"
|
|
62
|
+
>
|
|
63
|
+
<TagsInputItemText />
|
|
64
|
+
<TagsInputItemDelete
|
|
65
|
+
:aria-label="`Remove ${tag}`"
|
|
66
|
+
class="ep:flex ep:size-5 ep:cursor-pointer ep:items-center ep:justify-center ep:rounded-md ep:border-0 ep:bg-transparent ep:text-muted-ep ep:transition ep:hover:bg-white/10 ep:hover:text-white"
|
|
67
|
+
>
|
|
68
|
+
<Icon
|
|
69
|
+
name="mingcute:close-line"
|
|
70
|
+
class="ep:size-3.5"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
/>
|
|
73
|
+
</TagsInputItemDelete>
|
|
74
|
+
</TagsInputItem>
|
|
75
|
+
|
|
76
|
+
<input
|
|
77
|
+
v-model="search"
|
|
78
|
+
:placeholder="full ? void 0 : placeholder"
|
|
79
|
+
:disabled="disabled || full"
|
|
80
|
+
class="ep:min-w-24 ep:flex-1 ep:border-0 ep:bg-transparent ep:px-2 ep:text-sm ep:text-white ep:outline-none ep:placeholder:text-muted-ep ep:disabled:cursor-not-allowed"
|
|
81
|
+
@keydown.enter="onEnter"
|
|
82
|
+
>
|
|
83
|
+
</TagsInputRoot>
|
|
84
|
+
|
|
85
|
+
<div
|
|
86
|
+
v-if="available.length"
|
|
87
|
+
class="ep:mt-2 ep:flex ep:flex-wrap ep:gap-1.5"
|
|
88
|
+
>
|
|
89
|
+
<button
|
|
90
|
+
v-for="suggestion in available"
|
|
91
|
+
:key="suggestion"
|
|
92
|
+
type="button"
|
|
93
|
+
:disabled="disabled || full"
|
|
94
|
+
class="ep:cursor-pointer ep:rounded-lg ep:border ep:border-border-ep ep:bg-transparent ep:px-2.5 ep:py-1 ep:text-sm ep:text-muted-ep ep:transition ep:hover:border-muted-ep ep:hover:text-white ep:disabled:cursor-not-allowed ep:disabled:opacity-50"
|
|
95
|
+
@click="add(suggestion)"
|
|
96
|
+
>
|
|
97
|
+
{{ suggestion }}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chips plus a row of suggestion buttons, knowing nothing about Eponyme.
|
|
3
|
+
*
|
|
4
|
+
* The suggestions the developer configured are not a filtered autocomplete list: they're
|
|
5
|
+
* rendered as standing buttons below the input so an editor can see and click them directly,
|
|
6
|
+
* with no typing or listbox navigation required. `TagsInputRoot` still owns the chips, their
|
|
7
|
+
* keyboard handling and the delete buttons.
|
|
8
|
+
*/
|
|
9
|
+
type __VLS_Props = {
|
|
10
|
+
modelValue?: string[];
|
|
11
|
+
/** Rendered as clickable buttons below the input, minus the ones already picked. */
|
|
12
|
+
suggestions?: readonly string[];
|
|
13
|
+
/** Whether a value outside `suggestions` may be added. */
|
|
14
|
+
allowCustom?: boolean;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
max?: number;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
"update:modelValue": (value: string[]) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
25
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
|
+
declare const _default: typeof __VLS_export;
|
|
27
|
+
export default _default;
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import type { ComputedRef, Ref } from 'vue';
|
|
2
2
|
import type eponymeConfig from '#eponyme/config';
|
|
3
|
-
import type { EponymeCollectionDataByName, EponymeCollectionName } from '../types/index.js';
|
|
3
|
+
import type { EponymeCollectionDataByName, EponymeCollectionFilter, EponymeCollectionName } from '../types/index.js';
|
|
4
4
|
import type { EponymeCollectionEntry, EponymeSortDirection, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
|
|
5
5
|
type ConfigCollectionName = EponymeCollectionName<typeof eponymeConfig>;
|
|
6
6
|
/** Sortable keys: the entry metadata plus every field of the collection. */
|
|
7
7
|
export type EponymeCollectionSortKey<Name extends ConfigCollectionName> = 'updatedAt' | 'publishedAt' | 'title' | 'slug' | (keyof EponymeCollectionDataByName<typeof eponymeConfig, Name> & string);
|
|
8
|
+
/**
|
|
9
|
+
* The `where` a collection accepts: only the fields stored in a form the index can compare,
|
|
10
|
+
* each with the values that field can actually hold. The server rejects anything else, so
|
|
11
|
+
* the two agree rather than one silently widening the other.
|
|
12
|
+
*/
|
|
13
|
+
export type EponymeCollectionFilterInput<Name extends ConfigCollectionName> = EponymeCollectionFilter<typeof eponymeConfig, Name>;
|
|
8
14
|
export interface UseEponymeCollectionOptions<Name extends ConfigCollectionName> {
|
|
9
15
|
take?: number;
|
|
10
16
|
skip?: number;
|
|
11
17
|
orderBy?: EponymeCollectionSortKey<Name>;
|
|
12
18
|
order?: EponymeSortDirection;
|
|
19
|
+
/**
|
|
20
|
+
* Keeps only the entries matching every key. A list of values matches any of them; `not`
|
|
21
|
+
* excludes; `contains` matches a substring; and `gte`/`lte`/`gt`/`lt` compare as text,
|
|
22
|
+
* which is exact for `field.date()`, pinned by validation to `YYYY-MM-DD`.
|
|
23
|
+
*/
|
|
24
|
+
where?: EponymeCollectionFilterInput<Name>;
|
|
13
25
|
}
|
|
14
26
|
export interface UseEponymeCollectionResult<Data extends Record<string, unknown>> {
|
|
15
27
|
entries: ComputedRef<EponymeCollectionEntry<Data>[]>;
|
|
@@ -3,18 +3,43 @@ import { useEventListener } from "@vueuse/core";
|
|
|
3
3
|
import { computed } from "vue";
|
|
4
4
|
import { readPreviewQuery, readPreviewVersion } from "../utils/preview.js";
|
|
5
5
|
import { cacheForPublicRead } from "../utils/hydration-cache.js";
|
|
6
|
+
function serializeFilter(where) {
|
|
7
|
+
const query = {};
|
|
8
|
+
const parts = [];
|
|
9
|
+
const write = (name, values) => {
|
|
10
|
+
if (!values.length) return;
|
|
11
|
+
query[name] = values;
|
|
12
|
+
parts.push(`${name}=${values.join("|")}`);
|
|
13
|
+
};
|
|
14
|
+
const list = (value) => (Array.isArray(value) ? value : [value]).map(String).filter(Boolean);
|
|
15
|
+
for (const key of Object.keys(where ?? {}).sort()) {
|
|
16
|
+
const condition = where[key];
|
|
17
|
+
if (condition === void 0 || condition === null) continue;
|
|
18
|
+
if (typeof condition !== "object" || Array.isArray(condition)) {
|
|
19
|
+
write(`where[${key}]`, list(condition));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
for (const operator of Object.keys(condition).sort()) {
|
|
23
|
+
write(`where[${key}][${operator}]`, list(condition[operator]));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { query, key: parts.join(",") };
|
|
27
|
+
}
|
|
6
28
|
export function useEponymeCollection(name, options = {}) {
|
|
7
29
|
const requestFetch = useRequestFetch();
|
|
30
|
+
const filter = serializeFilter(options.where);
|
|
8
31
|
const query = {
|
|
9
32
|
take: options.take,
|
|
10
33
|
skip: options.skip,
|
|
11
34
|
orderBy: options.orderBy,
|
|
12
|
-
order: options.order
|
|
35
|
+
order: options.order,
|
|
36
|
+
...filter.query
|
|
13
37
|
};
|
|
14
38
|
const result = useAsyncData(
|
|
15
|
-
//
|
|
16
|
-
// same collection would share one cache entry
|
|
17
|
-
|
|
39
|
+
// Every option belongs in the key, otherwise two differently sorted — or differently
|
|
40
|
+
// filtered — calls to the same collection would share one cache entry and one would
|
|
41
|
+
// silently answer with the other's results.
|
|
42
|
+
`eponyme:collection:public:${name}:${options.take ?? ""}:${options.skip ?? ""}:${options.orderBy ?? ""}:${options.order ?? ""}:${filter.key}`,
|
|
18
43
|
// Always the published listing, which is publicly cacheable: the browser cache may answer it.
|
|
19
44
|
() => requestFetch(`/api/eponyme-collections/${name}`, { query }),
|
|
20
45
|
{ getCachedData: cacheForPublicRead(true) }
|
|
@@ -24,10 +24,6 @@ export interface UseEponymeFormResult<Data extends Record<string, unknown>> {
|
|
|
24
24
|
submitted: Ref<boolean>;
|
|
25
25
|
/** Name of the trap field to render hidden, or `false` when disabled. */
|
|
26
26
|
honeypot: string | false;
|
|
27
|
-
/** True when the form declares `captcha: true`; render a widget and fill `captchaToken`. */
|
|
28
|
-
requiresCaptcha: boolean;
|
|
29
|
-
/** Bind this to the captcha widget, for example `<NuxtTurnstile v-model="captchaToken" />`. */
|
|
30
|
-
captchaToken: Ref<string>;
|
|
31
27
|
submit: () => Promise<boolean>;
|
|
32
28
|
reset: () => void;
|
|
33
29
|
}
|