@wssto2/vue-core 0.16.0 → 0.17.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 (146) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -3
  3. package/bin/architecture/engine.mjs +3 -0
  4. package/bin/architecture/permissions.mjs +56 -0
  5. package/bin/architecture/rules.mjs +17 -0
  6. package/bin/commands/check-architecture.mjs +19 -0
  7. package/bin/config.mjs +2 -0
  8. package/dist/{AccessGate.vue_vue_type_script_setup_true_lang-BH3f0hda.js → AccessGate.vue_vue_type_script_setup_true_lang-Bg8JFAOT.js} +1 -1
  9. package/dist/{ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js → ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js} +4 -4
  10. package/dist/{Activity-DOUuIkZR.js → Activity-CiYn5E6A.js} +7 -7
  11. package/dist/{AdaptivePageShell.vue_vue_type_script_setup_true_lang-d_dPurWG.js → AdaptivePageShell.vue_vue_type_script_setup_true_lang-oXWQTfBs.js} +1 -1
  12. package/dist/{Changes-BfcbVgmu.js → Changes-Cy8STpLX.js} +3 -3
  13. package/dist/{CollectionPage.vue_vue_type_script_setup_true_lang-B5hSkJ2N.js → CollectionPage.vue_vue_type_script_setup_true_lang-Dr6nFNBB.js} +6 -6
  14. package/dist/{DataTable.vue_vue_type_script_setup_true_lang-DbRS37n3.js → DataTable.vue_vue_type_script_setup_true_lang-D372wlxR.js} +1 -1
  15. package/dist/{DateField.vue_vue_type_script_setup_true_lang-DzCvYHAf.js → DateField.vue_vue_type_script_setup_true_lang-D0KcfW8R.js} +17 -16
  16. package/dist/{DeadLetters-CuMvfXH8.js → DeadLetters-DhJkMpd_.js} +4 -4
  17. package/dist/{FormGroup.vue_vue_type_script_setup_true_lang-dsO1m9Mm.js → FormGroup.vue_vue_type_script_setup_true_lang-lvAuBG-0.js} +2 -2
  18. package/dist/FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js +160 -0
  19. package/dist/{FormView.vue_vue_type_script_setup_true_lang-Bq_Qa4py.js → FormView.vue_vue_type_script_setup_true_lang-D01Jr4UU.js} +5 -5
  20. package/dist/{General-B5vcSEhP.js → General-CwTPC5UP.js} +10 -10
  21. package/dist/{IncidentRecord-wmStcEag.js → IncidentRecord-hXEGuLGg.js} +13 -13
  22. package/dist/{IncidentsList-CFx5tsxP.js → IncidentsList-C41VyDwK.js} +6 -6
  23. package/dist/{NewRolePage-CZxAp7LR.js → NewRolePage-BYYZJkK7.js} +3 -3
  24. package/dist/{NumberField.vue_vue_type_script_setup_true_lang-U-_kcnGr.js → NumberField.vue_vue_type_script_setup_true_lang-BhNZgEW6.js} +7 -6
  25. package/dist/{PersonAccess.vue_vue_type_script_setup_true_lang-BPDh9s2m.js → PersonAccess.vue_vue_type_script_setup_true_lang-mYFuG-Y3.js} +56 -56
  26. package/dist/{ProfilePage-B-T8kBcc.js → ProfilePage-DGEsIycR.js} +13 -13
  27. package/dist/{RecordHeader.vue_vue_type_script_setup_true_lang-Bt5mT19g.js → RecordHeader.vue_vue_type_script_setup_true_lang-B3BnYg1x.js} +1 -1
  28. package/dist/{ResourcePage.vue_vue_type_script_setup_true_lang-DOiFuJnH.js → ResourcePage.vue_vue_type_script_setup_true_lang-1iZtj3rh.js} +1 -1
  29. package/dist/{RoleEditor.vue_vue_type_script_setup_true_lang-CqSsNPCw.js → RoleEditor.vue_vue_type_script_setup_true_lang-BslSDDiM.js} +9 -9
  30. package/dist/{RolePage-zcUWpuDE.js → RolePage-C08ZUqw1.js} +9 -9
  31. package/dist/{RolesPage-C7_zWw-c.js → RolesPage-FOuY1VKg.js} +10 -10
  32. package/dist/{SectionNavigator.vue_vue_type_script_setup_true_lang-BlCx9dVL.js → SectionNavigator.vue_vue_type_script_setup_true_lang-DCTFxfPB.js} +2 -2
  33. package/dist/{SegmentedField.vue_vue_type_script_setup_true_lang-zv_AxpBn.js → SegmentedField.vue_vue_type_script_setup_true_lang-CLR6EvzO.js} +13 -12
  34. package/dist/{SelectField.vue_vue_type_script_setup_true_lang-CLkoPbGm.js → SelectField.vue_vue_type_script_setup_true_lang-D5xYofdb.js} +10 -9
  35. package/dist/{ServiceAccountDialog.vue_vue_type_script_setup_true_lang-C2D_D5Hi.js → ServiceAccountDialog.vue_vue_type_script_setup_true_lang-CDr4WNIb.js} +9 -9
  36. package/dist/{ServiceAccountRecord-BYxpHzTR.js → ServiceAccountRecord-kWwIQGZb.js} +12 -12
  37. package/dist/{ServiceAccountsList-BUuK-RUj.js → ServiceAccountsList-CKy9_YHP.js} +8 -8
  38. package/dist/{Sessions-Cm-Kilwl.js → Sessions-BDc8gdYH.js} +4 -4
  39. package/dist/{SettingsPage-Bavl0K-F.js → SettingsPage-t-8kmFVT.js} +13 -13
  40. package/dist/{SignIn-SfEhFiyM.js → SignIn-CqtkvtCZ.js} +7 -7
  41. package/dist/{SignInPage-BU0n2ehq.js → SignInPage-DcdNKZIb.js} +4 -4
  42. package/dist/{SignIns-Ahbn5cO6.js → SignIns-D4-nGu0B.js} +3 -3
  43. package/dist/{SwitchField.vue_vue_type_script_setup_true_lang-Ds4q0bbl.js → SwitchField.vue_vue_type_script_setup_true_lang-CLtcuofe.js} +15 -14
  44. package/dist/{TextField.vue_vue_type_script_setup_true_lang-CWIeeZRE.js → TextField.vue_vue_type_script_setup_true_lang-CVGh-p3J.js} +7 -6
  45. package/dist/{TextareaField.vue_vue_type_script_setup_true_lang-D2j4s96s.js → TextareaField.vue_vue_type_script_setup_true_lang-YxUlNHSk.js} +6 -5
  46. package/dist/{TimeField.vue_vue_type_script_setup_true_lang-C1mLvOkc.js → TimeField.vue_vue_type_script_setup_true_lang-DHNC_MLt.js} +30 -29
  47. package/dist/{Timestamp.vue_vue_type_script_setup_true_lang-jCXYFLtq.js → Timestamp.vue_vue_type_script_setup_true_lang-d_KuxWjH.js} +1 -1
  48. package/dist/{UserRecord-3pCKMHkq.js → UserRecord-CwZCr9Im.js} +10 -10
  49. package/dist/{UsersList-vw3aDpvo.js → UsersList-Cn19sFgv.js} +4 -4
  50. package/dist/access/index.js +4 -4
  51. package/dist/app/application.d.ts +2 -0
  52. package/dist/app/index.js +2 -2
  53. package/dist/app/validate.d.ts +3 -1
  54. package/dist/{application-CHPpOssP.js → application-CDSweJb9.js} +144 -142
  55. package/dist/collection/authors.d.ts +27 -0
  56. package/dist/collection/index.d.ts +2 -0
  57. package/dist/collection/index.js +81 -76
  58. package/dist/config/index.d.ts +42 -0
  59. package/dist/config/index.js +12 -3
  60. package/dist/content/Panel.vue.d.ts +1 -1
  61. package/dist/content/index.js +2 -2
  62. package/dist/{contributions-9CPT-Z5v.js → contributions-B0x5x5xD.js} +1 -1
  63. package/dist/form/CommandDialog.vue.d.ts +1 -1
  64. package/dist/form/DateField.vue.d.ts +1 -1
  65. package/dist/form/DateTimeField.vue.d.ts +1 -1
  66. package/dist/form/FileField.vue.d.ts +1 -1
  67. package/dist/form/FormRow.vue.d.ts +2 -2
  68. package/dist/form/I18nField.vue.d.ts +2 -2
  69. package/dist/form/TextField.vue.d.ts +1 -1
  70. package/dist/form/TextareaField.vue.d.ts +2 -2
  71. package/dist/form/date/value.d.ts +21 -0
  72. package/dist/form/index.d.ts +1 -0
  73. package/dist/form/index.js +745 -715
  74. package/dist/format/format.d.ts +8 -2
  75. package/dist/format/index.js +1 -1
  76. package/dist/format-C_HXlOYQ.js +136 -0
  77. package/dist/{guards-6fJxfUtk.js → guards-CNJ0ATVC.js} +1 -1
  78. package/dist/i18n/index.d.ts +14 -2
  79. package/dist/i18n/index.js +84 -73
  80. package/dist/i18n/messages.d.ts +10 -1
  81. package/dist/i18n/runtime.d.ts +3 -1
  82. package/dist/identity/index.js +3 -3
  83. package/dist/identity/profile/EmailChangeDialog.vue.d.ts +2 -2
  84. package/dist/identity/users/list.d.ts +1 -1
  85. package/dist/incidents/index.js +1 -1
  86. package/dist/{index-D9_xqF5g.js → index-1HS7O2-e.js} +9 -9
  87. package/dist/{index-nByFc1eL.js → index-BA2w8uwD.js} +86 -86
  88. package/dist/{index-Ct7W_gYE.js → index-SLJWm40T.js} +4 -4
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/modal/DiscardChangesModal.vue.d.ts +2 -2
  92. package/dist/modal/Modal.vue.d.ts +2 -2
  93. package/dist/modal/Sheet.vue.d.ts +2 -2
  94. package/dist/modules/identity/routes.d.ts +1 -1
  95. package/dist/modules/identity/schemas.d.ts +2 -2
  96. package/dist/{navigation-DMJ6akat.js → navigation-d-JAPBGA.js} +1 -1
  97. package/dist/{neighbors-BpypL3Wb.js → neighbors-Dg_LU9ry.js} +1 -1
  98. package/dist/notifications/index.js +1 -1
  99. package/dist/onboarding/index.js +1 -1
  100. package/dist/options-mOOX6JSS.js +214 -0
  101. package/dist/overlay/AlertDialog.vue.d.ts +3 -3
  102. package/dist/overlay/Menu.vue.d.ts +2 -2
  103. package/dist/overlay/PhotoViewer.vue.d.ts +2 -2
  104. package/dist/overlay/Popover.vue.d.ts +2 -2
  105. package/dist/page/SectionNavigator.vue.d.ts +1 -1
  106. package/dist/page/SectionPanel.vue.d.ts +1 -1
  107. package/dist/page/index.js +3 -3
  108. package/dist/phone/index.js +57 -56
  109. package/dist/platform/index.d.ts +10 -1
  110. package/dist/platform/index.js +16 -15
  111. package/dist/platform/platform.d.ts +4 -2
  112. package/dist/{platform-ORzRR3Tw.js → platform-DU9QFts8.js} +7 -6
  113. package/dist/playwright/config.d.ts +33 -0
  114. package/dist/playwright/index.d.ts +2 -0
  115. package/dist/playwright/index.js +192 -171
  116. package/dist/resource/index.js +1 -1
  117. package/dist/router/index.js +3 -3
  118. package/dist/{rowLayout-QKfNHsML.js → rowLayout-PIuD7MGo.js} +2 -2
  119. package/dist/{sections-CSpJN0Z7.js → sections-BpiRLntE.js} +1 -1
  120. package/dist/{serverMessages-Ce8-MK4e.js → serverMessages-C_KkDvpT.js} +1 -1
  121. package/dist/shell/index.js +4 -4
  122. package/dist/state/NextStepPanel.vue.d.ts +63 -0
  123. package/dist/state/PhaseTiles.vue.d.ts +20 -0
  124. package/dist/state/PhaseTrack.vue.d.ts +48 -0
  125. package/dist/state/RequirementList.vue.d.ts +10 -0
  126. package/dist/state/TodoRow.vue.d.ts +52 -0
  127. package/dist/state/index.d.ts +9 -0
  128. package/dist/state/index.js +731 -126
  129. package/dist/state/phaseStep.d.ts +7 -0
  130. package/dist/state/phaseTile.d.ts +10 -0
  131. package/dist/state/requirementItem.d.ts +16 -0
  132. package/dist/state/todoRow.d.ts +8 -0
  133. package/dist/{state-QDaxntgR.js → state-z1iPF1DT.js} +1 -1
  134. package/dist/styles.css +1 -1
  135. package/dist/{suggestions-q_v3M7G6.js → suggestions-DeMgs_ki.js} +1 -1
  136. package/dist/testing/format.d.ts +1 -1
  137. package/dist/testing/index.js +31 -31
  138. package/dist/{useCommand-DAMgQ5NE.js → useCommand-DQKvro1n.js} +2 -2
  139. package/dist/{useGroupSheet-B-Hs4gNO.js → useGroupSheet-CRw3MUmX.js} +3 -3
  140. package/dist/{usePreferences-BfMXVbSO.js → usePreferences-DB29UTFD.js} +1 -1
  141. package/dist/{useRecordDialog-QPKFH1Ww.js → useRecordDialog-0azHxxvm.js} +1 -1
  142. package/dist/{useResourceForm-Oa6bk3NI.js → useResourceForm-zVuWerSn.js} +1 -1
  143. package/dist/{useTypedEntry-CITij2C_.js → useTypedEntry-BsMV66Yc.js} +46 -45
  144. package/package.json +1 -1
  145. package/dist/format-Cp1h0vsC.js +0 -125
  146. package/dist/options-B48UiYsG.js +0 -368
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 — 2026-11-04
4
+
5
+ What every app built on vue-core wrote again: the phase and to-do components, date values in the date fields' own shape, a lazy locale loader, the author of a list row, "today at 14:35", typed access, two architecture rules as data, and the Playwright and vitest setup. Breaking only in that `Platform.access` is now typed by the registered permissions (see Changed).
6
+
7
+ ### New
8
+ - **state:** `PhaseTrack`, `PhaseTiles`, `TodoRow`, `NextStepPanel`, `RequirementList` and their types (`PhaseStep`, `PhaseTile`, `PhaseTone`, `RequirementItem`, `TodoRowAction`, `TodoRowState`): where a record stands, what is next, and what is still missing. New texts `core.stepper`, `core.next_step`, `core.requirements`, `core.todo_state`, `core.phase_tiles` in the five locales. Recipe: `docs/recipes/workflow.md`.
9
+ - **i18n:** `localeMessages(namespace, import.meta.glob("./*.json"))`, still lazy: nothing loads until the namespace is needed. A file's locale is its name; `createApplication({ localeFiles: { bs: "ba" } })` renames the exceptions.
10
+ - **form:** `dateValue(api)` (`YYYY-MM-DD`, sliced as written), `dateTimeValue(api)` (`YYYY-MM-DDTHH:mm` in local time, the shape `DateTimeField` writes), `parseDateValue(model)` (the `T` and the space form, as local time), `now()`; `today()` is exported. Go's zero date is `null`.
11
+ - **collection:** `authorOf(meta, id)`, `ResourceAuthor`, `ResourceMeta`, for go-core's `meta.authors` / `meta.author` / `meta.editor`.
12
+ - **format:** `format.when(value)`: just now, minutes ago, today / yesterday / tomorrow at a time, else the date and time. New texts `core.when.*`.
13
+ - **platform:** `useAccess()`. An application registers its generated permissions once (`interface PermissionRegistry extends Record<Permission, true> {}`); `useAccess()`, `Platform.access`, route meta `access` and `AccessGate` then refuse an unknown permission at compile time, template literals over a namespace type included.
14
+ - **devkit:** `check-architecture` rule `permission-texts` (`architecture.permissions: { generated, keys, locales, dir }`): every generated permission has each key (`policies.{id}`, `policy_descriptions.{id}`) in each locale, and no locale keeps a text for one that is gone.
15
+ - **devkit:** `architecture.forbidden: [{ id, pattern, message, exclude? }]`: text rules as data. Comments are stripped (a `//` in a string or URL is kept), tests, e2e, docs and scripts are skipped by default, and each rule has its allowlist `allow[id]`.
16
+ - **playwright:** `playwrightConfig({ testDir, timezone, ignore, port, report }, overrides)`: `E2E_PORT`, the dev server, CI retries and workers, the reporters plus the JSON report `vue-core e2e-timings` reads, and the time zone for the specs and the browser.
17
+ - **config:** `vitestDefaults({ timezone, setupFiles })` for the vitest `test` block: globals, happy-dom, `e2e/` and `review/` excluded, `TZ` pinned.
18
+
19
+ ### Changed
20
+ - **platform:** `Platform.access` is `AccessClient<Permission>`, the registered permissions. Code that assigned it to a narrower `AccessClient<X>` registers `X` instead.
21
+ - **format:** `Formatting.translate` receives the message parameters.
22
+
3
23
  ## 0.16.0 — 2026-10-22
