@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.
Files changed (74) hide show
  1. package/README.md +109 -45
  2. package/dist/eponyme.mjs +2 -3
  3. package/dist/module.d.mts +109 -13
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +19 -6
  6. package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
  7. package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
  8. package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
  9. package/dist/runtime/components/fields/FieldRenderer.vue +22 -0
  10. package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
  11. package/dist/runtime/components/fields/PhoneField.vue +61 -0
  12. package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
  13. package/dist/runtime/components/fields/RichTextField.vue +37 -1
  14. package/dist/runtime/components/fields/TagsField.d.vue.ts +21 -0
  15. package/dist/runtime/components/fields/TagsField.vue +55 -0
  16. package/dist/runtime/components/fields/TagsField.vue.d.ts +21 -0
  17. package/dist/runtime/components/ui/EPAlertDialog.d.vue.ts +37 -0
  18. package/dist/runtime/components/ui/EPAlertDialog.vue +66 -0
  19. package/dist/runtime/components/ui/EPAlertDialog.vue.d.ts +37 -0
  20. package/dist/runtime/components/ui/EPDialog.d.vue.ts +1 -0
  21. package/dist/runtime/components/ui/EPDialog.vue +6 -2
  22. package/dist/runtime/components/ui/EPDialog.vue.d.ts +1 -0
  23. package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
  24. package/dist/runtime/components/ui/EPTagsInput.d.vue.ts +27 -0
  25. package/dist/runtime/components/ui/EPTagsInput.vue +101 -0
  26. package/dist/runtime/components/ui/EPTagsInput.vue.d.ts +27 -0
  27. package/dist/runtime/composables/useEponymeCollection.d.ts +13 -1
  28. package/dist/runtime/composables/useEponymeCollection.js +29 -4
  29. package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
  30. package/dist/runtime/composables/useEponymeForm.js +1 -8
  31. package/dist/runtime/fields/index.d.ts +4 -0
  32. package/dist/runtime/fields/index.js +3 -1
  33. package/dist/runtime/fields/phone.d.ts +2 -0
  34. package/dist/runtime/fields/phone.js +3 -0
  35. package/dist/runtime/fields/tags.d.ts +6 -0
  36. package/dist/runtime/fields/tags.js +3 -0
  37. package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
  38. package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
  39. package/dist/runtime/pages/EponymeIndexPage.vue +0 -3
  40. package/dist/runtime/server/api/eponyme-collections/[path].get.js +40 -1
  41. package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
  42. package/dist/runtime/server/plugins/eponyme-sync.js +11 -1
  43. package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
  44. package/dist/runtime/server/services/eponyme-form-store.js +4 -3
  45. package/dist/runtime/server/services/eponyme-store.d.ts +289 -30
  46. package/dist/runtime/server/services/eponyme-store.js +405 -73
  47. package/dist/runtime/server/utils/eponyme-reindex.d.ts +13 -0
  48. package/dist/runtime/server/utils/eponyme-reindex.js +4 -0
  49. package/dist/runtime/types/eponyme-config.d.ts +0 -7
  50. package/dist/runtime/types/eponyme.d.ts +54 -6
  51. package/dist/runtime/types/field.d.ts +82 -4
  52. package/dist/runtime/types/index.d.ts +0 -1
  53. package/dist/runtime/types/index.js +0 -1
  54. package/dist/runtime/utils/eponyme-entry-index.d.ts +39 -0
  55. package/dist/runtime/utils/eponyme-entry-index.js +51 -0
  56. package/dist/runtime/utils/get-field-default-value.js +2 -0
  57. package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
  58. package/dist/runtime/utils/middle-ellipsis.js +8 -0
  59. package/dist/runtime/utils/normalize-eponyme-values.d.ts +13 -0
  60. package/dist/runtime/utils/normalize-eponyme-values.js +40 -0
  61. package/dist/runtime/utils/normalize-phone.d.ts +26 -0
  62. package/dist/runtime/utils/normalize-phone.js +17 -0
  63. package/dist/runtime/utils/normalize-tags.d.ts +10 -0
  64. package/dist/runtime/utils/normalize-tags.js +17 -0
  65. package/dist/runtime/utils/validate-eponyme-data.js +45 -3
  66. package/dist/runtime/utils/variables.d.ts +4 -0
  67. package/dist/runtime/utils/variables.js +8 -1
  68. package/package.json +4 -2
  69. package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
  70. package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
  71. package/dist/runtime/types/captcha.d.ts +0 -16
  72. package/dist/runtime/types/captcha.js +0 -0
  73. package/dist/runtime/utils/no-captcha.d.ts +0 -7
  74. 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
+ };
@@ -2,6 +2,7 @@ type __VLS_Props = {
2
2
  open?: boolean;
3
3
  title: string;
4
4
  description?: string;
5
+ role?: 'dialog' | 'alertdialog';
5
6
  };
6
7
  declare var __VLS_16: {}, __VLS_47: {};
7
8
  type __VLS_Slots = {} & {
@@ -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 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">
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>
@@ -2,6 +2,7 @@ type __VLS_Props = {
2
2
  open?: boolean;
3
3
  title: string;
4
4
  description?: string;
5
+ role?: 'dialog' | 'alertdialog';
5
6
  };
6
7
  declare var __VLS_16: {}, __VLS_47: {};
7
8
  type __VLS_Slots = {} & {
@@ -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 || "No link configured" }}
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
- // The options belong in the key, otherwise two differently sorted calls to the
16
- // same collection would share one cache entry.
17
- `eponyme:collection:public:${name}:${options.take ?? ""}:${options.skip ?? ""}:${options.orderBy ?? ""}:${options.order ?? ""}`,
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
  }