@kinetis/vector-ui 0.0.0-stage → 0.9.1

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 (282) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +18 -0
  3. package/README.md +72 -2
  4. package/app/app.config.ts +63 -0
  5. package/app/app.vue +11 -0
  6. package/app/components/Vx/Action/Dialog.vue +107 -0
  7. package/app/components/Vx/Cell/Boolean.vue +10 -0
  8. package/app/components/Vx/Cell/Color.vue +12 -0
  9. package/app/components/Vx/Cell/Count.vue +12 -0
  10. package/app/components/Vx/Cell/Date.vue +10 -0
  11. package/app/components/Vx/Cell/DateTime.vue +10 -0
  12. package/app/components/Vx/Cell/Email.vue +9 -0
  13. package/app/components/Vx/Cell/Enum.vue +14 -0
  14. package/app/components/Vx/Cell/File.vue +30 -0
  15. package/app/components/Vx/Cell/Image.vue +38 -0
  16. package/app/components/Vx/Cell/Json.vue +18 -0
  17. package/app/components/Vx/Cell/KeyValue.vue +25 -0
  18. package/app/components/Vx/Cell/Markdown.vue +18 -0
  19. package/app/components/Vx/Cell/Money.vue +16 -0
  20. package/app/components/Vx/Cell/Number.vue +16 -0
  21. package/app/components/Vx/Cell/Relation.vue +16 -0
  22. package/app/components/Vx/Cell/RichText.vue +41 -0
  23. package/app/components/Vx/Cell/Text.vue +13 -0
  24. package/app/components/Vx/Cell/Url.vue +11 -0
  25. package/app/components/Vx/Cell.vue +25 -0
  26. package/app/components/Vx/CommandPalette.vue +118 -0
  27. package/app/components/Vx/Confirm.vue +19 -0
  28. package/app/components/Vx/Dashboard.vue +27 -0
  29. package/app/components/Vx/FieldsForm.vue +46 -0
  30. package/app/components/Vx/Import/Dialog.vue +131 -0
  31. package/app/components/Vx/Inbox.vue +49 -0
  32. package/app/components/Vx/Input/Boolean.vue +10 -0
  33. package/app/components/Vx/Input/Currency.vue +25 -0
  34. package/app/components/Vx/Input/DateTime.vue +38 -0
  35. package/app/components/Vx/Input/Enum.vue +21 -0
  36. package/app/components/Vx/Input/KeyValue.vue +78 -0
  37. package/app/components/Vx/Input/Markdown.vue +120 -0
  38. package/app/components/Vx/Input/Money.vue +47 -0
  39. package/app/components/Vx/Input/Number.vue +24 -0
  40. package/app/components/Vx/Input/Relation.vue +127 -0
  41. package/app/components/Vx/Input/Repeater.vue +134 -0
  42. package/app/components/Vx/Input/RichText.vue +182 -0
  43. package/app/components/Vx/Input/Text.vue +21 -0
  44. package/app/components/Vx/Input/Textarea.vue +20 -0
  45. package/app/components/Vx/Input/Upload.vue +89 -0
  46. package/app/components/Vx/Layout.vue +111 -0
  47. package/app/components/Vx/List/BulkBar.vue +44 -0
  48. package/app/components/Vx/List/BulkEdit.vue +91 -0
  49. package/app/components/Vx/List/ColumnManager.vue +74 -0
  50. package/app/components/Vx/List/FilterBar.vue +224 -0
  51. package/app/components/Vx/List/ViewMenu.vue +115 -0
  52. package/app/components/Vx/Notifications.vue +9 -0
  53. package/app/components/Vx/Resource/Audit.vue +82 -0
  54. package/app/components/Vx/Resource/Form.vue +351 -0
  55. package/app/components/Vx/Resource/RelationPanel.vue +217 -0
  56. package/app/components/Vx/Resource/Show.vue +35 -0
  57. package/app/components/Vx/Resource/Table.vue +513 -0
  58. package/app/components/Vx/Widget/Card.vue +80 -0
  59. package/app/components/Vx/Widget/Partition.vue +21 -0
  60. package/app/components/Vx/Widget/Progress.vue +22 -0
  61. package/app/components/Vx/Widget/Stat.vue +22 -0
  62. package/app/components/Vx/Widget/Table.vue +31 -0
  63. package/app/components/Vx/Widget/Trend.vue +37 -0
  64. package/app/composables/useConfirm.ts +34 -0
  65. package/app/composables/useLeaveGuard.ts +21 -0
  66. package/app/composables/useVectorApi.ts +74 -0
  67. package/app/composables/useVectorAuthStore.ts +252 -0
  68. package/app/composables/useVectorBranding.ts +45 -0
  69. package/app/composables/useVectorDashboards.ts +15 -0
  70. package/app/composables/useVectorExtensions.ts +44 -0
  71. package/app/composables/useVectorFields.ts +68 -0
  72. package/app/composables/useVectorForm.ts +36 -0
  73. package/app/composables/useVectorIdle.ts +39 -0
  74. package/app/composables/useVectorImpersonation.ts +78 -0
  75. package/app/composables/useVectorInputs.ts +61 -0
  76. package/app/composables/useVectorJobs.ts +62 -0
  77. package/app/composables/useVectorLocale.ts +70 -0
  78. package/app/composables/useVectorNotifications.ts +24 -0
  79. package/app/composables/useVectorNotificationsInbox.ts +26 -0
  80. package/app/composables/useVectorPreferences.ts +30 -0
  81. package/app/composables/useVectorResource.ts +305 -0
  82. package/app/composables/useVectorRuntime.ts +42 -0
  83. package/app/composables/useVectorSchema.ts +56 -0
  84. package/app/composables/useVectorT.ts +9 -0
  85. package/app/composables/useVectorViews.ts +25 -0
  86. package/app/i18n/ar/actions.ts +11 -0
  87. package/app/i18n/ar/common.ts +23 -0
  88. package/app/i18n/ar/dashboard.ts +28 -0
  89. package/app/i18n/ar/fields.ts +77 -0
  90. package/app/i18n/ar/index.ts +12 -0
  91. package/app/i18n/ar/jobs.ts +36 -0
  92. package/app/i18n/ar/list.ts +160 -0
  93. package/app/i18n/ar/record.ts +62 -0
  94. package/app/i18n/ar/settings.ts +10 -0
  95. package/app/i18n/ar/shell.ts +54 -0
  96. package/app/i18n/de/actions.ts +11 -0
  97. package/app/i18n/de/common.ts +23 -0
  98. package/app/i18n/de/dashboard.ts +28 -0
  99. package/app/i18n/de/fields.ts +77 -0
  100. package/app/i18n/de/index.ts +12 -0
  101. package/app/i18n/de/jobs.ts +36 -0
  102. package/app/i18n/de/list.ts +160 -0
  103. package/app/i18n/de/record.ts +62 -0
  104. package/app/i18n/de/settings.ts +10 -0
  105. package/app/i18n/de/shell.ts +54 -0
  106. package/app/i18n/en/actions.ts +11 -0
  107. package/app/i18n/en/common.ts +23 -0
  108. package/app/i18n/en/dashboard.ts +28 -0
  109. package/app/i18n/en/fields.ts +77 -0
  110. package/app/i18n/en/index.ts +12 -0
  111. package/app/i18n/en/jobs.ts +36 -0
  112. package/app/i18n/en/list.ts +160 -0
  113. package/app/i18n/en/record.ts +62 -0
  114. package/app/i18n/en/settings.ts +10 -0
  115. package/app/i18n/en/shell.ts +54 -0
  116. package/app/i18n/es/actions.ts +11 -0
  117. package/app/i18n/es/common.ts +23 -0
  118. package/app/i18n/es/dashboard.ts +28 -0
  119. package/app/i18n/es/fields.ts +77 -0
  120. package/app/i18n/es/index.ts +12 -0
  121. package/app/i18n/es/jobs.ts +36 -0
  122. package/app/i18n/es/list.ts +160 -0
  123. package/app/i18n/es/record.ts +62 -0
  124. package/app/i18n/es/settings.ts +10 -0
  125. package/app/i18n/es/shell.ts +54 -0
  126. package/app/i18n/fr/actions.ts +11 -0
  127. package/app/i18n/fr/common.ts +23 -0
  128. package/app/i18n/fr/dashboard.ts +28 -0
  129. package/app/i18n/fr/fields.ts +77 -0
  130. package/app/i18n/fr/index.ts +12 -0
  131. package/app/i18n/fr/jobs.ts +36 -0
  132. package/app/i18n/fr/list.ts +160 -0
  133. package/app/i18n/fr/record.ts +62 -0
  134. package/app/i18n/fr/settings.ts +10 -0
  135. package/app/i18n/fr/shell.ts +54 -0
  136. package/app/i18n/he/actions.ts +11 -0
  137. package/app/i18n/he/common.ts +23 -0
  138. package/app/i18n/he/dashboard.ts +28 -0
  139. package/app/i18n/he/fields.ts +77 -0
  140. package/app/i18n/he/index.ts +12 -0
  141. package/app/i18n/he/jobs.ts +36 -0
  142. package/app/i18n/he/list.ts +160 -0
  143. package/app/i18n/he/record.ts +62 -0
  144. package/app/i18n/he/settings.ts +10 -0
  145. package/app/i18n/he/shell.ts +54 -0
  146. package/app/i18n/index.ts +111 -0
  147. package/app/i18n/it/actions.ts +11 -0
  148. package/app/i18n/it/common.ts +23 -0
  149. package/app/i18n/it/dashboard.ts +28 -0
  150. package/app/i18n/it/fields.ts +77 -0
  151. package/app/i18n/it/index.ts +12 -0
  152. package/app/i18n/it/jobs.ts +36 -0
  153. package/app/i18n/it/list.ts +160 -0
  154. package/app/i18n/it/record.ts +62 -0
  155. package/app/i18n/it/settings.ts +10 -0
  156. package/app/i18n/it/shell.ts +54 -0
  157. package/app/i18n/ja/actions.ts +11 -0
  158. package/app/i18n/ja/common.ts +23 -0
  159. package/app/i18n/ja/dashboard.ts +28 -0
  160. package/app/i18n/ja/fields.ts +77 -0
  161. package/app/i18n/ja/index.ts +12 -0
  162. package/app/i18n/ja/jobs.ts +36 -0
  163. package/app/i18n/ja/list.ts +160 -0
  164. package/app/i18n/ja/record.ts +62 -0
  165. package/app/i18n/ja/settings.ts +10 -0
  166. package/app/i18n/ja/shell.ts +54 -0
  167. package/app/i18n/ko/actions.ts +11 -0
  168. package/app/i18n/ko/common.ts +23 -0
  169. package/app/i18n/ko/dashboard.ts +28 -0
  170. package/app/i18n/ko/fields.ts +77 -0
  171. package/app/i18n/ko/index.ts +12 -0
  172. package/app/i18n/ko/jobs.ts +36 -0
  173. package/app/i18n/ko/list.ts +160 -0
  174. package/app/i18n/ko/record.ts +62 -0
  175. package/app/i18n/ko/settings.ts +10 -0
  176. package/app/i18n/ko/shell.ts +54 -0
  177. package/app/i18n/nl/actions.ts +11 -0
  178. package/app/i18n/nl/common.ts +23 -0
  179. package/app/i18n/nl/dashboard.ts +28 -0
  180. package/app/i18n/nl/fields.ts +77 -0
  181. package/app/i18n/nl/index.ts +12 -0
  182. package/app/i18n/nl/jobs.ts +36 -0
  183. package/app/i18n/nl/list.ts +160 -0
  184. package/app/i18n/nl/record.ts +62 -0
  185. package/app/i18n/nl/settings.ts +10 -0
  186. package/app/i18n/nl/shell.ts +54 -0
  187. package/app/i18n/pl/actions.ts +11 -0
  188. package/app/i18n/pl/common.ts +23 -0
  189. package/app/i18n/pl/dashboard.ts +28 -0
  190. package/app/i18n/pl/fields.ts +77 -0
  191. package/app/i18n/pl/index.ts +12 -0
  192. package/app/i18n/pl/jobs.ts +36 -0
  193. package/app/i18n/pl/list.ts +160 -0
  194. package/app/i18n/pl/record.ts +62 -0
  195. package/app/i18n/pl/settings.ts +10 -0
  196. package/app/i18n/pl/shell.ts +54 -0
  197. package/app/i18n/pt/actions.ts +11 -0
  198. package/app/i18n/pt/common.ts +23 -0
  199. package/app/i18n/pt/dashboard.ts +28 -0
  200. package/app/i18n/pt/fields.ts +77 -0
  201. package/app/i18n/pt/index.ts +12 -0
  202. package/app/i18n/pt/jobs.ts +36 -0
  203. package/app/i18n/pt/list.ts +160 -0
  204. package/app/i18n/pt/record.ts +62 -0
  205. package/app/i18n/pt/settings.ts +10 -0
  206. package/app/i18n/pt/shell.ts +54 -0
  207. package/app/i18n/pt-BR/actions.ts +11 -0
  208. package/app/i18n/pt-BR/common.ts +23 -0
  209. package/app/i18n/pt-BR/dashboard.ts +28 -0
  210. package/app/i18n/pt-BR/fields.ts +77 -0
  211. package/app/i18n/pt-BR/index.ts +12 -0
  212. package/app/i18n/pt-BR/jobs.ts +36 -0
  213. package/app/i18n/pt-BR/list.ts +160 -0
  214. package/app/i18n/pt-BR/record.ts +62 -0
  215. package/app/i18n/pt-BR/settings.ts +10 -0
  216. package/app/i18n/pt-BR/shell.ts +54 -0
  217. package/app/i18n/ru/actions.ts +11 -0
  218. package/app/i18n/ru/common.ts +23 -0
  219. package/app/i18n/ru/dashboard.ts +28 -0
  220. package/app/i18n/ru/fields.ts +77 -0
  221. package/app/i18n/ru/index.ts +12 -0
  222. package/app/i18n/ru/jobs.ts +36 -0
  223. package/app/i18n/ru/list.ts +160 -0
  224. package/app/i18n/ru/record.ts +62 -0
  225. package/app/i18n/ru/settings.ts +10 -0
  226. package/app/i18n/ru/shell.ts +54 -0
  227. package/app/i18n/tr/actions.ts +11 -0
  228. package/app/i18n/tr/common.ts +23 -0
  229. package/app/i18n/tr/dashboard.ts +28 -0
  230. package/app/i18n/tr/fields.ts +77 -0
  231. package/app/i18n/tr/index.ts +12 -0
  232. package/app/i18n/tr/jobs.ts +36 -0
  233. package/app/i18n/tr/list.ts +160 -0
  234. package/app/i18n/tr/record.ts +62 -0
  235. package/app/i18n/tr/settings.ts +10 -0
  236. package/app/i18n/tr/shell.ts +54 -0
  237. package/app/i18n/uk/actions.ts +11 -0
  238. package/app/i18n/uk/common.ts +23 -0
  239. package/app/i18n/uk/dashboard.ts +28 -0
  240. package/app/i18n/uk/fields.ts +77 -0
  241. package/app/i18n/uk/index.ts +12 -0
  242. package/app/i18n/uk/jobs.ts +36 -0
  243. package/app/i18n/uk/list.ts +160 -0
  244. package/app/i18n/uk/record.ts +62 -0
  245. package/app/i18n/uk/settings.ts +10 -0
  246. package/app/i18n/uk/shell.ts +54 -0
  247. package/app/i18n/zh-Hans/actions.ts +11 -0
  248. package/app/i18n/zh-Hans/common.ts +23 -0
  249. package/app/i18n/zh-Hans/dashboard.ts +28 -0
  250. package/app/i18n/zh-Hans/fields.ts +77 -0
  251. package/app/i18n/zh-Hans/index.ts +12 -0
  252. package/app/i18n/zh-Hans/jobs.ts +36 -0
  253. package/app/i18n/zh-Hans/list.ts +160 -0
  254. package/app/i18n/zh-Hans/record.ts +62 -0
  255. package/app/i18n/zh-Hans/settings.ts +10 -0
  256. package/app/i18n/zh-Hans/shell.ts +54 -0
  257. package/app/layouts/default.vue +8 -0
  258. package/app/layouts/vector.vue +238 -0
  259. package/app/middleware/vector-auth.ts +16 -0
  260. package/app/pages/d/[name].vue +12 -0
  261. package/app/pages/index.vue +22 -0
  262. package/app/pages/jobs/[id].vue +52 -0
  263. package/app/pages/jobs/index.vue +42 -0
  264. package/app/pages/login.vue +109 -0
  265. package/app/pages/p/[name].vue +37 -0
  266. package/app/pages/r/[resource]/[id]/edit.vue +54 -0
  267. package/app/pages/r/[resource]/[id]/index.vue +143 -0
  268. package/app/pages/r/[resource]/create.vue +53 -0
  269. package/app/pages/r/[resource]/index.vue +29 -0
  270. package/app/pages/settings.vue +104 -0
  271. package/app/plugins/vector-csp.client.ts +14 -0
  272. package/app/plugins/vector-extensions.client.ts +84 -0
  273. package/app/plugins/vector-i18n.ts +76 -0
  274. package/app/types/vector.ts +438 -0
  275. package/app/utils/files.ts +14 -0
  276. package/app/utils/format.ts +100 -0
  277. package/app/utils/richtext.ts +305 -0
  278. package/app/utils/rules.ts +113 -0
  279. package/app/utils/translate.ts +45 -0
  280. package/app/utils/widgets.ts +11 -0
  281. package/nuxt.config.ts +83 -0
  282. package/package.json +83 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@kinetis/vector-ui` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.9.1] - 2026-10-18
11
+
12
+ ### Changed
13
+
14
+ - Tiptap 3.32.0 for the rich-text editor, every `@tiptap/*` package
15
+ together, and `@tiptap/suggestion` declared directly, since
16
+ `@tiptap/extension-mention` requires that exact version as a peer.
17
+
18
+ ## [0.9.0] - 2026-10-18
19
+
20
+ ### Added
21
+
22
+ - A Nuxt 4 layer on Vuetify 4 that renders the `kinetis/vector`
23
+ descriptor: navigation with badges, lists, record pages, generated
24
+ forms, relation panels, actions, dashboards, jobs, notifications,
25
+ settings and global search.
26
+ - Standalone mode with a bearer token and refresh, and hosted mode on
27
+ the application's session cookie with CSRF, with an idle sign-out in
28
+ both.
29
+ - A rich-text editor on Tiptap and a Markdown editor, rendered through
30
+ DOMPurify.
31
+ - Overrides for cells, inputs and widget renderers in `app.config`, and
32
+ runtime extensions through `window.Vector`, custom pages included.
33
+ - English and sixteen machine-translated languages, right to left for
34
+ Hebrew and Arabic, with per-locale message overrides.
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alon Noy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,73 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://kinetis.dev/vector/docs/_static/logo.svg" alt="Kinetis" width="420">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <p align="center">
6
+ <strong>@kinetis/vector-ui</strong>
7
+ <br>
8
+ <strong>The Kinetis Vector admin UI, as a Nuxt layer on Vuetify</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/v/@kinetis/vector-ui?label=version" alt="npm Version"></a>
13
+ <a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/dm/@kinetis/vector-ui" alt="npm Downloads"></a>
14
+ <a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/l/@kinetis/vector-ui" alt="License"></a>
15
+ <a href="https://github.com/kinetis-dev/vector-dev/actions/workflows/ci.yml"><img src="https://github.com/kinetis-dev/vector-dev/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ Part of [Kinetis](https://kinetis.dev/), a non-blocking PHP framework for
21
+ API-first applications. Vector is developed in the
22
+ [kinetis-dev/vector-dev](https://github.com/kinetis-dev/vector-dev)
23
+ monorepo, beside its PHP package and a demo application.
24
+
25
+ The admin UI for [`kinetis/vector`](https://packagist.org/packages/kinetis/vector):
26
+ a Nuxt 4 layer on Vuetify 4 that renders the descriptor the API
27
+ publishes — navigation, lists, forms, relation panels, actions,
28
+ dashboards, jobs, settings — and contains no application knowledge. An
29
+ application is a Nuxt app that extends the layer, served as a static
30
+ site on its own origin with a bearer token (standalone), or published
31
+ into the PHP application and served at `/vector/ui` behind its session
32
+ (hosted).
33
+
34
+ ## Installation
35
+
36
+ ```sh
37
+ pnpm add @kinetis/vector-ui nuxt vuetify vuetify-nuxt-module
38
+ ```
39
+
40
+ ```ts
41
+ // nuxt.config.ts
42
+ export default defineNuxtConfig({
43
+ extends: ['@kinetis/vector-ui'],
44
+ runtimeConfig: { public: { vector: { apiBase: 'https://api.example.com' } } },
45
+ })
46
+ ```
47
+
48
+ `pnpm nuxt dev` for development, `pnpm nuxt generate` for a static site
49
+ any web server hosts. The API allows the UI's origin through
50
+ `CorsMiddleware` and offers the `POST /auth/login` endpoint the sign-in
51
+ page calls; see
52
+ [Deployment](https://kinetis.dev/vector/docs/deployment.html).
53
+
54
+ ## Configuration
55
+
56
+ | Where | Key | Default | Purpose |
57
+ |---|---|---|---|
58
+ | `runtimeConfig.public.vector` | `apiBase` | `http://localhost:8080` | The API origin in standalone mode (`NUXT_PUBLIC_VECTOR_API_BASE`). |
59
+ | | `extensions` | `[]` | ES-module URLs to import after boot, in standalone mode. |
60
+ | `app.config.vector` | `brand.title` | `Vector` | The title before the Branding settings load. |
61
+ | | `usersResource` | `null` | The resource whose records are users, for **Impersonate**. |
62
+ | | `idleMinutes` | `30` | Minutes without use after which the admin signs the user out; `0` never. |
63
+ | | `locale` | `en` | The language before the descriptor names the user's. |
64
+
65
+ Every setting, including component overrides, custom pages and
66
+ messages:
67
+ [kinetis.dev/vector/docs/configuration.html](https://kinetis.dev/vector/docs/configuration.html).
68
+ Cells, inputs, widgets and runtime extensions:
69
+ [Customizing the UI](https://kinetis.dev/vector/docs/customizing-the-ui.html).
70
+
71
+ ## License
72
+
73
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,63 @@
1
+ export default defineAppConfig({
2
+ vector: {
3
+ brand: {
4
+ title: 'Vector',
5
+ },
6
+ // A path prefix inside the router; the deployment's URL prefix is
7
+ // Nuxt's app.baseURL ('/' standalone, '/vector/ui/' hosted).
8
+ basePath: '',
9
+ // Field components a consuming app adds or replaces, keyed by the
10
+ // descriptor's field type or explicit component name (Phase 2).
11
+ fields: {} as Record<string, string>,
12
+ // Input components, the same way (Phase 3).
13
+ inputs: {} as Record<string, string>,
14
+ // The Ctrl/Cmd+K global search palette (Phase 4).
15
+ commandPalette: true,
16
+ // Widget renderers by type, the same way (Phase 5).
17
+ widgets: {} as Record<string, string>,
18
+ // The resource whose records are users, for the Impersonate button (Phase 5).
19
+ usersResource: null as string | null,
20
+ // The UI's language before the descriptor names the user's, when neither the hosted shell nor this browser does.
21
+ locale: 'en',
22
+ // Minutes without a pointer, key, touch or scroll after which the admin signs the user out; 0 never does.
23
+ idleMinutes: 30,
24
+ // Messages an application adds or overrides, by locale and area: { de: { common: { save: 'Speichern' } } }.
25
+ messages: {} as Record<string, Record<string, unknown>>,
26
+ // Custom pages at /p/{name}: { stock: { title: 'Stock', icon: 'mdi-warehouse', component: 'StockPage' } },
27
+ // the component registered globally by the application.
28
+ pages: {} as Record<string, { title: string, icon?: string, group?: string, component: string }>,
29
+ },
30
+ })
31
+
32
+ declare module 'nuxt/schema' {
33
+ interface AppConfigInput {
34
+ vector?: {
35
+ brand?: { title?: string }
36
+ basePath?: string
37
+ fields?: Record<string, string>
38
+ inputs?: Record<string, string>
39
+ commandPalette?: boolean
40
+ widgets?: Record<string, string>
41
+ usersResource?: string | null
42
+ locale?: string
43
+ idleMinutes?: number
44
+ messages?: Record<string, Record<string, unknown>>
45
+ pages?: Record<string, { title: string, icon?: string, group?: string, component: string }>
46
+ }
47
+ }
48
+ interface AppConfig {
49
+ vector: {
50
+ brand: { title: string }
51
+ basePath: string
52
+ fields: Record<string, string>
53
+ inputs: Record<string, string>
54
+ commandPalette: boolean
55
+ widgets: Record<string, string>
56
+ usersResource: string | null
57
+ locale: string
58
+ idleMinutes: number
59
+ messages: Record<string, Record<string, unknown>>
60
+ pages: Record<string, { title: string, icon?: string, group?: string, component: string }>
61
+ }
62
+ }
63
+ }
package/app/app.vue ADDED
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ // The document's language follows the admin's locale, for screen readers and hyphenation.
3
+ const { locale } = useVectorT()
4
+ useHead({ htmlAttrs: { lang: () => locale.value } })
5
+ </script>
6
+
7
+ <template>
8
+ <NuxtLayout>
9
+ <NuxtPage />
10
+ </NuxtLayout>
11
+ </template>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import type { SubmitEventPromise } from 'vuetify'
3
+ import type { VectorAction, VectorResource, VectorSelection } from '#vector/app/types/vector'
4
+
5
+ // Runs one action: the confirmation, the dialog's fields, the request,
6
+ // and what the response asks the client to do.
7
+ const props = defineProps<{
8
+ resource: VectorResource
9
+ action: VectorAction
10
+ selection: VectorSelection | null
11
+ count: number
12
+ }>()
13
+
14
+ const emit = defineEmits<{ done: [affected: number], cancel: [] }>()
15
+
16
+ const { t, noun } = useVectorT()
17
+ const appConfig = useAppConfig()
18
+ const notifications = useVectorNotifications()
19
+ const client = useVectorResource(() => props.resource.slug)
20
+
21
+ const values = reactive<Record<string, unknown>>({})
22
+ const serverErrors = reactive<Record<string, string[]>>({})
23
+ const formError = ref<string | null>(null)
24
+ const running = ref(false)
25
+
26
+ for (const field of props.action.fields) {
27
+ values[field.name] = initialValue(field)
28
+ }
29
+
30
+ const subject = computed(() => props.action.standalone
31
+ ? ''
32
+ : props.selection?.all
33
+ ? t('actions.subjectAll', { label: noun(props.resource.labelSingular), count: props.count })
34
+ : t('actions.subjectCount', { count: props.count, label: noun(props.count === 1 ? props.resource.labelSingular : props.resource.label) }))
35
+
36
+ async function run(event: SubmitEventPromise) {
37
+ const { valid } = await event
38
+ if (!valid) return
39
+
40
+ running.value = true
41
+ formError.value = null
42
+ for (const key of Object.keys(serverErrors)) delete serverErrors[key]
43
+
44
+ try {
45
+ const response = await client.runAction(props.action.name, props.action.standalone ? null : props.selection, { ...values })
46
+
47
+ switch (response.type) {
48
+ case 'message':
49
+ notifications.success(response.message ?? t('actions.done', { label: props.action.label }))
50
+ break
51
+ case 'danger':
52
+ formError.value = response.message ?? t('actions.declined')
53
+ return
54
+ case 'redirect':
55
+ if (response.message) notifications.success(response.message)
56
+ emit('done', response.affected)
57
+ await navigateTo(`${appConfig.vector.basePath}${response.url ?? ''}`)
58
+ return
59
+ case 'url':
60
+ if (response.message) notifications.success(response.message)
61
+ if (response.url && import.meta.client) window.open(response.url, '_blank', 'noopener')
62
+ break
63
+ case 'download':
64
+ notifications.success(t('actions.downloadStarted'))
65
+ break
66
+ case 'job':
67
+ notifications.success(response.message ?? t('actions.queued', { label: props.action.label }))
68
+ emit('done', 0)
69
+ await navigateTo(`${appConfig.vector.basePath}/jobs/${response.jobId}`)
70
+ return
71
+ }
72
+
73
+ emit('done', response.affected)
74
+ } catch (error) {
75
+ formError.value = applyViolations(error, values, serverErrors)
76
+ } finally {
77
+ running.value = false
78
+ }
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <v-dialog :model-value="true" max-width="560" persistent @update:model-value="emit('cancel')">
84
+ <v-form validate-on="blur lazy" @submit.prevent="run">
85
+ <v-card>
86
+ <v-card-title class="d-flex align-center ga-2">
87
+ <v-icon v-if="action.icon" :icon="action.icon" />
88
+ {{ action.label }}
89
+ </v-card-title>
90
+ <v-card-text>
91
+ <p v-if="action.confirmation" class="mb-4">{{ action.confirmation }}</p>
92
+ <i18n-t v-else-if="!action.standalone" keypath="actions.runOn" tag="p" class="mb-4" scope="global">
93
+ <template #action><strong>{{ action.label }}</strong></template>
94
+ <template #subject>{{ subject }}</template>
95
+ </i18n-t>
96
+ <v-alert v-if="formError" type="error" variant="tonal" class="mb-4" :text="formError" />
97
+ <VxFieldsForm v-if="action.fields.length" :fields="action.fields" :values="values" :server-errors="serverErrors" :resource="resource.slug" @clear="(name: string) => delete serverErrors[name]" />
98
+ </v-card-text>
99
+ <v-card-actions>
100
+ <v-spacer />
101
+ <v-btn variant="text" :disabled="running" @click="emit('cancel')">{{ t('common.cancel') }}</v-btn>
102
+ <v-btn type="submit" :color="action.destructive ? 'error' : 'primary'" :loading="running">{{ action.label }}</v-btn>
103
+ </v-card-actions>
104
+ </v-card>
105
+ </v-form>
106
+ </v-dialog>
107
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ value: unknown }>()
3
+ const { t } = useVectorT()
4
+ </script>
5
+
6
+ <template>
7
+ <v-icon v-if="value === true" icon="mdi-check-circle" color="success" size="small" />
8
+ <v-icon v-else-if="value === false" icon="mdi-close-circle-outline" color="grey" size="small" />
9
+ <span v-else class="text-disabled">{{ t('common.none') }}</span>
10
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ value: unknown }>()
3
+ const { t } = useVectorT()
4
+ </script>
5
+
6
+ <template>
7
+ <span v-if="typeof value === 'string' && value" class="d-inline-flex align-center ga-2">
8
+ <v-avatar :color="value" size="16" />
9
+ <code>{{ value }}</code>
10
+ </span>
11
+ <span v-else class="text-disabled">{{ t('common.none') }}</span>
12
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import type { VectorCountValue } from '#vector/app/types/vector'
3
+
4
+ const props = defineProps<{ value: unknown }>()
5
+ const count = computed(() => (props.value && typeof props.value === 'object' && 'count' in props.value ? (props.value as VectorCountValue).count : null))
6
+ const { t } = useVectorT()
7
+ </script>
8
+
9
+ <template>
10
+ <v-chip v-if="count !== null" size="small" variant="tonal" :color="count > 0 ? 'primary' : undefined">{{ count }}</v-chip>
11
+ <span v-else class="text-disabled">{{ t('common.none') }}</span>
12
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{ value: unknown }>()
3
+ const text = computed(() => typeof props.value === 'string' ? formatDate(props.value) : '')
4
+ const { t } = useVectorT()
5
+ </script>
6
+
7
+ <template>
8
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
9
+ <span v-else class="text-no-wrap">{{ text }}</span>
10
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{ value: unknown }>()
3
+ const text = computed(() => typeof props.value === 'string' ? formatDateTime(props.value) : '')
4
+ const { t } = useVectorT()
5
+ </script>
6
+
7
+ <template>
8
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
9
+ <span v-else class="text-no-wrap" :title="String(value)">{{ text }}</span>
10
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ value: unknown }>()
3
+ const { t } = useVectorT()
4
+ </script>
5
+
6
+ <template>
7
+ <a v-if="typeof value === 'string' && value" :href="`mailto:${value}`" @click.stop>{{ value }}</a>
8
+ <span v-else class="text-disabled">{{ t('common.none') }}</span>
9
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField } from '#vector/app/types/vector'
3
+
4
+ const props = defineProps<{ field: VectorField, value: unknown }>()
5
+ const option = computed(() => props.field.options?.find(o => o.value === props.value))
6
+ const { t } = useVectorT()
7
+ </script>
8
+
9
+ <template>
10
+ <span v-if="value === null || value === undefined" class="text-disabled">{{ t('common.none') }}</span>
11
+ <v-chip v-else size="small" :color="option?.color ?? undefined" variant="tonal" label>
12
+ {{ option?.label ?? String(value) }}
13
+ </v-chip>
14
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField, VectorResource } from '#vector/app/types/vector'
3
+
4
+ // A stored document as a download button, fetched with the API's credentials.
5
+ const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
6
+
7
+ const client = useVectorResource(() => props.resource.slug)
8
+ const notifications = useVectorNotifications()
9
+ const key = computed(() => (typeof props.value === 'string' && props.value !== '' ? props.value : null))
10
+ const name = computed(() => key.value?.split('/').pop() ?? '')
11
+ const busy = ref(false)
12
+
13
+ async function download() {
14
+ if (!key.value) return
15
+ busy.value = true
16
+ try {
17
+ await client.downloadFile(props.record.id as string | number, props.field.name, name.value)
18
+ } catch (error) {
19
+ notifications.error(vectorErrorMessage(error))
20
+ } finally {
21
+ busy.value = false
22
+ }
23
+ }
24
+ const { t } = useVectorT()
25
+ </script>
26
+
27
+ <template>
28
+ <span v-if="!key" class="text-disabled">{{ t('common.none') }}</span>
29
+ <v-btn v-else size="small" variant="tonal" prepend-icon="mdi-download" :loading="busy" @click.stop="download">{{ context === 'show' ? name : t('common.download') }}</v-btn>
30
+ </template>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField, VectorResource } from '#vector/app/types/vector'
3
+
4
+ // A stored image as a thumbnail. The bytes come through the API client,
5
+ // so a bearer-authenticated admin sees them too; a key with no stored
6
+ // file (or no upload support) shows as its name.
7
+ const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
8
+
9
+ const client = useVectorResource(() => props.resource.slug)
10
+ const url = ref<string | null>(null)
11
+ const failed = ref(false)
12
+ const key = computed(() => (typeof props.value === 'string' && props.value !== '' ? props.value : null))
13
+ const size = computed(() => (props.context === 'show' ? 200 : 40))
14
+
15
+ watch([key, () => props.record.id], async () => {
16
+ url.value = null
17
+ failed.value = false
18
+ if (!key.value || props.record.id === undefined || props.record.id === null) return
19
+
20
+ try {
21
+ url.value = await dataUrl(await client.fileBlob(props.record.id as string | number, props.field.name))
22
+ } catch {
23
+ failed.value = true
24
+ }
25
+ }, { immediate: true })
26
+
27
+ const { t } = useVectorT()
28
+ </script>
29
+
30
+ <template>
31
+ <span v-if="!key" class="text-disabled">{{ t('common.none') }}</span>
32
+ <v-img v-else-if="url" :src="url" :width="size" :height="size" :alt="field.label" cover class="rounded" />
33
+ <span v-else-if="failed" class="d-inline-flex align-center ga-1 text-medium-emphasis" :title="key">
34
+ <v-icon icon="mdi-image-off-outline" size="small" />
35
+ <span class="text-caption">{{ key.split('/').pop() }}</span>
36
+ </span>
37
+ <v-progress-circular v-else indeterminate size="16" width="2" :aria-label="t('fields.cell.loading', { label: field.label })" />
38
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
3
+ const pretty = computed(() => {
4
+ if (typeof props.value !== 'string' || props.value === '') return ''
5
+ try {
6
+ return JSON.stringify(JSON.parse(props.value), null, props.context === 'show' ? 2 : 0)
7
+ } catch {
8
+ return props.value
9
+ }
10
+ })
11
+ const { t } = useVectorT()
12
+ </script>
13
+
14
+ <template>
15
+ <span v-if="pretty === ''" class="text-disabled">{{ t('common.none') }}</span>
16
+ <pre v-else-if="context === 'show'" class="text-body-2 pa-3 rounded bg-surface-light overflow-auto">{{ pretty }}</pre>
17
+ <code v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ pretty }}</code>
18
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ // A key-value field: its first entries as chips in a list, every entry
3
+ // as a two-column table on a record.
4
+ const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
5
+ const entries = computed(() => (props.value && typeof props.value === 'object' && !Array.isArray(props.value)
6
+ ? Object.entries(props.value as Record<string, unknown>).map(([key, value]) => [key, String(value ?? '')] as const)
7
+ : []))
8
+ const { t } = useVectorT()
9
+ </script>
10
+
11
+ <template>
12
+ <span v-if="!entries.length" class="text-disabled">{{ t('common.none') }}</span>
13
+ <v-table v-else-if="context === 'show'" density="compact">
14
+ <tbody>
15
+ <tr v-for="[key, text] in entries" :key="key">
16
+ <th scope="row" class="text-medium-emphasis font-weight-regular">{{ key }}</th>
17
+ <td>{{ text }}</td>
18
+ </tr>
19
+ </tbody>
20
+ </v-table>
21
+ <span v-else class="d-inline-flex flex-wrap align-center ga-1">
22
+ <v-chip v-for="[key, text] in entries.slice(0, 3)" :key="key" size="x-small" label>{{ key }}: {{ text }}</v-chip>
23
+ <span v-if="entries.length > 3" class="text-caption text-medium-emphasis">{{ t('fields.keyValue.more', { count: entries.length - 3 }) }}</span>
24
+ </span>
25
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ // Markdown source. On the record page it is rendered: markdown-it with HTML
3
+ // in the source turned off (raw HTML shows as text), then DOMPurify with the
4
+ // same allow-list as rich text — the only other `v-html` in the layer. In a
5
+ // list, the plain text, shortened.
6
+ const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
7
+ const text = computed(() => typeof props.value === 'string' ? props.value : '')
8
+ const html = computed(() => props.context === 'show' && text.value !== '' ? markdownHtml(text.value) : '')
9
+ const plain = computed(() => props.context === 'index' && text.value !== '' ? markdownPlain(text.value) : '')
10
+ const { t } = useVectorT()
11
+ </script>
12
+
13
+ <template>
14
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
15
+ <!-- eslint-disable-next-line vue/no-v-html -- rendered with HTML off, then sanitized (see above) -->
16
+ <div v-else-if="context === 'show'" class="vx-richtext-view text-body-1" v-html="html" />
17
+ <span v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ plain }}</span>
18
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField } from '#vector/app/types/vector'
3
+
4
+ const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown> }>()
5
+ const text = computed(() => {
6
+ if (typeof props.value !== 'number') return ''
7
+ const currency = currencyOf(props.field, props.record)
8
+ return currency ? formatMoney(props.value, currency) : formatNumber(props.value / 100, 2)
9
+ })
10
+ const { t } = useVectorT()
11
+ </script>
12
+
13
+ <template>
14
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
15
+ <span v-else class="text-no-wrap font-weight-medium">{{ text }}</span>
16
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import { fieldProps, type VectorField } from '#vector/app/types/vector'
3
+
4
+ const props = defineProps<{ field: VectorField, value: unknown }>()
5
+ const text = computed(() => {
6
+ if (typeof props.value !== 'number') return ''
7
+ const decimals = fieldProps(props.field).decimals
8
+ return formatNumber(props.value, typeof decimals === 'number' ? decimals : undefined)
9
+ })
10
+ const { t } = useVectorT()
11
+ </script>
12
+
13
+ <template>
14
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
15
+ <span v-else class="text-no-wrap">{{ text }}</span>
16
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField, VectorRelationValue } from '#vector/app/types/vector'
3
+
4
+ const props = defineProps<{ field: VectorField, value: unknown }>()
5
+ const appConfig = useAppConfig()
6
+ const related = computed(() => (props.value && typeof props.value === 'object' && 'id' in props.value ? props.value as VectorRelationValue : null))
7
+ const to = computed(() => related.value && props.field.relation
8
+ ? `${appConfig.vector.basePath}/r/${props.field.relation.resource}/${encodeURIComponent(String(related.value.id))}`
9
+ : undefined)
10
+ const { t } = useVectorT()
11
+ </script>
12
+
13
+ <template>
14
+ <v-chip v-if="related" size="small" variant="outlined" :to="to" link @click.stop>{{ related.label }}</v-chip>
15
+ <span v-else class="text-disabled">{{ t('common.none') }}</span>
16
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import type { VectorField, VectorResource } from '#vector/app/types/vector'
3
+
4
+ // A rich-text value. On the record page it is shown as HTML — and this is
5
+ // the one `v-html` in the layer. It is safe because the HTML is never the
6
+ // stored value: it is generated here from the document through the
7
+ // field's fixed schema (anything outside the schema cannot be expressed),
8
+ // then passed through DOMPurify with an allow-list matching that schema
9
+ // (no style, no event handlers, links only to http(s), mailto or relative
10
+ // places). Images carry their storage key and are fetched with the API's
11
+ // credentials. In a list, the plain text, shortened.
12
+ const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
13
+
14
+ const { t } = useVectorT()
15
+ const client = useVectorResource(() => props.resource.slug)
16
+ const { richText: custom } = useVectorExtensions()
17
+ const extensions = richTextExtensions({ features: props.field.richtext?.features ?? [], custom })
18
+ const text = computed(() => typeof props.value === 'string' ? props.value : '')
19
+ const html = computed(() => props.context === 'show' && text.value !== '' ? richTextHtml(text.value, extensions) : '')
20
+ const plain = computed(() => props.context === 'index' && text.value !== '' ? richTextPlain(text.value, extensions) : '')
21
+
22
+ const root = ref<HTMLElement | null>(null)
23
+ watch([html, root], async () => {
24
+ await nextTick()
25
+ const imageField = props.field.richtext?.imageField
26
+ const id = props.record.id
27
+ if (!root.value || !imageField || id === undefined || id === null) return
28
+
29
+ for (const img of root.value.querySelectorAll<HTMLImageElement>('img[data-vector-key]')) {
30
+ const key = img.getAttribute('data-vector-key')!
31
+ client.fileBlob(id as string | number, imageField, key).then(dataUrl).then((url) => { img.src = url }).catch(() => undefined)
32
+ }
33
+ }, { immediate: true })
34
+ </script>
35
+
36
+ <template>
37
+ <span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
38
+ <!-- eslint-disable-next-line vue/no-v-html -- generated through the schema, then sanitized (see above) -->
39
+ <div v-else-if="context === 'show'" ref="root" class="vx-richtext-view text-body-1" v-html="html" />
40
+ <span v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ plain }}</span>
41
+ </template>