4
24
 
5
25
  Back to the page a record was opened from.
package/README.md CHANGED
@@ -116,8 +116,8 @@ export function createTicketsFeature(http: HttpClient) {
116
116
  id: "tickets",
117
117
  routes: ticketRoutes.records,
118
118
  context: provideContext(TICKETS, { api, list: createTicketList(api) }),
119
- // Loaded the first time a route of this feature is entered, in the active locale.
120
- messages: localeMessages("tickets", { en: () => import("./i18n/en.json"), hr: () => import("./i18n/hr.json") }),
119
+ // Loaded the first time a route of this feature is entered, in the active locale. The lazy glob takes the file names as locales (en.json, hr.json); `createApplication({ localeFiles: { bs: "ba" } })` renames the exceptions.
120
+ messages: localeMessages("tickets", import.meta.glob("./i18n/*.json")),
121
121
  // The backend's menu names destinations; this says which route opens "tickets" and which records keep it highlighted.
122
122
  navigation: [{ destination: "tickets", to: ticketRoutes.index, within: ["tickets.record"] }],
123
123
  });
@@ -327,7 +327,7 @@ Dark mode is the `dark` class on `<html>`; nothing is decided for you. An app th
327
327
 
328
328
  ## Texts and locales
329
329
 
330
- The library's own texts are under the `core` namespace in English, Croatian, Bosnian and Slovenian (`t("core.actions.cancel")`). `createApplication` merges them into its vue-i18n instance; override any key with `i18n.messages` and restrict the offered locales with `locale.supported`. A feature's texts are a namespace (`localeMessages("tickets", { en: () => import("./i18n/en.json") })`, used as `t("tickets.title")`) loaded per locale on demand. A locale switch (the account menu has one) loads every namespace in use first, so nothing shows a raw key; `locale.onChange` lets you save the choice on the user.
330
+ The library's own texts are under the `core` namespace in English, Croatian, Bosnian and Slovenian (`t("core.actions.cancel")`). `createApplication` merges them into its vue-i18n instance; override any key with `i18n.messages` and restrict the offered locales with `locale.supported`. A feature's texts are a namespace (`localeMessages("tickets", import.meta.glob("./i18n/*.json"))`, or one `() => import("./i18n/en.json")` per locale; used as `t("tickets.title")`) loaded per locale on demand. A locale switch (the account menu has one) loads every namespace in use first, so nothing shows a raw key; `locale.onChange` lets you save the choice on the user.
331
331
 
