@karibsen/eponyme 0.5.0 → 0.7.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 (68) hide show
  1. package/README.md +51 -39
  2. package/dist/eponyme.mjs +2 -3
  3. package/dist/module.d.mts +94 -13
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +23 -6
  6. package/dist/runtime/assets/icon.ico +0 -0
  7. package/dist/runtime/assets/logo.png +0 -0
  8. package/dist/runtime/assets.d.ts +9 -0
  9. package/dist/runtime/components/editor/EponymeFolderNav.vue +1 -8
  10. package/dist/runtime/components/editor/EponymeFormPage.vue +2 -2
  11. package/dist/runtime/components/editor/EponymePreviewPanel.vue +4 -3
  12. package/dist/runtime/components/editor/EponymeSidebar.vue +7 -2
  13. package/dist/runtime/components/fields/FieldRenderer.vue +11 -0
  14. package/dist/runtime/components/fields/PhoneField.d.vue.ts +22 -0
  15. package/dist/runtime/components/fields/PhoneField.vue +61 -0
  16. package/dist/runtime/components/fields/PhoneField.vue.d.ts +22 -0
  17. package/dist/runtime/components/ui/EPLinkEditor.vue +2 -1
  18. package/dist/runtime/composables/useEponyme.d.ts +22 -14
  19. package/dist/runtime/composables/useEponyme.js +5 -3
  20. package/dist/runtime/composables/useEponymeCollection.js +6 -4
  21. package/dist/runtime/composables/useEponymeFavicon.d.ts +1 -0
  22. package/dist/runtime/composables/useEponymeFavicon.js +14 -0
  23. package/dist/runtime/composables/useEponymeForm.d.ts +0 -4
  24. package/dist/runtime/composables/useEponymeForm.js +1 -8
  25. package/dist/runtime/fields/index.d.ts +2 -0
  26. package/dist/runtime/fields/index.js +2 -1
  27. package/dist/runtime/fields/phone.d.ts +2 -0
  28. package/dist/runtime/fields/phone.js +3 -0
  29. package/dist/runtime/middleware/eponyme-auth.d.ts +1 -1
  30. package/dist/runtime/middleware/eponyme-owner.d.ts +1 -1
  31. package/dist/runtime/pages/EponymeChangePasswordPage.vue +2 -0
  32. package/dist/runtime/pages/EponymeDetailPage.vue +2 -0
  33. package/dist/runtime/pages/EponymeIndexPage.vue +2 -0
  34. package/dist/runtime/pages/EponymeLoginPage.vue +9 -4
  35. package/dist/runtime/pages/EponymeUsersPage.vue +2 -0
  36. package/dist/runtime/server/api/eponyme-forms/[path].post.js +0 -15
  37. package/dist/runtime/server/middleware/eponyme-no-store.js +4 -1
  38. package/dist/runtime/server/services/eponyme-form-store.d.ts +1 -0
  39. package/dist/runtime/server/services/eponyme-form-store.js +4 -3
  40. package/dist/runtime/server/services/eponyme-store.d.ts +11 -0
  41. package/dist/runtime/server/services/eponyme-store.js +36 -6
  42. package/dist/runtime/server/utils/eponyme-cache.d.ts +2 -14
  43. package/dist/runtime/server/utils/eponyme-cache.js +1 -6
  44. package/dist/runtime/types/eponyme-config.d.ts +0 -7
  45. package/dist/runtime/types/eponyme.d.ts +2 -5
  46. package/dist/runtime/types/field.d.ts +49 -4
  47. package/dist/runtime/types/index.d.ts +0 -1
  48. package/dist/runtime/types/index.js +0 -1
  49. package/dist/runtime/utils/cache-tags.d.ts +46 -0
  50. package/dist/runtime/utils/cache-tags.js +28 -0
  51. package/dist/runtime/utils/hydration-cache.d.ts +10 -0
  52. package/dist/runtime/utils/hydration-cache.js +3 -0
  53. package/dist/runtime/utils/middle-ellipsis.d.ts +1 -0
  54. package/dist/runtime/utils/middle-ellipsis.js +8 -0
  55. package/dist/runtime/utils/normalize-eponyme-phones.d.ts +9 -0
  56. package/dist/runtime/utils/normalize-eponyme-phones.js +38 -0
  57. package/dist/runtime/utils/normalize-phone.d.ts +26 -0
  58. package/dist/runtime/utils/normalize-phone.js +17 -0
  59. package/dist/runtime/utils/preview.d.ts +10 -0
  60. package/dist/runtime/utils/preview.js +7 -2
  61. package/dist/runtime/utils/validate-eponyme-data.js +29 -3
  62. package/package.json +3 -2
  63. package/dist/runtime/server/utils/eponyme-captcha.d.ts +0 -6
  64. package/dist/runtime/server/utils/eponyme-captcha.js +0 -14
  65. package/dist/runtime/types/captcha.d.ts +0 -16
  66. package/dist/runtime/types/captcha.js +0 -0
  67. package/dist/runtime/utils/no-captcha.d.ts +0 -7
  68. package/dist/runtime/utils/no-captcha.js +0 -5
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![Eponyme cover](https://raw.githubusercontent.com/karibsen-studio/eponyme/main/.github/assets/cover.png)
2
+
1
3
  # Eponyme
2
4
 
3
5
  [![npm version][npm-version-src]][npm-version-href]
@@ -11,7 +13,7 @@ Define your content in `eponyme.config.ts`. Eponyme provides defaults, validatio
11
13
 
12
14
  ## Features
13
15
 
14
- - Type-safe fields for text, slugs, rich text, numbers, booleans, images, links, dates, colors, sections, tabs, and arrays
16
+ - Type-safe fields for text, slugs, rich text, numbers, booleans, images, links, phone numbers, dates, colors, sections, tabs, and arrays
15
17
  - Declarative defaults and validation
16
18
  - Conditional fields, character counters, and sortable arrays
17
19
  - Private drafts with explicit publishing
@@ -42,6 +44,13 @@ Install the module:
42
44
  pnpm add @karibsen/eponyme
43
45
  ```
44
46
 
47
+ Eponyme brings its own components, so `@nuxt/ui` is not required. If your application does use
48
+ it, it has to be `^4.10.0` or newer: both ship Tailwind, and an older `@nuxt/ui` resolves a
49
+ version the dashboard was not built against. What that breaks is not predictable — broken
50
+ dashboard styles are the visible case, but it can also fail the build or misbehave elsewhere with
51
+ no obvious link back to the cause. The module checks the installed version and warns at build
52
+ time.
53
+
45
54
  Add it to `nuxt.config.ts`:
46
55
 
47
56
  ```ts
@@ -85,8 +94,16 @@ export default defineEponymeConfig({
85
94
  label: 'Contact email',
86
95
  defaultValue: 'hello@example.com',
87
96
  }),
97
+ contactPhone: field.phone({
98
+ label: 'Contact phone',
99
+ defaultCountry: 'FR',
100
+ countries: ['FR', 'BE'],
101
+ autocomplete: 'tel',
102
+ }),
88
103
  callToAction: field.url({
89
104
  label: 'Call to action',
105
+ // Schemes an external link may use; defaults to ['http', 'https']
106
+ protocols: ['https', 'mailto'],
90
107
  defaultValue: {
91
108
  href: '/contact',
92
109
  type: 'internal',
@@ -152,6 +169,39 @@ field.string({
152
169
  })
153
170
  ```
154
171
 
172
+ ### Phone numbers
173
+
174
+ `field.phone()` stores its value in **E.164** — `+33611131143` — whatever format it was typed
175
+ in. Normalisation happens on the server before the write, so the stored format is a guarantee
176
+ rather than a convention a client could skip.
177
+
178
+ ```ts
179
+ phone: field.phone({
180
+ label: 'Phone',
181
+ // ISO 3166-1 alpha-2, autocompleted from libphonenumber-js
182
+ countries: ['FR', 'BE'],
183
+ // Country a number typed without `+` belongs to
184
+ defaultCountry: 'FR',
185
+ autocomplete: 'tel',
186
+ placeholder: '06 11 13 11 43',
187
+ required: true,
188
+ })
189
+ ```
190
+
191
+ | Option | Effect |
192
+ |---|---|
193
+ | `countries` | Countries whose numbers are accepted. A number resolving to any other is refused, with an error naming the country it came from. Omitted, every country is accepted |
194
+ | `defaultCountry` | Country assumed for a number written without an international prefix. Without it, only `+…` numbers can be understood |
195
+ | `detectCountry` | `false` requires the international `+…` form, so the country is never guessed. Defaults to `true` |
196
+ | `autocomplete` | `tel`, `tel-national` or `tel-country-code`, set on the input |
197
+ | `defaultValue`, `required`, `placeholder`, `label`, `description` | As on every field |
198
+
199
+ Validation uses the `min` metadata bundle of `libphonenumber-js`, a fraction of the size of the
200
+ full one. It parses and formats every country, and is slightly more permissive: a few numbers
201
+ that are impossible for their region still pass.
202
+
203
+ A phone field is also allowed in a public `form()`.
204
+
155
205
  ## Prisma
156
206
 
157
207
  Eponyme uses the Prisma client owned by your application. It does not create the connection or run migrations.
@@ -740,44 +790,6 @@ Two protections apply to the public submission route:
740
790
  - a **body size limit**, 64 KB by default, configurable with `maxBodyBytes`. An oversized
741
791
  body is refused with `413` before it is parsed.
742
792
 
743
- ### Captcha
744
-
745
- Set `captcha: true` on the form, then install an adapter — Eponyme defines the contract
746
- but ships no provider:
747
-
748
- ```bash
749
- pnpm add @karibsen/eponyme-captcha
750
- ```
751
-
752
- ```ts
753
- export default defineNuxtConfig({
754
- modules: ['@karibsen/eponyme', '@karibsen/eponyme-captcha'],
755
- eponymeCaptcha: { siteKey: '<your-turnstile-site-key>' },
756
- })
757
- ```
758
-
759
- Render the widget and bind its token; `useEponymeForm` sends it alongside the fields and
760
- the server verifies it before validating anything:
761
-
762
- ```vue
763
- <script setup lang="ts">
764
- const { captchaToken, requiresCaptcha, submit } = useEponymeForm('contact')
765
- </script>
766
-
767
- <template>
768
- <NuxtTurnstile v-if="requiresCaptcha" v-model="captchaToken" />
769
- </template>
770
- ```
771
-
772
- **It fails closed.** Without an adapter, a form declaring `captcha: true` answers `500`
773
- rather than accepting submissions unprotected. A rejected token answers `422` with a
774
- `_form` error; the provider's reason stays in the server log, since telling a bot which
775
- check it failed only helps it.
776
-
777
- Writing an adapter for another provider means supplying a `name` and an
778
- `async verify(token, { ip })` returning `{ success, reason? }`, then pointing the
779
- `#eponyme/captcha` alias at it.
780
-
781
793
  Rate limiting and file uploads are not implemented yet.
782
794
 
783
795
  ## Hooks
package/dist/eponyme.mjs CHANGED
@@ -11,7 +11,7 @@ function collection(options) {
11
11
  return { __eponymeCollection: true, ...options };
12
12
  }
13
13
 
14
- const PUBLIC_FIELD_TYPES = ["string", "textarea", "email", "url", "number", "boolean", "select", "radio", "checkboxGroup"];
14
+ const PUBLIC_FIELD_TYPES = ["string", "textarea", "email", "phone", "url", "number", "boolean", "select", "radio", "checkboxGroup"];
15
15
  const DEFAULT_HONEYPOT = "_eponyme_hp";
16
16
  const DEFAULT_MAX_BODY_BYTES = 64 * 1024;
17
17
  function form(options) {
@@ -31,8 +31,7 @@ function form(options) {
31
31
  // Storing a submission has to be an explicit decision, so `custom` is the default.
32
32
  submission: { mode: options.submission?.mode ?? "custom" },
33
33
  honeypot,
34
- maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
35
- captcha: options.captcha ?? false
34
+ maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
36
35
  };
37
36
  }
38
37
 
package/dist/module.d.mts CHANGED
@@ -5,37 +5,118 @@ export { EPONYME_ROLES, canEditEponyme, isEponymeRole } from '../dist/runtime/ty
5
5
  export { ArrayFieldDefinition, ArrayFieldOptions, ArrayItemDefinition, ArrayItemFieldDefinition, ArrayItemSchema, ArrayItemValue, BooleanFieldDefinition, BooleanFieldOptions, CheckboxGroupFieldDefinition, CheckboxGroupFieldOptions, CollectionSlugField, ColorFieldDefinition, ColorFieldOptions, DateFieldDefinition, DateFieldOptions, EmailFieldDefinition, EmailFieldOptions, EponymeAuthSession, EponymeAuthUser, EponymeCollectionDataByName, EponymeCollectionDefinition, EponymeCollectionDefinitionBase, EponymeCollectionName, EponymeCollectionOptions, EponymeConfig, EponymeData, EponymeDataByName, EponymeFormDataByName, EponymeFormDefinition, EponymeFormDefinitionBase, EponymeFormFieldDefinition, EponymeFormMode, EponymeFormName, EponymeFormOptions, EponymeFormSchema, EponymeManagedUserResult, EponymeName, EponymeRole, EponymeSchema, EponymeVariableDefinition, EponymeVariableSummary, EponymeVariableValue, EponymeVariables, FieldDefinition, FieldValidationResult, FieldValidator, FieldVisibilityCondition, ImageFieldDefinition, ImageFieldOptions, NumberFieldDefinition, NumberFieldOptions, RadioFieldDefinition, RadioFieldOptions, RichTextFieldDefinition, RichTextFieldOptions, SectionFieldDefinition, SectionFieldOptions, SectionItemFieldDefinition, SectionItemValue, SectionSchema, SectionValue, SelectFieldDefinition, SelectFieldOptions, SelectOption, SlugFieldDefinition, SlugFieldOptions, StringFieldDefinition, StringFieldOptions, TabDefinition, TabFieldDefinition, TabFieldOptions, TabsSchema, TabsValue, TextareaFieldDefinition, TextareaFieldOptions, TodayDateDefault, UrlFieldDefinition, UrlFieldOptions, UrlType, UrlValue } from '../dist/runtime/types/index.js';
6
6
  import '../dist/runtime/types/variables.js';
7
7
 
8
+ /**
9
+ * Options read from the `eponyme` key of `nuxt.config`.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * export default defineNuxtConfig({
14
+ * modules: ['@karibsen/eponyme'],
15
+ * eponyme: {
16
+ * prismaClient: '~~/server/utils/prisma',
17
+ * previewPaths: {
18
+ * homepage: '/',
19
+ * articles: '/articles/:slug',
20
+ * },
21
+ * },
22
+ * })
23
+ * ```
24
+ */
8
25
  interface ModuleOptions {
26
+ /**
27
+ * Route the dashboard is served on. Surrounding slashes are normalised, so `__eponyme` and
28
+ * `/__eponyme/` are equivalent.
29
+ *
30
+ * @default "/__eponyme"
31
+ */
9
32
  dashboardPath?: string;
10
- /** Public routes used for previews and sitemap entries. Collections use a `:slug` placeholder. */
33
+ /**
34
+ * Public route rendering each entry, keyed by singleton name or by collection name. A
35
+ * collection value must contain `:slug`, which is replaced by the entry slug.
36
+ *
37
+ * @remarks
38
+ * An entry left out of this map has no preview, no sitemap entry, and no cache tag on the page
39
+ * that renders it.
40
+ *
41
+ * @default {}
42
+ * @example
43
+ * ```ts
44
+ * previewPaths: {
45
+ * homepage: '/',
46
+ * articles: '/articles/:slug',
47
+ * }
48
+ * ```
49
+ */
11
50
  previewPaths?: Record<string, string>;
12
- /** Path to a server module exporting an already initialized PrismaClient. */
51
+ /**
52
+ * Module specifier of a server file whose default export is an initialised `PrismaClient`.
53
+ * Nuxt aliases apply, and keep their usual meaning: a server file lives under `~~/`, not `~/`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * prismaClient: '~~/server/utils/prisma'
58
+ * ```
59
+ * @see https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/introduction
60
+ */
13
61
  prismaClient: string;
14
- /** Default palette offered by every color field; a field can override it with its own `presets`. */
62
+ /**
63
+ * Palette offered by every color field, which a field overrides with its own `presets`.
64
+ *
65
+ * @remarks
66
+ * A bare string is labelled from its value. An entry that is not a valid hex colour is dropped.
67
+ *
68
+ * @default []
69
+ * @example
70
+ * ```ts
71
+ * colorPresets: [
72
+ * { label: 'Ink', value: '#171714' },
73
+ * '#ffffff',
74
+ * ]
75
+ * ```
76
+ */
15
77
  colorPresets?: Array<string | {
16
78
  label: string;
17
79
  value: string;
18
80
  }>;
19
81
  auth?: {
20
- /** Fixed lifetime of an authenticated session. */
82
+ /**
83
+ * Lifetime of an authenticated session, in days, counted from sign-in and never extended by
84
+ * activity.
85
+ *
86
+ * @default 7
87
+ */
21
88
  sessionDurationDays?: number;
22
89
  };
23
90
  /**
24
- * How long a server instance may reuse content it has already read, in seconds.
25
- * A save clears what it changed on its own instance, so this only bounds how long
26
- * another instance can still serve the previous content. `0` disables the cache.
91
+ * How long a server instance may reuse content it has already read, in seconds. `0` disables it.
92
+ *
93
+ * @remarks
94
+ * A save only clears the instance that served it, so this bounds how long another instance can
95
+ * still answer with the previous content.
96
+ *
97
+ * @default 5
27
98
  */
28
99
  cacheSeconds?: number;
29
100
  /**
30
- * How long a browser may reuse published content, in seconds. This is what makes a
31
- * client-side navigation instant, and it is the one window nobody can purge: a visitor
32
- * who already holds a copy keeps it until it expires. Keep it short. `0` disables it.
101
+ * How long a browser may reuse published content, in seconds. `0` disables it.
102
+ *
103
+ * @remarks
104
+ * A browser cache cannot be purged: a visitor holding a copy keeps it until it expires, whatever
105
+ * is published meanwhile.
106
+ *
107
+ * @default 30
108
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
33
109
  */
34
110
  browserCacheSeconds?: number;
35
111
  /**
36
- * How long a CDN may reuse published content, in seconds. A CDN can be purged — the
37
- * `eponyme:entry:published` hook is the place to do it — so this can be generous.
38
- * `0` disables it.
112
+ * How long a CDN may reuse published content, in seconds. `0` disables it.
113
+ *
114
+ * @remarks
115
+ * Sent as `s-maxage`, with `stale-while-revalidate` twelve times longer. Cached responses carry
116
+ * cache tags, so a publication can purge them from the `eponyme:entry:published` hook.
117
+ *
118
+ * @default 300
119
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
39
120
  */
40
121
  cdnCacheSeconds?: number;
41
122
  }
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@karibsen/eponyme",
3
3
  "configKey": "eponyme",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,8 +1,21 @@
1
- import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, useLogger, createResolver, findPath, addImports, addImportsDir, addServerImports, addRouteMiddleware, addTypeTemplate, addServerHandler, addServerPlugin, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion } from '@nuxt/kit';
2
2
  import pc from 'picocolors';
3
3
  import tailwindcss from '@tailwindcss/vite';
4
4
  import { resolve } from 'node:path';
5
+ import { tagPreviewPathRoutes } from '../dist/runtime/utils/cache-tags.js';
5
6
 
7
+ const MINIMUM_NUXT_UI = "4.10.0";
8
+ async function warnOnIncompatibleNuxtUi(nuxt, logger) {
9
+ if (!hasNuxtModule("@nuxt/ui", nuxt)) return;
10
+ if (await hasNuxtModuleCompatibility("@nuxt/ui", `>=${MINIMUM_NUXT_UI}`, nuxt)) return;
11
+ const found = await getNuxtModuleVersion("@nuxt/ui", nuxt);
12
+ logger.warn(
13
+ `@nuxt/ui ${found || "(unknown version)"} is older than ${MINIMUM_NUXT_UI}, which Eponyme needs in order to share Tailwind with it.
14
+ Upgrade it with \`npm i @nuxt/ui@^${MINIMUM_NUXT_UI}\`. Keeping an older one resolves a Tailwind this module was not built
15
+ against, and what that breaks is not predictable: broken dashboard styles are the visible case, but it can also fail the
16
+ build or misbehave somewhere with no obvious link back to this warning.`
17
+ );
18
+ }
6
19
  const module$1 = defineNuxtModule({
7
20
  meta: {
8
21
  name: "@karibsen/eponyme",
@@ -63,6 +76,7 @@ const module$1 = defineNuxtModule({
63
76
  async setup(options, nuxt) {
64
77
  const logger = useLogger("eponyme");
65
78
  const resolver = createResolver(import.meta.url);
79
+ await warnOnIncompatibleNuxtUi(nuxt, logger);
66
80
  const dashboardPath = `/${(options.dashboardPath || "__eponyme").replace(/^\/+|\/+$/g, "")}`;
67
81
  const publicRuntimeConfig = nuxt.options.runtimeConfig.public;
68
82
  publicRuntimeConfig.eponyme = {
@@ -78,6 +92,14 @@ const module$1 = defineNuxtModule({
78
92
  browserCacheSeconds: options.browserCacheSeconds ?? 30,
79
93
  cdnCacheSeconds: options.cdnCacheSeconds ?? 300
80
94
  };
95
+ const tagged = tagPreviewPathRoutes(options.previewPaths ?? {}, nuxt.options.routeRules ??= {});
96
+ if (tagged.length) {
97
+ const width = Math.max(...tagged.map(({ route }) => route.length));
98
+ logger.info(
99
+ `Cache tags added to ${tagged.length} public route${tagged.length > 1 ? "s" : ""}, so publishing can purge them:
100
+ ` + tagged.map(({ route, tag }) => ` ${route.padEnd(width)} ${tag}`).join("\n")
101
+ );
102
+ }
81
103
  const preparing = Boolean(nuxt.options._prepare);
82
104
  if (!options.prismaClient) {
83
105
  if (!preparing) throw new Error(`${pc.red("[Eponyme]")} eponyme.prismaClient is required.`);
@@ -141,7 +163,6 @@ Note that '~/' points at ${nuxt.options.srcDir}, so a server module needs '~~/'.
141
163
  nuxt.options.alias["#eponyme/prisma"] = prismaPath;
142
164
  const variablesPath = await findPath(resolve(nuxt.options.rootDir, "eponyme.variables.ts"));
143
165
  nuxt.options.alias["#eponyme/variables"] = variablesPath ?? resolver.resolve("./runtime/utils/empty-variables");
144
- nuxt.options.alias["#eponyme/captcha"] ??= resolver.resolve("./runtime/utils/no-captcha");
145
166
  addTypeTemplate({
146
167
  filename: "types/eponyme-config.d.ts",
147
168
  getContents: () => `declare module '#eponyme/config' {
@@ -152,10 +173,6 @@ declare module '#eponyme/variables' {
152
173
  const variables: typeof import(${JSON.stringify(nuxt.options.alias["#eponyme/variables"])})['default']
153
174
  export default variables
154
175
  }
155
- declare module '#eponyme/captcha' {
156
- const verifier: import('${resolver.resolve("./runtime/types/captcha")}').EponymeCaptchaVerifier
157
- export default verifier
158
- }
159
176
  `
160
177
  });
161
178
  addServerHandler({ middleware: true, handler: resolver.resolve("./runtime/server/middleware/eponyme-no-store") });
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ declare module '*.ico?url' {
2
+ const url: string
3
+ export default url
4
+ }
5
+
6
+ declare module '*.png?url' {
7
+ const url: string
8
+ export default url
9
+ }
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
+ import { middleEllipsis } from "../../utils/middle-ellipsis";
3
4
  const props = defineProps({
4
5
  basePath: { type: String, required: true },
5
6
  eponyme: { type: String, required: true }
@@ -24,14 +25,6 @@ const visibleBreadcrumbs = computed(() => {
24
25
  function label(name) {
25
26
  return name.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
26
27
  }
27
- function middleEllipsis(value, maxLength = 28) {
28
- const characters = [...value];
29
- if (characters.length <= maxLength) return value;
30
- const available = maxLength - 1;
31
- const startLength = Math.ceil(available / 2);
32
- const endLength = Math.floor(available / 2);
33
- return `${characters.slice(0, startLength).join("")}\u2026${characters.slice(-endLength).join("")}`;
34
- }
35
28
  function updateBreadcrumbMenuScroll(event) {
36
29
  const menu = event.currentTarget;
37
30
  if (!menu) return;
@@ -171,11 +171,11 @@ async function deleteSubmission(submission) {
171
171
  v-for="header in headerGroup.headers"
172
172
  :key="header.id"
173
173
  scope="col"
174
- class="ep:border-b ep:border-border-ep ep:p-3 ep:text-[11px] ep:font-semibold ep:tracking-widest ep:text-muted-ep ep:uppercase"
174
+ class="ep:border-b ep:border-border-ep ep:p-3 ep:text-[11px] ep:font-semibold ep:text-muted-ep ep:uppercase"
175
175
  >
176
176
  <button
177
177
  type="button"
178
- class="ep:flex ep:cursor-pointer ep:items-center ep:gap-1 ep:border-0 ep:bg-transparent ep:p-0 ep:text-inherit ep:uppercase"
178
+ class="ep:flex ep:cursor-pointer ep:items-center ep:gap-1 ep:border-0 ep:bg-transparent ep:p-0 ep:text-inherit"
179
179
  :aria-sort="header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : 'none'"
180
180
  @click="header.column.toggleSorting()"
181
181
  >
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
3
3
  import EPButton from "../ui/EPButton.vue";
4
4
  import EPSelect from "../ui/EPSelect.vue";
5
5
  import { formatVersionDate, useEponymeHistory } from "../../composables/useEponymeHistory";
6
+ import { EPONYME_PREVIEW_QUERY, EPONYME_PREVIEW_TOKEN_QUERY, EPONYME_PREVIEW_VERSION_QUERY } from "../../utils/preview";
6
7
  const props = defineProps({
7
8
  name: { type: String, required: true },
8
9
  path: { type: String, required: true },
@@ -25,9 +26,9 @@ const options = computed(() => [
25
26
  ]);
26
27
  const previewUrl = computed(() => {
27
28
  const url = new URL(props.path, "http://placeholder");
28
- url.searchParams.set("__eponyme_preview", props.name);
29
- url.searchParams.set("__eponyme_preview_version", selected.value);
30
- url.searchParams.set("__eponyme_preview_token", previewToken.value);
29
+ url.searchParams.set(EPONYME_PREVIEW_QUERY, props.name);
30
+ url.searchParams.set(EPONYME_PREVIEW_VERSION_QUERY, selected.value);
31
+ url.searchParams.set(EPONYME_PREVIEW_TOKEN_QUERY, previewToken.value);
31
32
  return `${url.pathname}${url.search}`;
32
33
  });
33
34
  async function reload() {
@@ -12,6 +12,7 @@ import { useEponymeAuth } from "../../composables/useEponymeAuth";
12
12
  import EPAvatar from "../ui/EPAvatar.vue";
13
13
  import EPButton from "../ui/EPButton.vue";
14
14
  import EPInputText from "../ui/EPInputText.vue";
15
+ import logoUrl from "../../assets/logo.png?url";
15
16
  const props = defineProps({
16
17
  basePath: { type: String, required: true }
17
18
  });
@@ -128,9 +129,13 @@ watch(() => route.path, () => {
128
129
  <NuxtLink
129
130
  v-if="!collapsed"
130
131
  :to="normalizedBasePath"
131
- class="ep:flex ep:min-w-0 ep:items-center ep:gap-2 ep:px-3 ep:text-sm ep:font-bold ep:tracking-tight ep:text-white ep:no-underline"
132
+ class="ep:flex ep:min-w-0 ep:items-center ep:no-underline"
132
133
  >
133
- Eponyme
134
+ <img
135
+ :src="logoUrl"
136
+ alt="Eponyme"
137
+ class="ep:block ep:size-8"
138
+ >
134
139
  </NuxtLink>
135
140
  <button
136
141
  type="button"
@@ -5,6 +5,7 @@ import CheckboxGroupField from "./CheckboxGroupField.vue";
5
5
  import ColorField from "./ColorField.vue";
6
6
  import DateField from "./DateField.vue";
7
7
  import NumberField from "./NumberField.vue";
8
+ import PhoneField from "./PhoneField.vue";
8
9
  import RadioField from "./RadioField.vue";
9
10
  import RichTextField from "./RichTextField.vue";
10
11
  import SelectField from "./SelectField.vue";
@@ -59,6 +60,8 @@ const component = computed(() => {
59
60
  return ColorField;
60
61
  case "url":
61
62
  return UrlField;
63
+ case "phone":
64
+ return PhoneField;
62
65
  default:
63
66
  return TextField;
64
67
  }
@@ -95,6 +98,14 @@ const specificProps = computed(() => {
95
98
  return { presets: field.options.presets };
96
99
  case "url":
97
100
  return { placeholder: field.options.placeholder };
101
+ case "phone":
102
+ return {
103
+ placeholder: field.options.placeholder,
104
+ countries: field.options.countries,
105
+ defaultCountry: field.options.defaultCountry,
106
+ detectCountry: field.options.detectCountry,
107
+ autocomplete: field.options.autocomplete
108
+ };
98
109
  default: {
99
110
  const options = field.options;
100
111
  return {
@@ -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;
@@ -0,0 +1,61 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import EPFormField from "../ui/EPFormField.vue";
4
+ import EPInputText from "../ui/EPInputText.vue";
5
+ import { normalizeEponymePhone } from "../../utils/normalize-phone";
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
+ placeholder: { type: String, required: false },
13
+ countries: { type: Array, required: false },
14
+ defaultCountry: { type: String, required: false },
15
+ detectCountry: { type: Boolean, required: false },
16
+ autocomplete: { 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 value = computed(() => typeof props.modelValue === "string" ? props.modelValue : "");
22
+ const options = computed(() => ({
23
+ countries: props.countries,
24
+ defaultCountry: props.defaultCountry,
25
+ detectCountry: props.detectCountry
26
+ }));
27
+ const parsed = computed(() => normalizeEponymePhone(value.value, options.value));
28
+ function normalize() {
29
+ const next = parsed.value.e164;
30
+ if (next && next !== value.value) emit("update:modelValue", next);
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <EPFormField
36
+ :id="id"
37
+ :label="label"
38
+ :description="description"
39
+ :required="required"
40
+ :errors="errors"
41
+ >
42
+ <EPInputText
43
+ :id="id"
44
+ :model-value="value"
45
+ type="tel"
46
+ :autocomplete="autocomplete"
47
+ :placeholder="placeholder"
48
+ :required="required"
49
+ :invalid="Boolean(errors.length)"
50
+ :disabled="disabled"
51
+ @update:model-value="emit('update:modelValue', String($event))"
52
+ @blur="normalize"
53
+ />
54
+ <p
55
+ v-if="parsed.e164 && parsed.country"
56
+ class="ep:mt-1.5 ep:mb-0 ep:text-[11px] ep:text-muted-ep"
57
+ >
58
+ {{ parsed.country }} · {{ parsed.e164 }}
59
+ </p>
60
+ </EPFormField>
61
+ </template>
@@ -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,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" }}
@@ -1,4 +1,4 @@
1
- import type { Ref } from 'vue';
1
+ import type { ComputedRef, Ref } from 'vue';
2
2
  import type eponymeConfig from '#eponyme/config';
3
3
  import type { EponymeDataByName, EponymeName } from '../types/index.js';
4
4
  import type { EponymeAction, EponymeStatus, EponymeVersionSelector } from '../server/services/eponyme-store.js';
@@ -9,18 +9,26 @@ export interface UseEponymeOptions {
9
9
  /** Keep `{{ variables }}` unresolved. The dashboard editor needs the source text. */
10
10
  raw?: boolean;
11
11
  }
12
- /** Read and save a configured eponyme through the public or draft API. */
13
- export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): {
14
- data: Ref<EponymeDataByName<any, Name> | undefined>;
15
- status: import("vue").ComputedRef<EponymeStatus>;
16
- publishedAt: import("vue").ComputedRef<string | null>;
17
- pending: import("vue").ComputedRef<boolean>;
18
- error: Ref<import("nuxt/app").NuxtError<unknown> | undefined, import("nuxt/app").NuxtError<unknown> | undefined>;
19
- errors: Ref<ValidationErrors, ValidationErrors>;
12
+ /**
13
+ * Named, and annotated on the function below, rather than left to inference.
14
+ *
15
+ * An inferred return type has to be expanded when the declaration files are generated, and
16
+ * the expansion loses `typeof eponymeConfig`: it came out as `EponymeDataByName<any, Name>`,
17
+ * which collapses every field to the union of all possible field values. Source and
18
+ * playground were unaffected — they read `src` — so only installed versions of the package
19
+ * lost their types, on the one composable everything else goes through.
20
+ */
21
+ export interface UseEponymeResult<Data extends Record<string, unknown>> {
22
+ data: Ref<Data | undefined>;
23
+ status: ComputedRef<EponymeStatus>;
24
+ publishedAt: ComputedRef<string | null>;
25
+ pending: ComputedRef<boolean>;
26
+ error: Ref<Error | null | undefined>;
27
+ errors: Ref<ValidationErrors>;
20
28
  refresh: () => Promise<void>;
21
- save: {
22
- (): Promise<EponymeDataByName<any, Name> | undefined>;
23
- (patch: Partial<EponymeDataByName<any, Name>>, action?: EponymeAction): Promise<EponymeDataByName<any, Name> | undefined>;
24
- };
25
- };
29
+ /** Without a patch, saves the data currently held. Publishes unless told otherwise. */
30
+ save: (patch?: Partial<Data>, action?: EponymeAction) => Promise<Data | undefined>;
31
+ }
32
+ /** Read and save a configured eponyme through the public or draft API. */
33
+ export declare function useEponyme<const Name extends ConfigEponymeName>(name: Name, options?: UseEponymeOptions): UseEponymeResult<EponymeDataByName<typeof eponymeConfig, Name>>;
26
34
  export {};