332
332
  ## Recipes
333
333
 
@@ -345,6 +345,7 @@ The library's own texts are under the `core` namespace in English, Croatian, Bos
345
345
  - [Notifications](docs/recipes/notifications.md): the bell, the inbox and the live stream, categories, the failed-events page, trying it against go-core's dev server
346
346
  - [Incidents](docs/recipes/incidents.md): go-core's incident module: the list and an incident, "Report a bug", the browser's errors reported (what is and is not sent), source maps
347
347
  - [Preferences and tips](docs/recipes/preferences.md): `usePreferences`, the person's stored settings, `darkMode()` on them, the `Tip` beside something new
348
+ - [Where a record stands](docs/recipes/workflow.md): `PhaseTrack`, `PhaseTiles`, `TodoRow`, `NextStepPanel`, `RequirementList` in `/state`
348
349
  - [Theming](docs/recipes/theming.md): brand accent, dark mode, tokens, status tones and category hues
349
350
  - [Testing](docs/recipes/testing.md): the application's environment and a fake backend for your tests
350
351
  - [End-to-end specs](docs/recipes/e2e.md): `@wssto2/vue-core/playwright`, the Playwright mocks of go-core's envelope, session, access, notification, profile and users routes
@@ -4,6 +4,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  import { PACKS } from "./packs.mjs";
7
+ import { checkPermissionTexts } from "./permissions.mjs";
7
8
  import { builtInRules } from "./rules.mjs";
8
9
 
9
10
  const SOURCE_EXTENSIONS = new Set([".ts", ".vue"]);
@@ -142,6 +143,8 @@ export function check({ root, config, configName }) {
142
143
  }
143
144
  }
144
145
 
146
+ if (config.permissions) violations.push(...checkPermissionTexts({ root, config: config.permissions }));
147
+
145
148
  // every directory name under the features folder is a single lowercase word
146
149
  for (const dirPath of collectFeatureDirs(featuresRoot)) {
147
150
  const rel = toPosix(path.relative(root, dirPath));
@@ -0,0 +1,56 @@
1
+ // permission-texts: the permissions the generated catalogue lists and the texts the locale files keep for them agree. Every id has each key
2
+ // in each locale, and no locale keeps a key for an id that is gone. A key template is `<namespace>.<prefix>{id}`: the namespace is the folder
3
+ // `<dir>/<namespace>/<locale>.json`, and the file is nested by dots (`"crm": { "lead:view": "…" }` holds `crm.lead:view`).
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /** The ids of a generated permissions file: its quoted strings (`export type Permission = 'a:view' | 'b:view'`), comments cut out. */
8
+ export function generatedIds(source) {
9
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
10
+ // Only the `Permission` union names ids; go-core's generator writes a metadata map after it, full of other strings.
11
+ const union = /export\s+type\s+Permission\s*=([^;]*);/.exec(code);
12
+ if (!union) throw new Error("permission-texts: the generated file has no `export type Permission = …;` union");
13
+ return [...new Set([...union[1].matchAll(/(["'`])([^"'`\n]+)\1/g)].map((match) => match[2]))];
14
+ }
15
+
16
+ // A nested messages file as the identifiers it holds: a leaf string is named by the dotted path to it.
17
+ function flatten(node, prefix, out) {
18
+ for (const [key, value] of Object.entries(node)) {
19
+ if (typeof value === "string") out.add(prefix + key);
20
+ else if (value && typeof value === "object") flatten(value, `${prefix}${key}.`, out);
21
+ }
22
+ }
23
+
24
+ /** The violations of `permission-texts`, as messages that start with the file they concern. */
25
+ export function checkPermissionTexts({ root, config: { generated, keys, locales, dir = "i18n" } }) {
26
+ const generatedFile = path.join(root, generated);
27
+ if (!existsSync(generatedFile)) return [`${generated}: permission-texts — the generated permissions file does not exist.`];
28
+ const ids = generatedIds(readFileSync(generatedFile, "utf8"));
29
+ const known = new Set(ids);
30
+ const violations = [];
31
+ for (const locale of locales) {
32
+ for (const key of keys) {
33
+ const [namespace, ...rest] = key.split(".");
34
+ const prefix = rest.join(".").replace("{id}", "");
35
+ const file = `${dir}/${namespace}/${locale}.json`;
36
+ if (!existsSync(path.join(root, file))) {
37
+ violations.push(`${file}: permission-texts — the file does not exist.`);
38
+ continue;
39
+ }
40
+ let messages;
41
+ try {
42
+ messages = JSON.parse(readFileSync(path.join(root, file), "utf8"));
43
+ } catch (error) {
44
+ violations.push(`${file}: permission-texts — not valid JSON (${error.message}).`);
45
+ continue;
46
+ }
47
+ const have = new Set();
48
+ flatten(messages, "", have);
49
+ for (const id of ids) if (!have.has(prefix + id)) violations.push(`${file}: permission-texts — ${id} has no text (${key.replace("{id}", id)}).`);
50
+ for (const name of [...have].sort()) {
51
+ if (name.startsWith(prefix) && !known.has(name.slice(prefix.length))) violations.push(`${file}: permission-texts — ${name.slice(prefix.length)} is not in ${generated}; remove its text.`);
52
+ }
53
+ }
54
+ }
55
+ return violations;
56
+ }
@@ -81,6 +81,22 @@ function noRoutePolicy({ sessionField, exclude }) {
81
81
  };
82
82
  }
83
83
 
84
+ /**
85
+ * forbidden: the application's own text rules as data. The first line of the file (comments cut out, tests never scanned, and by default
86
+ * not the e2e / review / scripts / tests / docs / templates / static folders) that `pattern` matches. `exclude` replaces that folder default.
87
+ */
88
+ function forbiddenRule({ pattern, message, exclude = PLAIN_SOURCE }) {
89
+ return ({ file, content, isTest }) => {
90
+ if (isTest || matches(exclude, file)) return null;
91
+ const hit = withoutComments(content)
92
+ .split("\n")
93
+ .find((line) => matches(pattern, line));
94
+ return hit ? `“${hit.trim().slice(0, 80)}” — ${message}` : null;
95
+ };
96
+ }
97
+
98
+ const PLAIN_SOURCE = /^(e2e|review|scripts|tests|docs|templates|static)\//;
99
+
84
100
  export const RULE_IDS = ["no-hardcoded-copy", "no-route-policy"];
85
101
 
86
102
  /** The rules the config switches on, each `{ id, check, seen }` (`seen`: the allowlisted files that tripped it). */
@@ -91,5 +107,6 @@ export function builtInRules(config) {
91
107
  rules.push({ id: "no-hardcoded-copy", check: noHardcodedCopy({ ...config.copy, scope }) });
92
108
  }
93
109
  if (config.routeAccess) rules.push({ id: "no-route-policy", check: noRoutePolicy(config.routeAccess) });
110
+ for (const rule of config.forbidden) rules.push({ id: rule.id, check: forbiddenRule(rule) });
94
111
  return rules.map((rule) => ({ ...rule, seen: new Set() }));
95
112
  }
@@ -20,10 +20,29 @@ export default function run({ config, root, configPath, err }) {
20
20
  const packRules = [
21
21
  ...settings.packs.filter((name) => Object.hasOwn(PACKS, name)).flatMap((name) => PACKS[name].rules.map((rule) => rule.id)),
22
22
  ...RULE_IDS.filter((id) => (id === "no-hardcoded-copy" ? settings.copy : settings.routeAccess)),
23
+ ...settings.forbidden.map((rule) => rule.id),
23
24
  ];
24
25
  if (settings.copy && !(settings.copy.pattern instanceof RegExp)) problems.push("copy.pattern: a regular expression for the language's letters and words, e.g. /[čćžšđ]|\\b(Spremi|Odustani)\\b/");
25
26
  for (const key of ["scope", "properNames"]) if (settings.copy?.[key] !== undefined && !(settings.copy[key] instanceof RegExp)) problems.push(`copy.${key}: must be a regular expression`);
26
27
  if (settings.routeAccess?.exclude !== undefined && !(settings.routeAccess.exclude instanceof RegExp)) problems.push("routeAccess.exclude: must be a regular expression");
28
+ const ids = new Set([...RULE_IDS, ...ALLOW_RULES, "permission-texts"]);
29
+ for (const [index, rule] of settings.forbidden.entries()) {
30
+ const at = `forbidden[${index}]`;
31
+ if (typeof rule?.id !== "string" || rule.id === "") problems.push(`${at}.id: a rule name, used in the message and the allowlist`);
32
+ else if (ids.has(rule.id)) problems.push(`${at}.id: "${rule.id}" is a built-in rule's name`);
33
+ else ids.add(rule.id);
34
+ if (!(rule?.pattern instanceof RegExp)) problems.push(`${at}.pattern: must be a regular expression`);
35
+ if (typeof rule?.message !== "string" || rule.message === "") problems.push(`${at}.message: what to do instead`);
36
+ if (rule?.exclude !== undefined && !(rule.exclude instanceof RegExp)) problems.push(`${at}.exclude: must be a regular expression`);
37
+ }
38
+ const texts = settings.permissions;
39
+ if (texts) {
40
+ if (typeof texts.generated !== "string") problems.push("permissions.generated: the generated permissions file, relative to the config's folder");
41
+ if (!Array.isArray(texts.keys) || texts.keys.length === 0 || !texts.keys.every((key) => typeof key === "string" && key.includes("{id}") && key.includes("."))) {
42
+ problems.push('permissions.keys: message keys with {id}, "<namespace>.{id}", e.g. ["policies.{id}", "policy_descriptions.{id}"]');
43
+ }
44
+ if (!Array.isArray(texts.locales) || texts.locales.length === 0) problems.push('permissions.locales: the locale file names, e.g. ["hr", "en"]');
45
+ }
27
46
  for (const rule of Object.keys(settings.allow)) {
28
47
  if (!ALLOW_RULES.includes(rule) && !packRules.includes(rule)) problems.push(`allow.${rule}: no such rule; the allowlists are ${[...ALLOW_RULES, ...packRules].join(", ")}`);
29
48
  }
package/bin/config.mjs CHANGED
@@ -32,6 +32,8 @@ export const DEFAULTS = {
32
32
  doc: {},
33
33
  copy: null,
34
34
  routeAccess: null,
35
+ forbidden: [],
36
+ permissions: null,
35
37
  },
36
38
  generator: { template: join(dirname(fileURLToPath(import.meta.url)), "templates", "feature-crud"), out: "features" },
37
39
  e2eTimings: { report: "e2e-timings/results.json" },
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as t, computed as i, renderSlot as n } from "vue";
2
- import { u as l } from "./platform-ORzRR3Tw.js";
2
+ import { u as l } from "./platform-DU9QFts8.js";
3
3
  const m = /* @__PURE__ */ t({
4
4
  __name: "AccessGate",
5
5
  props: {
@@ -1,7 +1,7 @@
1
1
  import { defineComponent as r, openBlock as o, createElementBlock as l, createElementVNode as s, toDisplayString as n, createCommentVNode as d, renderSlot as f, createBlock as u, unref as c, withCtx as p, normalizeClass as _, createVNode as x } from "vue";
2
- import { g as b } from "./options-B48UiYsG.js";
2
+ import { _ as b } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
3
3
  import { _ as h } from "./Icon.vue_vue_type_script_lang-BhhsFYwp.js";
4
- const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, g = { class: "flex min-w-0 grow basis-56 flex-col gap-1" }, w = { class: "text-title font-semibold tracking-tight text-content-strong" }, y = {
4
+ const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, w = { class: "flex min-w-0 grow basis-56 flex-col gap-1" }, g = { class: "text-title font-semibold tracking-tight text-content-strong" }, y = {
5
5
  key: 0,
6
6
  class: "max-w-readable text-subheadline text-content-muted"
7
7
  }, E = /* @__PURE__ */ r({
@@ -12,8 +12,8 @@ const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, g = { class:
12
12
  },
13
13
  setup(e) {
14
14
  return (i, t) => (o(), l("div", k, [
15
- s("div", g, [
16
- s("h2", w, n(e.title), 1),
15
+ s("div", w, [
16
+ s("h2", g, n(e.title), 1),
17
17
  e.description ? (o(), l("p", y, n(e.description), 1)) : d("", !0)
18
18
  ]),
19
19
  f(i.$slots, "actions")
@@ -3,18 +3,18 @@ import { useI18n as D } from "vue-i18n";
3
3
  import { RouterLink as I } from "vue-router";
4
4
  import { _ as T } from "./IconTile.vue_vue_type_script_setup_true_lang-BmxCuUZ9.js";
5
5
  import { _ as z } from "./Tabs.vue_vue_type_script_setup_true_lang-tpkGp98F.js";
6
- import { u as G } from "./format-Cp1h0vsC.js";
7
- import { _ as B } from "./DateField.vue_vue_type_script_setup_true_lang-DzCvYHAf.js";
8
- import { a as H } from "./FormGroup.vue_vue_type_script_setup_true_lang-dsO1m9Mm.js";
9
- import { g as J } from "./options-B48UiYsG.js";
6
+ import { u as G } from "./format-C_HXlOYQ.js";
7
+ import { _ as B } from "./DateField.vue_vue_type_script_setup_true_lang-D0KcfW8R.js";
8
+ import { a as H } from "./FormGroup.vue_vue_type_script_setup_true_lang-lvAuBG-0.js";
9
+ import { _ as J } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
10
10
  import { i as M } from "./preferences-hYhrw8Uy.js";
11
- import { u as Q } from "./platform-ORzRR3Tw.js";
11
+ import { u as Q } from "./platform-DU9QFts8.js";
12
12
  import { b as W } from "./routeResource-DlTsGTg_.js";
13
13
  import { _ as X } from "./AsyncSection.vue_vue_type_script_setup_true_lang-CX8mpXRE.js";
14
14
  import { _ as Y } from "./Badge.vue_vue_type_script_setup_true_lang-DJyZc0C7.js";
15
15
  import { u as Z } from "./useLoad-M7V-AVwR.js";
16
- import { P as tt, f as et, j as at } from "./index-nByFc1eL.js";
17
- import { _ as st } from "./ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js";
16
+ import { P as tt, f as et, j as at } from "./index-BA2w8uwD.js";
17
+ import { _ as st } from "./ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js";
18
18
  const ot = {
19
19
  class: "flex min-w-0 flex-col gap-group-gap",
20
20
  "data-person-activity": ""
@@ -6,7 +6,7 @@ import { _ as Q } from "./Button.vue_vue_type_script_setup_true_lang-JrqhO99l.js
6
6
  import { _ as $ } from "./Icon.vue_vue_type_script_lang-BhhsFYwp.js";
7
7
  import { _ as U } from "./Menu.vue_vue_type_script_setup_true_lang-Cqu4Ebsb.js";
8
8
  import { RouterLink as E, useRoute as W } from "vue-router";
9
- import { u as Y } from "./format-Cp1h0vsC.js";
9
+ import { u as Y } from "./format-C_HXlOYQ.js";
10
10
  import { u as F, c as G, p as J } from "./chrome-kMBKIB1N.js";
11
11
  import { b as X } from "./back-LcLlFodC.js";
12
12
  import { d as Z } from "./context-Ch1r4fdN.js";
@@ -1,12 +1,12 @@
1
1
  import { defineComponent as h, ref as m, watch as p, openBlock as l, createElementBlock as d, createVNode as c, unref as t, withCtx as y, toDisplayString as x, createBlock as f, createCommentVNode as k } from "vue";
2
2
  import { useI18n as C } from "vue-i18n";
3
3
  import { i as $ } from "./preferences-hYhrw8Uy.js";
4
- import { u as b } from "./platform-ORzRR3Tw.js";
4
+ import { u as b } from "./platform-DU9QFts8.js";
5
5
  import { b as V } from "./routeResource-DlTsGTg_.js";
6
6
  import { _ as R } from "./AsyncSection.vue_vue_type_script_setup_true_lang-CX8mpXRE.js";
7
7
  import { u as E } from "./useLoad-M7V-AVwR.js";
8
- import { P as N, f as B, k as P, j as S } from "./index-nByFc1eL.js";
9
- import { _ as U } from "./ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js";
8
+ import { P as N, f as B, k as P, j as S } from "./index-BA2w8uwD.js";
9
+ import { _ as U } from "./ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js";
10
10
  import { _ as I } from "./ViewTabs.vue_vue_type_script_setup_true_lang-BjdDXE3T.js";
11
11
  const W = {
12
12
  class: "flex min-w-0 flex-col gap-group-gap",
@@ -2,22 +2,22 @@ import { hasInjectionContext as lt, inject as ot, shallowRef as xe, computed as
2
2
  import { routerKey as ct } from "vue-router";
3
3
  import { i as qe, a as ut } from "./error-CD7x0Jrz.js";
4
4
  import { d as dt } from "./http-D6qOS6xs.js";
5
- import { d as Re, o as ft, L as Te, e as je, w as pt, r as vt } from "./state-QDaxntgR.js";
6
- import { _ as mt } from "./AdaptivePageShell.vue_vue_type_script_setup_true_lang-d_dPurWG.js";
5
+ import { d as Re, o as ft, L as Te, e as je, w as pt, r as vt } from "./state-z1iPF1DT.js";
6
+ import { _ as mt } from "./AdaptivePageShell.vue_vue_type_script_setup_true_lang-oXWQTfBs.js";
7
7
  import { useI18n as he } from "vue-i18n";
8
8
  import { _ as ce } from "./Button.vue_vue_type_script_setup_true_lang-JrqhO99l.js";
9
9
  import { _ as Ge } from "./Tabs.vue_vue_type_script_setup_true_lang-tpkGp98F.js";
10
- import { u as Pe } from "./format-Cp1h0vsC.js";
10
+ import { u as Pe } from "./format-C_HXlOYQ.js";
11
11
  import { _ as I } from "./Icon.vue_vue_type_script_lang-BhhsFYwp.js";
12
12
  import { _ as Ne } from "./Banner.vue_vue_type_script_setup_true_lang-sv0phngk.js";
13
13
  import { _ as Ae } from "./EmptyState.vue_vue_type_script_setup_true_lang-DbZiO2RF.js";
14
- import { u as bt, _ as gt } from "./rowLayout-QKfNHsML.js";
14
+ import { u as bt, _ as gt } from "./rowLayout-PIuD7MGo.js";
15
15
  import { u as Ee } from "./shortcut-oixX55SZ.js";
16
16
  import { _ as Ye } from "./Menu.vue_vue_type_script_setup_true_lang-Cqu4Ebsb.js";
17
17
  import { _ as Ze } from "./Popover.vue_vue_type_script_setup_true_lang-BK8HL_V0.js";
18
18
  import { _ as we } from "./ToneDot.vue_vue_type_script_setup_true_lang-99rNn-L5.js";
19
- import { _ as ht } from "./NumberField.vue_vue_type_script_setup_true_lang-U-_kcnGr.js";
20
- import { _ as yt } from "./SelectField.vue_vue_type_script_setup_true_lang-CLkoPbGm.js";
19
+ import { _ as ht } from "./NumberField.vue_vue_type_script_setup_true_lang-BhNZgEW6.js";
20
+ import { _ as yt } from "./SelectField.vue_vue_type_script_setup_true_lang-D5xYofdb.js";
21
21
  import { a as _t } from "./mediaQuery-CygcXmFW.js";
22
22
  import { _ as xt } from "./Sheet.vue_vue_type_script_setup_true_lang-BG8HUNfs.js";
23
23
  import { t as ke } from "./toast-CqSDTWRX.js";
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as d, computed as u, useSlots as p, openBlock as w, createElementBlock as y, createVNode as f, unref as k, createSlots as b, renderList as m, withCtx as h, renderSlot as v, normalizeProps as _, guardReactiveProps as g } from "vue";
2
- import { u as R, _ as L } from "./rowLayout-QKfNHsML.js";
2
+ import { u as R, _ as L } from "./rowLayout-PIuD7MGo.js";
3
3
  const F = ["aria-busy"], V = /* @__PURE__ */ d({
4
4
  __name: "DataTable",
5
5
  props: {
@@ -1,12 +1,13 @@
1
1
  import { computed as p, defineComponent as re, useModel as oe, useTemplateRef as T, openBlock as g, createBlock as Y, mergeProps as E, unref as s, createSlots as ae, withCtx as y, createElementBlock as C, Fragment as H, createVNode as D, createTextVNode as ne, toDisplayString as K, createCommentVNode as M, createElementVNode as O, normalizeClass as le, renderSlot as S, mergeModels as G, mergeDefaults as se, nextTick as J } from "vue";
2
2
  import { useI18n as L } from "vue-i18n";
3
3
  import { _ as ie } from "./DateButton.vue_vue_type_script_setup_true_lang-CovKG5lN.js";
4
- import { u as W } from "./format-Cp1h0vsC.js";
4
+ import { u as W } from "./format-C_HXlOYQ.js";
5
5
  import { _ as de } from "./Icon.vue_vue_type_script_lang-BhhsFYwp.js";
6
6
  import { u as ue } from "./mediaQuery-CygcXmFW.js";
7
7
  import { _ as ce } from "./Popover.vue_vue_type_script_setup_true_lang-BK8HL_V0.js";
8
- import { e as me, _ as fe, f as pe, b as be } from "./options-B48UiYsG.js";
9
- import { e as q, f as j, m as X, g as Z, h as ve, j as ye, u as ke, k as ge, _ as he, l as xe, c as we } from "./useTypedEntry-CITij2C_.js";
8
+ import { u as me, _ as fe } from "./options-mOOX6JSS.js";
9
+ import { e as q, f as j, m as X, g as Z, h as pe, j as be, u as ve, k as ye, _ as ke, l as ge, c as he } from "./useTypedEntry-BsMV66Yc.js";
10
+ import { f as xe, b as we } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
10
11
  const _e = ["d", "m", "y"];
11
12
  function $e(o) {
12
13
  const r = o("2033-11-22"), t = { y: r.indexOf("2033") >= 0 ? r.indexOf("2033") : r.indexOf("33"), m: r.indexOf("11"), d: r.indexOf("22") };
@@ -53,7 +54,7 @@ function Te(o, r) {
53
54
  function F(o, r) {
54
55
  return Object.fromEntries(o.map((t, e) => [t, r[e]]));
55
56
  }
56
- function Qe(o) {
57
+ function Ye(o) {
57
58
  const r = o.trim().replace(/^(\d{4}-\d{2}-\d{2})T/, "$1 ").split(/\s+/).filter(Boolean), t = r[r.length - 1];
58
59
  if (t === void 0) return { date: null, time: null };
59
60
  if (!(t.includes(":") || r.length > 1 && /^\d{1,2}\.\d{2}$/.test(t))) return { date: r.join(" "), time: null };
@@ -83,8 +84,8 @@ function qe(o, r, t) {
83
84
  const { t: e } = L();
84
85
  return p(() => {
85
86
  const a = q();
86
- return (o() ?? ve(r())).map((u) => {
87
- const i = ye(u, (n) => e(`core.form.date.${n.replace(/-/g, "_")}`), a);
87
+ return (o() ?? pe(r())).map((u) => {
88
+ const i = be(u, (n) => e(`core.form.date.${n.replace(/-/g, "_")}`), a);
88
89
  return { ...i, disabled: Z(i.value, t()) };
89
90
  });
90
91
  });
@@ -92,7 +93,7 @@ function qe(o, r, t) {
92
93
  const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-required", "aria-invalid", "aria-describedby"], Ve = ["aria-label", "onClick"], Be = { class: "flex flex-col gap-2.5" }, Ee = {
93
94
  key: 0,
94
95
  class: "border-t border-border-separator pt-2.5"
95
- }, Me = { class: "text-footnote text-content-muted" }, Ye = /* @__PURE__ */ re({
96
+ }, Me = { class: "text-footnote text-content-muted" }, He = /* @__PURE__ */ re({
96
97
  __name: "DateField",
97
98
  props: /* @__PURE__ */ G(/* @__PURE__ */ se({
98
99
  label: {},
@@ -110,20 +111,20 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
110
111
  disabledDates: { type: [Array, Function] },
111
112
  openOn: {},
112
113
  quickPicks: {}
113
- }, { ...be, min: void 0, max: void 0, disabledDates: void 0, openOn: void 0, quickPicks: void 0 }), {
114
+ }, { ...we, min: void 0, max: void 0, disabledDates: void 0, openOn: void 0, quickPicks: void 0 }), {
114
115
  modelValue: { default: null },
115
116
  modelModifiers: {}
116
117
  }),
117
118
  emits: /* @__PURE__ */ G(["focus", "blur"], ["update:modelValue"]),
118
119
  setup(o, { expose: r, emit: t }) {
119
- const e = o, a = oe(o, "modelValue"), u = t, { t: i } = L(), n = W(), x = ue(), c = T("input"), m = T("popover"), w = T("calendar"), N = T("sheet"), k = p(() => ({ min: e.min, max: e.max, disabled: e.disabledDates })), b = Oe(() => k.value), f = ke({
120
+ const e = o, a = oe(o, "modelValue"), u = t, { t: i } = L(), n = W(), x = ue(), c = T("input"), m = T("popover"), w = T("calendar"), N = T("sheet"), k = p(() => ({ min: e.min, max: e.max, disabled: e.disabledDates })), b = Oe(() => k.value), f = ve({
120
121
  model: a,
121
122
  display: (l) => n.date(l),
122
123
  parse: (l) => {
123
124
  const d = b.parse(l);
124
125
  return d ? b.available(d) ? { value: d } : { error: b.unavailableMessage() } : { error: b.invalidMessage() };
125
126
  }
126
- }), P = qe(() => e.quickPicks, () => x.value, () => k.value), z = p(() => e.error || f.message.value), V = p(() => a.value ? n.date(a.value) : null), ee = me("date", () => z.value ? "error" : e.disabled ? "locked" : "rest"), U = p(() => f.typed.value ?? (ge(a.value) ? a.value : null));
127
+ }), P = qe(() => e.quickPicks, () => x.value, () => k.value), z = p(() => e.error || f.message.value), V = p(() => a.value ? n.date(a.value) : null), ee = me("date", () => z.value ? "error" : e.disabled ? "locked" : "rest"), U = p(() => f.typed.value ?? (ye(a.value) ? a.value : null));
127
128
  async function B(l) {
128
129
  e.disabled || (m.value?.present(), l && (await J(), await J(), w.value?.focus()));
129
130
  }
@@ -133,7 +134,7 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
133
134
  function R(l, d) {
134
135
  f.set(l), d();
135
136
  }
136
- return r({ focus: () => c.value?.focus() }), (l, d) => (g(), Y(fe, E({ ...s(pe)(e), error: z.value }, {
137
+ return r({ focus: () => c.value?.focus() }), (l, d) => (g(), Y(fe, E({ ...s(xe)(e), error: z.value }, {
137
138
  value: V.value,
138
139
  "value-style": "numeric"
139
140
  }), ae({
@@ -152,7 +153,7 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
152
153
  ]),
153
154
  _: 1
154
155
  }, 16, ["disabled", "invalid", "placeholder"]),
155
- D(he, {
156
+ D(ke, {
156
157
  ref_key: "sheet",
157
158
  ref: N,
158
159
  title: e.label ?? s(i)("core.form.date.calendar"),
@@ -223,7 +224,7 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
223
224
  ]),
224
225
  default: y(({ dismiss: _ }) => [
225
226
  O("div", Be, [
226
- D(xe, {
227
+ D(ge, {
227
228
  ref_key: "calendar",
228
229
  ref: w,
229
230
  "model-value": U.value,
@@ -235,7 +236,7 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
235
236
  "onUpdate:modelValue": ($) => R($, _)
236
237
  }, null, 8, ["model-value", "min", "max", "disabled-dates", "open-on", "onUpdate:modelValue"]),
237
238
  s(P).length > 0 ? (g(), C("div", Ee, [
238
- D(we, {
239
+ D(he, {
239
240
  items: s(P),
240
241
  selected: U.value,
241
242
  onPick: ($) => R($, _)
@@ -274,8 +275,8 @@ const Pe = ["id", "name", "value", "placeholder", "disabled", "required", "aria-
274
275
  }
275
276
  });
276
277
  export {
277
- Ye as _,
278
+ He as _,
278
279
  qe as a,
279
- Qe as s,
280
+ Ye as s,
280
281
  Oe as u
281
282
  };
@@ -1,14 +1,14 @@
1
1
  import { defineComponent as B, useTemplateRef as C, computed as y, ref as v, openBlock as D, createBlock as z, unref as r, withCtx as m, createVNode as F, createElementVNode as b, normalizeClass as $, toDisplayString as h } from "vue";
2
2
  import { useI18n as q } from "vue-i18n";
3
3
  import { a as k } from "./error-CD7x0Jrz.js";
4
- import { u as V, _ as N } from "./CollectionPage.vue_vue_type_script_setup_true_lang-B5hSkJ2N.js";
4
+ import { u as V, _ as N } from "./CollectionPage.vue_vue_type_script_setup_true_lang-Dr6nFNBB.js";
5
5
  import { u as P } from "./describeError-B-NMa--w.js";
6
- import { u as I } from "./format-Cp1h0vsC.js";
7
- import { e as a, R as O } from "./index-Ct7W_gYE.js";
6
+ import { u as I } from "./format-C_HXlOYQ.js";
7
+ import { e as a, R as O } from "./index-SLJWm40T.js";
8
8
  import { _ as W } from "./AlertDialog.vue_vue_type_script_setup_true_lang-WuGvAOrj.js";
9
9
  import { t as i } from "./toast-CqSDTWRX.js";
10
10
  import { u as Y } from "./useLoad-M7V-AVwR.js";
11
- import { u as j } from "./platform-ORzRR3Tw.js";
11
+ import { u as j } from "./platform-DU9QFts8.js";
12
12
  import { d as G, r as H } from "./http-D6qOS6xs.js";
13
13
  function J(u) {
14
14
  return G({
@@ -4,11 +4,11 @@ var z = (o, e, t) => de(o, typeof e != "symbol" ? e + "" : e, t);
4
4
  import { shallowRef as N, computed as v, toRaw as fe, reactive as me, ref as V, watch as pe, nextTick as ve, toValue as ge, defineComponent as J, unref as $, openBlock as g, createBlock as Q, withCtx as ne, createElementBlock as k, toDisplayString as j, createCommentVNode as y, Fragment as W, createElementVNode as R, normalizeClass as P, renderList as he, createTextVNode as D, inject as le, provide as H, useSlots as ye, shallowReactive as re, useTemplateRef as be, useId as ke, normalizeStyle as _e, renderSlot as I } from "vue";
5
5
  import { useI18n as X } from "vue-i18n";
6
6
  import { i as we, a as L } from "./error-CD7x0Jrz.js";
7
- import { f as je } from "./serverMessages-Ce8-MK4e.js";
7
+ import { f as je } from "./serverMessages-C_KkDvpT.js";
8
8
  import { d as Ee, u as Oe, a as xe } from "./describeError-B-NMa--w.js";
9
9
  import { _ as $e } from "./Banner.vue_vue_type_script_setup_true_lang-sv0phngk.js";
10
10
  import { s as Ae, u as Fe } from "./sectionIndex-BS0ic5W7.js";
11
- import { d as se, i as Be } from "./options-B48UiYsG.js";
11
+ import { c as se, d as Be } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
12
12
  import { _ as De } from "./Button.vue_vue_type_script_setup_true_lang-JrqhO99l.js";
13
13
  class Se {
14
14
  constructor() {