@kinetis/vector-ui 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/README.md +72 -2
- package/app/app.config.ts +63 -0
- package/app/app.vue +11 -0
- package/app/components/Vx/Action/Dialog.vue +107 -0
- package/app/components/Vx/Cell/Boolean.vue +10 -0
- package/app/components/Vx/Cell/Color.vue +12 -0
- package/app/components/Vx/Cell/Count.vue +12 -0
- package/app/components/Vx/Cell/Date.vue +10 -0
- package/app/components/Vx/Cell/DateTime.vue +10 -0
- package/app/components/Vx/Cell/Email.vue +9 -0
- package/app/components/Vx/Cell/Enum.vue +14 -0
- package/app/components/Vx/Cell/File.vue +30 -0
- package/app/components/Vx/Cell/Image.vue +38 -0
- package/app/components/Vx/Cell/Json.vue +18 -0
- package/app/components/Vx/Cell/KeyValue.vue +25 -0
- package/app/components/Vx/Cell/Markdown.vue +18 -0
- package/app/components/Vx/Cell/Money.vue +16 -0
- package/app/components/Vx/Cell/Number.vue +16 -0
- package/app/components/Vx/Cell/Relation.vue +16 -0
- package/app/components/Vx/Cell/RichText.vue +41 -0
- package/app/components/Vx/Cell/Text.vue +13 -0
- package/app/components/Vx/Cell/Url.vue +11 -0
- package/app/components/Vx/Cell.vue +25 -0
- package/app/components/Vx/CommandPalette.vue +118 -0
- package/app/components/Vx/Confirm.vue +19 -0
- package/app/components/Vx/Dashboard.vue +27 -0
- package/app/components/Vx/FieldsForm.vue +46 -0
- package/app/components/Vx/Import/Dialog.vue +131 -0
- package/app/components/Vx/Inbox.vue +49 -0
- package/app/components/Vx/Input/Boolean.vue +10 -0
- package/app/components/Vx/Input/Currency.vue +25 -0
- package/app/components/Vx/Input/DateTime.vue +38 -0
- package/app/components/Vx/Input/Enum.vue +21 -0
- package/app/components/Vx/Input/KeyValue.vue +78 -0
- package/app/components/Vx/Input/Markdown.vue +120 -0
- package/app/components/Vx/Input/Money.vue +47 -0
- package/app/components/Vx/Input/Number.vue +24 -0
- package/app/components/Vx/Input/Relation.vue +127 -0
- package/app/components/Vx/Input/Repeater.vue +134 -0
- package/app/components/Vx/Input/RichText.vue +182 -0
- package/app/components/Vx/Input/Text.vue +21 -0
- package/app/components/Vx/Input/Textarea.vue +20 -0
- package/app/components/Vx/Input/Upload.vue +89 -0
- package/app/components/Vx/Layout.vue +111 -0
- package/app/components/Vx/List/BulkBar.vue +44 -0
- package/app/components/Vx/List/BulkEdit.vue +91 -0
- package/app/components/Vx/List/ColumnManager.vue +74 -0
- package/app/components/Vx/List/FilterBar.vue +224 -0
- package/app/components/Vx/List/ViewMenu.vue +115 -0
- package/app/components/Vx/Notifications.vue +9 -0
- package/app/components/Vx/Resource/Audit.vue +82 -0
- package/app/components/Vx/Resource/Form.vue +351 -0
- package/app/components/Vx/Resource/RelationPanel.vue +217 -0
- package/app/components/Vx/Resource/Show.vue +35 -0
- package/app/components/Vx/Resource/Table.vue +513 -0
- package/app/components/Vx/Widget/Card.vue +80 -0
- package/app/components/Vx/Widget/Partition.vue +21 -0
- package/app/components/Vx/Widget/Progress.vue +22 -0
- package/app/components/Vx/Widget/Stat.vue +22 -0
- package/app/components/Vx/Widget/Table.vue +31 -0
- package/app/components/Vx/Widget/Trend.vue +37 -0
- package/app/composables/useConfirm.ts +34 -0
- package/app/composables/useLeaveGuard.ts +21 -0
- package/app/composables/useVectorApi.ts +74 -0
- package/app/composables/useVectorAuthStore.ts +252 -0
- package/app/composables/useVectorBranding.ts +45 -0
- package/app/composables/useVectorDashboards.ts +15 -0
- package/app/composables/useVectorExtensions.ts +44 -0
- package/app/composables/useVectorFields.ts +68 -0
- package/app/composables/useVectorForm.ts +36 -0
- package/app/composables/useVectorIdle.ts +39 -0
- package/app/composables/useVectorImpersonation.ts +78 -0
- package/app/composables/useVectorInputs.ts +61 -0
- package/app/composables/useVectorJobs.ts +62 -0
- package/app/composables/useVectorLocale.ts +70 -0
- package/app/composables/useVectorNotifications.ts +24 -0
- package/app/composables/useVectorNotificationsInbox.ts +26 -0
- package/app/composables/useVectorPreferences.ts +30 -0
- package/app/composables/useVectorResource.ts +305 -0
- package/app/composables/useVectorRuntime.ts +42 -0
- package/app/composables/useVectorSchema.ts +56 -0
- package/app/composables/useVectorT.ts +9 -0
- package/app/composables/useVectorViews.ts +25 -0
- package/app/i18n/ar/actions.ts +11 -0
- package/app/i18n/ar/common.ts +23 -0
- package/app/i18n/ar/dashboard.ts +28 -0
- package/app/i18n/ar/fields.ts +77 -0
- package/app/i18n/ar/index.ts +12 -0
- package/app/i18n/ar/jobs.ts +36 -0
- package/app/i18n/ar/list.ts +160 -0
- package/app/i18n/ar/record.ts +62 -0
- package/app/i18n/ar/settings.ts +10 -0
- package/app/i18n/ar/shell.ts +54 -0
- package/app/i18n/de/actions.ts +11 -0
- package/app/i18n/de/common.ts +23 -0
- package/app/i18n/de/dashboard.ts +28 -0
- package/app/i18n/de/fields.ts +77 -0
- package/app/i18n/de/index.ts +12 -0
- package/app/i18n/de/jobs.ts +36 -0
- package/app/i18n/de/list.ts +160 -0
- package/app/i18n/de/record.ts +62 -0
- package/app/i18n/de/settings.ts +10 -0
- package/app/i18n/de/shell.ts +54 -0
- package/app/i18n/en/actions.ts +11 -0
- package/app/i18n/en/common.ts +23 -0
- package/app/i18n/en/dashboard.ts +28 -0
- package/app/i18n/en/fields.ts +77 -0
- package/app/i18n/en/index.ts +12 -0
- package/app/i18n/en/jobs.ts +36 -0
- package/app/i18n/en/list.ts +160 -0
- package/app/i18n/en/record.ts +62 -0
- package/app/i18n/en/settings.ts +10 -0
- package/app/i18n/en/shell.ts +54 -0
- package/app/i18n/es/actions.ts +11 -0
- package/app/i18n/es/common.ts +23 -0
- package/app/i18n/es/dashboard.ts +28 -0
- package/app/i18n/es/fields.ts +77 -0
- package/app/i18n/es/index.ts +12 -0
- package/app/i18n/es/jobs.ts +36 -0
- package/app/i18n/es/list.ts +160 -0
- package/app/i18n/es/record.ts +62 -0
- package/app/i18n/es/settings.ts +10 -0
- package/app/i18n/es/shell.ts +54 -0
- package/app/i18n/fr/actions.ts +11 -0
- package/app/i18n/fr/common.ts +23 -0
- package/app/i18n/fr/dashboard.ts +28 -0
- package/app/i18n/fr/fields.ts +77 -0
- package/app/i18n/fr/index.ts +12 -0
- package/app/i18n/fr/jobs.ts +36 -0
- package/app/i18n/fr/list.ts +160 -0
- package/app/i18n/fr/record.ts +62 -0
- package/app/i18n/fr/settings.ts +10 -0
- package/app/i18n/fr/shell.ts +54 -0
- package/app/i18n/he/actions.ts +11 -0
- package/app/i18n/he/common.ts +23 -0
- package/app/i18n/he/dashboard.ts +28 -0
- package/app/i18n/he/fields.ts +77 -0
- package/app/i18n/he/index.ts +12 -0
- package/app/i18n/he/jobs.ts +36 -0
- package/app/i18n/he/list.ts +160 -0
- package/app/i18n/he/record.ts +62 -0
- package/app/i18n/he/settings.ts +10 -0
- package/app/i18n/he/shell.ts +54 -0
- package/app/i18n/index.ts +111 -0
- package/app/i18n/it/actions.ts +11 -0
- package/app/i18n/it/common.ts +23 -0
- package/app/i18n/it/dashboard.ts +28 -0
- package/app/i18n/it/fields.ts +77 -0
- package/app/i18n/it/index.ts +12 -0
- package/app/i18n/it/jobs.ts +36 -0
- package/app/i18n/it/list.ts +160 -0
- package/app/i18n/it/record.ts +62 -0
- package/app/i18n/it/settings.ts +10 -0
- package/app/i18n/it/shell.ts +54 -0
- package/app/i18n/ja/actions.ts +11 -0
- package/app/i18n/ja/common.ts +23 -0
- package/app/i18n/ja/dashboard.ts +28 -0
- package/app/i18n/ja/fields.ts +77 -0
- package/app/i18n/ja/index.ts +12 -0
- package/app/i18n/ja/jobs.ts +36 -0
- package/app/i18n/ja/list.ts +160 -0
- package/app/i18n/ja/record.ts +62 -0
- package/app/i18n/ja/settings.ts +10 -0
- package/app/i18n/ja/shell.ts +54 -0
- package/app/i18n/ko/actions.ts +11 -0
- package/app/i18n/ko/common.ts +23 -0
- package/app/i18n/ko/dashboard.ts +28 -0
- package/app/i18n/ko/fields.ts +77 -0
- package/app/i18n/ko/index.ts +12 -0
- package/app/i18n/ko/jobs.ts +36 -0
- package/app/i18n/ko/list.ts +160 -0
- package/app/i18n/ko/record.ts +62 -0
- package/app/i18n/ko/settings.ts +10 -0
- package/app/i18n/ko/shell.ts +54 -0
- package/app/i18n/nl/actions.ts +11 -0
- package/app/i18n/nl/common.ts +23 -0
- package/app/i18n/nl/dashboard.ts +28 -0
- package/app/i18n/nl/fields.ts +77 -0
- package/app/i18n/nl/index.ts +12 -0
- package/app/i18n/nl/jobs.ts +36 -0
- package/app/i18n/nl/list.ts +160 -0
- package/app/i18n/nl/record.ts +62 -0
- package/app/i18n/nl/settings.ts +10 -0
- package/app/i18n/nl/shell.ts +54 -0
- package/app/i18n/pl/actions.ts +11 -0
- package/app/i18n/pl/common.ts +23 -0
- package/app/i18n/pl/dashboard.ts +28 -0
- package/app/i18n/pl/fields.ts +77 -0
- package/app/i18n/pl/index.ts +12 -0
- package/app/i18n/pl/jobs.ts +36 -0
- package/app/i18n/pl/list.ts +160 -0
- package/app/i18n/pl/record.ts +62 -0
- package/app/i18n/pl/settings.ts +10 -0
- package/app/i18n/pl/shell.ts +54 -0
- package/app/i18n/pt/actions.ts +11 -0
- package/app/i18n/pt/common.ts +23 -0
- package/app/i18n/pt/dashboard.ts +28 -0
- package/app/i18n/pt/fields.ts +77 -0
- package/app/i18n/pt/index.ts +12 -0
- package/app/i18n/pt/jobs.ts +36 -0
- package/app/i18n/pt/list.ts +160 -0
- package/app/i18n/pt/record.ts +62 -0
- package/app/i18n/pt/settings.ts +10 -0
- package/app/i18n/pt/shell.ts +54 -0
- package/app/i18n/pt-BR/actions.ts +11 -0
- package/app/i18n/pt-BR/common.ts +23 -0
- package/app/i18n/pt-BR/dashboard.ts +28 -0
- package/app/i18n/pt-BR/fields.ts +77 -0
- package/app/i18n/pt-BR/index.ts +12 -0
- package/app/i18n/pt-BR/jobs.ts +36 -0
- package/app/i18n/pt-BR/list.ts +160 -0
- package/app/i18n/pt-BR/record.ts +62 -0
- package/app/i18n/pt-BR/settings.ts +10 -0
- package/app/i18n/pt-BR/shell.ts +54 -0
- package/app/i18n/ru/actions.ts +11 -0
- package/app/i18n/ru/common.ts +23 -0
- package/app/i18n/ru/dashboard.ts +28 -0
- package/app/i18n/ru/fields.ts +77 -0
- package/app/i18n/ru/index.ts +12 -0
- package/app/i18n/ru/jobs.ts +36 -0
- package/app/i18n/ru/list.ts +160 -0
- package/app/i18n/ru/record.ts +62 -0
- package/app/i18n/ru/settings.ts +10 -0
- package/app/i18n/ru/shell.ts +54 -0
- package/app/i18n/tr/actions.ts +11 -0
- package/app/i18n/tr/common.ts +23 -0
- package/app/i18n/tr/dashboard.ts +28 -0
- package/app/i18n/tr/fields.ts +77 -0
- package/app/i18n/tr/index.ts +12 -0
- package/app/i18n/tr/jobs.ts +36 -0
- package/app/i18n/tr/list.ts +160 -0
- package/app/i18n/tr/record.ts +62 -0
- package/app/i18n/tr/settings.ts +10 -0
- package/app/i18n/tr/shell.ts +54 -0
- package/app/i18n/uk/actions.ts +11 -0
- package/app/i18n/uk/common.ts +23 -0
- package/app/i18n/uk/dashboard.ts +28 -0
- package/app/i18n/uk/fields.ts +77 -0
- package/app/i18n/uk/index.ts +12 -0
- package/app/i18n/uk/jobs.ts +36 -0
- package/app/i18n/uk/list.ts +160 -0
- package/app/i18n/uk/record.ts +62 -0
- package/app/i18n/uk/settings.ts +10 -0
- package/app/i18n/uk/shell.ts +54 -0
- package/app/i18n/zh-Hans/actions.ts +11 -0
- package/app/i18n/zh-Hans/common.ts +23 -0
- package/app/i18n/zh-Hans/dashboard.ts +28 -0
- package/app/i18n/zh-Hans/fields.ts +77 -0
- package/app/i18n/zh-Hans/index.ts +12 -0
- package/app/i18n/zh-Hans/jobs.ts +36 -0
- package/app/i18n/zh-Hans/list.ts +160 -0
- package/app/i18n/zh-Hans/record.ts +62 -0
- package/app/i18n/zh-Hans/settings.ts +10 -0
- package/app/i18n/zh-Hans/shell.ts +54 -0
- package/app/layouts/default.vue +8 -0
- package/app/layouts/vector.vue +238 -0
- package/app/middleware/vector-auth.ts +16 -0
- package/app/pages/d/[name].vue +12 -0
- package/app/pages/index.vue +22 -0
- package/app/pages/jobs/[id].vue +52 -0
- package/app/pages/jobs/index.vue +42 -0
- package/app/pages/login.vue +109 -0
- package/app/pages/p/[name].vue +37 -0
- package/app/pages/r/[resource]/[id]/edit.vue +54 -0
- package/app/pages/r/[resource]/[id]/index.vue +143 -0
- package/app/pages/r/[resource]/create.vue +53 -0
- package/app/pages/r/[resource]/index.vue +29 -0
- package/app/pages/settings.vue +104 -0
- package/app/plugins/vector-csp.client.ts +14 -0
- package/app/plugins/vector-extensions.client.ts +84 -0
- package/app/plugins/vector-i18n.ts +76 -0
- package/app/types/vector.ts +438 -0
- package/app/utils/files.ts +14 -0
- package/app/utils/format.ts +100 -0
- package/app/utils/richtext.ts +305 -0
- package/app/utils/rules.ts +113 -0
- package/app/utils/translate.ts +45 -0
- package/app/utils/widgets.ts +11 -0
- package/nuxt.config.ts +83 -0
- package/package.json +82 -5
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type { AnyExtension, Extensions, JSONContent } from '@tiptap/core'
|
|
2
|
+
import { generateHTML, generateJSON, generateText } from '@tiptap/core'
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
+
import { TableKit } from '@tiptap/extension-table'
|
|
5
|
+
import Image from '@tiptap/extension-image'
|
|
6
|
+
import Placeholder from '@tiptap/extension-placeholder'
|
|
7
|
+
import Mention, { type MentionOptions } from '@tiptap/extension-mention'
|
|
8
|
+
import DOMPurify from 'dompurify'
|
|
9
|
+
import MarkdownIt from 'markdown-it'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The rich-text schema on the client: the Tiptap extensions a field's
|
|
13
|
+
* `features` allow — the same node and mark types, with the same
|
|
14
|
+
* attributes, the server's Document validates — and the sanitizer every
|
|
15
|
+
* generated HTML string passes before the one `v-html` that shows it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Every feature a field may allow, as the server names them. */
|
|
19
|
+
export const RICH_TEXT_FEATURES = ['bold', 'italic', 'underline', 'strike', 'code', 'heading', 'list', 'blockquote', 'codeBlock', 'horizontalRule', 'link', 'table', 'image', 'mention'] as const
|
|
20
|
+
|
|
21
|
+
/** Links go to http(s), mailto or a relative place — the server's rule. */
|
|
22
|
+
export function allowedHref(href: string): boolean {
|
|
23
|
+
// Control characters and spaces are what hide a scheme, so they are stripped on purpose.
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
const compact = href.replace(/[\u0000- \u007F]+/g, '')
|
|
26
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(compact)
|
|
27
|
+
return scheme === null || ['http', 'https', 'mailto'].includes(scheme[1]!.toLowerCase())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RichTextOptions {
|
|
31
|
+
features: readonly string[]
|
|
32
|
+
placeholder?: string
|
|
33
|
+
/** Resolves an image's storage key to a URL it can show (a blob URL); editing only. */
|
|
34
|
+
imageSource?: (key: string) => Promise<string | null>
|
|
35
|
+
/** The `@` menu: records of the mentioned relation matching a query. */
|
|
36
|
+
mentionItems?: (query: string) => Promise<{ id: string | number, label: string }[]>
|
|
37
|
+
/** Node and mark types runtime extensions added, by name. */
|
|
38
|
+
custom?: ReadonlyMap<string, AnyExtension>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The Tiptap extensions for a field: exactly its features, nothing else. */
|
|
42
|
+
export function richTextExtensions(options: RichTextOptions): Extensions {
|
|
43
|
+
const on = (feature: string) => options.features.includes(feature)
|
|
44
|
+
const extensions: Extensions = [
|
|
45
|
+
StarterKit.configure({
|
|
46
|
+
bold: on('bold') ? {} : false,
|
|
47
|
+
italic: on('italic') ? {} : false,
|
|
48
|
+
underline: on('underline') ? {} : false,
|
|
49
|
+
strike: on('strike') ? {} : false,
|
|
50
|
+
code: on('code') ? {} : false,
|
|
51
|
+
heading: on('heading') ? { levels: [1, 2, 3, 4] } : false,
|
|
52
|
+
bulletList: on('list') ? {} : false,
|
|
53
|
+
orderedList: on('list') ? {} : false,
|
|
54
|
+
listItem: on('list') ? {} : false,
|
|
55
|
+
listKeymap: on('list') ? {} : false,
|
|
56
|
+
blockquote: on('blockquote') ? {} : false,
|
|
57
|
+
codeBlock: on('codeBlock') ? {} : false,
|
|
58
|
+
horizontalRule: on('horizontalRule') ? {} : false,
|
|
59
|
+
link: on('link')
|
|
60
|
+
? {
|
|
61
|
+
openOnClick: false,
|
|
62
|
+
autolink: true,
|
|
63
|
+
protocols: [],
|
|
64
|
+
HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer nofollow' },
|
|
65
|
+
isAllowedUri: (url: string) => allowedHref(url),
|
|
66
|
+
}
|
|
67
|
+
: false,
|
|
68
|
+
}),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if (on('table')) {
|
|
72
|
+
// not resizable: resizing writes style attributes a strict CSP refuses
|
|
73
|
+
extensions.push(TableKit.configure({ table: { resizable: false } }))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (on('image')) {
|
|
77
|
+
extensions.push(vectorImage(options.imageSource))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (on('mention')) {
|
|
81
|
+
extensions.push(Mention.configure({
|
|
82
|
+
HTMLAttributes: { class: 'vx-mention' },
|
|
83
|
+
suggestion: options.mentionItems ? mentionSuggestion(options.mentionItems) : undefined,
|
|
84
|
+
}))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options.placeholder) {
|
|
88
|
+
extensions.push(Placeholder.configure({ placeholder: options.placeholder }))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [name, extension] of options.custom ?? []) {
|
|
92
|
+
if (on(name)) extensions.push(extension)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return extensions
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An image stores a storage key in `src`, never a URL. Rendered, it
|
|
100
|
+
* carries the key as `data-vector-key` and no `src`: whoever shows it
|
|
101
|
+
* fetches the file with the API's credentials. Editing, a node view does
|
|
102
|
+
* that through `imageSource`.
|
|
103
|
+
*/
|
|
104
|
+
function vectorImage(imageSource?: (key: string) => Promise<string | null>): AnyExtension {
|
|
105
|
+
return Image.extend({
|
|
106
|
+
renderHTML({ HTMLAttributes }) {
|
|
107
|
+
const { src, ...rest } = HTMLAttributes as Record<string, unknown>
|
|
108
|
+
return ['img', { ...rest, 'data-vector-key': src }]
|
|
109
|
+
},
|
|
110
|
+
parseHTML() {
|
|
111
|
+
return [{ tag: 'img[src]' }, { tag: 'img[data-vector-key]', getAttrs: element => ({ src: (element as HTMLElement).getAttribute('data-vector-key') }) }]
|
|
112
|
+
},
|
|
113
|
+
addNodeView() {
|
|
114
|
+
return ({ node }) => {
|
|
115
|
+
const img = document.createElement('img')
|
|
116
|
+
img.alt = String(node.attrs.alt ?? '')
|
|
117
|
+
img.className = 'vx-richtext-image'
|
|
118
|
+
const key = String(node.attrs.src ?? '')
|
|
119
|
+
img.setAttribute('data-vector-key', key)
|
|
120
|
+
imageSource?.(key).then((url) => {
|
|
121
|
+
if (url) img.src = url
|
|
122
|
+
}).catch(() => undefined)
|
|
123
|
+
return { dom: img }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}).configure({ allowBase64: false })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A minimal `@` menu: a list under the caret, arrow keys and Enter to pick. */
|
|
130
|
+
function mentionSuggestion(items: (query: string) => Promise<{ id: string | number, label: string }[]>): NonNullable<MentionOptions['suggestion']> {
|
|
131
|
+
return {
|
|
132
|
+
items: ({ query }) => items(query),
|
|
133
|
+
render: () => {
|
|
134
|
+
let menu: HTMLDivElement | null = null
|
|
135
|
+
let choices: { id: string | number, label: string }[] = []
|
|
136
|
+
let active = 0
|
|
137
|
+
let pick: ((item: { id: string | number, label: string }) => void) | null = null
|
|
138
|
+
|
|
139
|
+
const draw = () => {
|
|
140
|
+
if (!menu) return
|
|
141
|
+
menu.replaceChildren(...choices.map((choice, index) => {
|
|
142
|
+
const button = document.createElement('button')
|
|
143
|
+
button.type = 'button'
|
|
144
|
+
button.className = `vx-mention-choice${index === active ? ' vx-mention-choice--active' : ''}`
|
|
145
|
+
button.textContent = choice.label
|
|
146
|
+
button.addEventListener('mousedown', (event) => {
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
pick?.(choice)
|
|
149
|
+
})
|
|
150
|
+
return button
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
const place = (rect: DOMRect | null) => {
|
|
154
|
+
if (!menu || !rect) return
|
|
155
|
+
menu.style.left = `${rect.left + window.scrollX}px`
|
|
156
|
+
menu.style.top = `${rect.bottom + window.scrollY + 4}px`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
onStart: (props) => {
|
|
161
|
+
menu = document.createElement('div')
|
|
162
|
+
menu.className = 'vx-mention-menu'
|
|
163
|
+
menu.setAttribute('role', 'listbox')
|
|
164
|
+
document.body.appendChild(menu)
|
|
165
|
+
choices = props.items as typeof choices
|
|
166
|
+
pick = item => props.command({ id: String(item.id), label: item.label })
|
|
167
|
+
active = 0
|
|
168
|
+
draw()
|
|
169
|
+
place(props.clientRect?.() ?? null)
|
|
170
|
+
},
|
|
171
|
+
onUpdate: (props) => {
|
|
172
|
+
choices = props.items as typeof choices
|
|
173
|
+
pick = item => props.command({ id: String(item.id), label: item.label })
|
|
174
|
+
active = Math.min(active, Math.max(0, choices.length - 1))
|
|
175
|
+
draw()
|
|
176
|
+
place(props.clientRect?.() ?? null)
|
|
177
|
+
},
|
|
178
|
+
onKeyDown: ({ event }) => {
|
|
179
|
+
if (!choices.length) return false
|
|
180
|
+
if (event.key === 'ArrowDown') active = (active + 1) % choices.length
|
|
181
|
+
else if (event.key === 'ArrowUp') active = (active - 1 + choices.length) % choices.length
|
|
182
|
+
else if (event.key === 'Enter') {
|
|
183
|
+
pick?.(choices[active]!)
|
|
184
|
+
return true
|
|
185
|
+
} else return false
|
|
186
|
+
draw()
|
|
187
|
+
return true
|
|
188
|
+
},
|
|
189
|
+
onExit: () => {
|
|
190
|
+
menu?.remove()
|
|
191
|
+
menu = null
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A stored value as a document: JSON, or HTML (a field that stores HTML) read through the schema. */
|
|
199
|
+
export function toDocument(value: string, extensions: Extensions): JSONContent | null {
|
|
200
|
+
if (value.trim() === '') return null
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(value) as unknown
|
|
203
|
+
if (parsed && typeof parsed === 'object' && (parsed as JSONContent).type === 'doc') return parsed as JSONContent
|
|
204
|
+
} catch {
|
|
205
|
+
// not JSON: HTML
|
|
206
|
+
}
|
|
207
|
+
return generateJSON(value, extensions) as JSONContent
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether a document holds no text, image, table or other content. */
|
|
211
|
+
export function isEmptyDocument(doc: JSONContent | null): boolean {
|
|
212
|
+
const empty = (node: JSONContent): boolean => {
|
|
213
|
+
if (node.type === 'text') return (node.text ?? '').trim() === ''
|
|
214
|
+
if (!['doc', 'paragraph', 'hardBreak'].includes(node.type ?? '')) return false
|
|
215
|
+
return (node.content ?? []).every(empty)
|
|
216
|
+
}
|
|
217
|
+
return doc === null || empty(doc)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let purifier: ReturnType<typeof DOMPurify> | null = null
|
|
221
|
+
let purifierWorks: boolean | null = null
|
|
222
|
+
|
|
223
|
+
const SANITIZE_CONFIG = {
|
|
224
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'hr', 'a', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'colgroup', 'col', 'img', 'span'],
|
|
225
|
+
ALLOWED_ATTR: ['href', 'target', 'rel', 'colspan', 'rowspan', 'alt', 'title', 'width', 'height', 'start', 'data-vector-key', 'data-type', 'data-id', 'data-label'],
|
|
226
|
+
ALLOW_DATA_ATTR: false,
|
|
227
|
+
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Text shown as text: what a sanitizer that cannot be trusted leaves. */
|
|
231
|
+
function escapeHtml(html: string): string {
|
|
232
|
+
return html.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' })[c]!)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The allow-list the generated HTML passes before `v-html`: the schema's
|
|
237
|
+
* elements and attributes only. No `style`, no event handler, no
|
|
238
|
+
* `class`, no `src` (images carry their key), links only to http(s),
|
|
239
|
+
* mailto or a relative place, and always opening apart.
|
|
240
|
+
*/
|
|
241
|
+
export function sanitizeHtml(html: string): string {
|
|
242
|
+
if (!purifier) {
|
|
243
|
+
purifier = DOMPurify(window)
|
|
244
|
+
purifier.addHook('afterSanitizeAttributes', (node) => {
|
|
245
|
+
if (node.tagName === 'A') {
|
|
246
|
+
if (!allowedHref(node.getAttribute('href') ?? '')) node.removeAttribute('href')
|
|
247
|
+
node.setAttribute('target', '_blank')
|
|
248
|
+
node.setAttribute('rel', 'noopener noreferrer nofollow')
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fail closed: a sanitizer that does not clean a known-bad canary exactly
|
|
254
|
+
// (a DOM that iterates wrongly, a patched library) is not trusted, and
|
|
255
|
+
// everything is shown as text instead.
|
|
256
|
+
purifierWorks ??= purifier.sanitize('<p onclick="x">a<script>b</script><img src="x" onerror="y"><a href="javascript:z">c</a></p>', SANITIZE_CONFIG)
|
|
257
|
+
=== '<p>a<img><a target="_blank" rel="noopener noreferrer nofollow">c</a></p>'
|
|
258
|
+
|
|
259
|
+
return purifierWorks ? purifier.sanitize(html, SANITIZE_CONFIG) : escapeHtml(html)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Whether this DOM's sanitizer passed its canary: false means every rich value is shown as text. */
|
|
263
|
+
export function sanitizerWorks(): boolean {
|
|
264
|
+
sanitizeHtml('')
|
|
265
|
+
return purifierWorks === true
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A stored value as sanitized HTML, generated through the schema. */
|
|
269
|
+
export function richTextHtml(value: string, extensions: Extensions): string {
|
|
270
|
+
try {
|
|
271
|
+
const doc = toDocument(value, extensions)
|
|
272
|
+
return doc ? sanitizeHtml(generateHTML(doc, extensions)) : ''
|
|
273
|
+
} catch {
|
|
274
|
+
return '' // a node the schema does not know: the server refuses such a document, so it is not shown either
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A stored value's plain text, for a list cell. */
|
|
279
|
+
export function richTextPlain(value: string, extensions: Extensions): string {
|
|
280
|
+
try {
|
|
281
|
+
const doc = toDocument(value, extensions)
|
|
282
|
+
return doc ? generateText(doc, extensions, { blockSeparator: ' ' }).trim() : ''
|
|
283
|
+
} catch {
|
|
284
|
+
return ''
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let markdown: ReturnType<typeof MarkdownIt> | null = null
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Markdown source as sanitized HTML: markdown-it with HTML in the source
|
|
292
|
+
* disabled (it shows as text) and bare URLs linked, then the same
|
|
293
|
+
* allow-list as rich text.
|
|
294
|
+
*/
|
|
295
|
+
export function markdownHtml(source: string): string {
|
|
296
|
+
markdown ??= new MarkdownIt({ html: false, linkify: true, breaks: false })
|
|
297
|
+
return sanitizeHtml(markdown.render(source))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Markdown source as its plain text, for a list cell. */
|
|
301
|
+
export function markdownPlain(source: string): string {
|
|
302
|
+
const holder = document.createElement('div')
|
|
303
|
+
holder.innerHTML = markdownHtml(source)
|
|
304
|
+
return (holder.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
305
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { VectorCondition, VectorField, VectorFieldState, VectorRule } from '#vector/app/types/vector'
|
|
2
|
+
|
|
3
|
+
type Rule = (value: unknown) => true | string
|
|
4
|
+
|
|
5
|
+
const empty = (value: unknown) => value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The descriptor's rules, compiled to Vuetify rule functions. Each
|
|
9
|
+
* mirrors the framework constraint the server runs, by its class's
|
|
10
|
+
* short name; a constraint with no client equivalent is left to the
|
|
11
|
+
* server, whose 422 lands on the field as `error-messages`.
|
|
12
|
+
*/
|
|
13
|
+
export function rulesFor(field: VectorField, operation: 'create' | 'update', state?: VectorFieldState): Rule[] {
|
|
14
|
+
const rules: Rule[] = []
|
|
15
|
+
const requiredHere = (operation === 'create' && field.nullable === false && !['manyToMany', 'hasMany', 'hasOne', 'boolean'].includes(field.type)) || state?.required === true
|
|
16
|
+
|
|
17
|
+
if (requiredHere) {
|
|
18
|
+
rules.push(value => !empty(value) || vt('common.required'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const rule of field.rules) {
|
|
22
|
+
const compiled = compile(rule)
|
|
23
|
+
|
|
24
|
+
if (compiled) {
|
|
25
|
+
rules.push(compiled)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return rules
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compile(rule: VectorRule): Rule | null {
|
|
33
|
+
const name = rule.constraint.split('\\').pop()
|
|
34
|
+
const [a, b] = rule.args as [unknown, unknown]
|
|
35
|
+
|
|
36
|
+
switch (name) {
|
|
37
|
+
case 'NotBlank':
|
|
38
|
+
return value => (typeof value === 'string' ? value.trim() !== '' : !empty(value)) || vt('common.required')
|
|
39
|
+
case 'MaxLength':
|
|
40
|
+
return value => empty(value) || typeof value !== 'string' || [...value].length <= Number(a) || vt('fields.rules.maxLength', { max: String(a) })
|
|
41
|
+
case 'MinLength':
|
|
42
|
+
return value => empty(value) || typeof value !== 'string' || [...value].length >= Number(a) || vt('fields.rules.minLength', { min: String(a) })
|
|
43
|
+
case 'Email':
|
|
44
|
+
return value => empty(value) || (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) || vt('fields.rules.email')
|
|
45
|
+
case 'Url':
|
|
46
|
+
case 'WebUrl':
|
|
47
|
+
return value => empty(value) || (typeof value === 'string' && /^https?:\/\/\S+$/i.test(value)) || vt('fields.rules.url')
|
|
48
|
+
case 'GreaterThan':
|
|
49
|
+
return value => empty(value) || Number(value) > Number(a) || vt('fields.rules.greaterThan', { limit: String(a) })
|
|
50
|
+
case 'GreaterThanOrEqual':
|
|
51
|
+
return value => empty(value) || Number(value) >= Number(a) || vt('fields.rules.greaterThanOrEqual', { limit: String(a) })
|
|
52
|
+
case 'LessThan':
|
|
53
|
+
return value => empty(value) || Number(value) < Number(a) || vt('fields.rules.lessThan', { limit: String(a) })
|
|
54
|
+
case 'LessThanOrEqual':
|
|
55
|
+
return value => empty(value) || Number(value) <= Number(a) || vt('fields.rules.lessThanOrEqual', { limit: String(a) })
|
|
56
|
+
case 'Regex':
|
|
57
|
+
return value => empty(value) || typeof value !== 'string' || phpRegex(String(a)).test(value) || (typeof b === 'string' ? b : vt('fields.rules.invalidFormat'))
|
|
58
|
+
case 'In':
|
|
59
|
+
return value => empty(value) || (Array.isArray(a) && a.includes(value)) || vt('fields.rules.notAllowed')
|
|
60
|
+
default:
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A PCRE pattern with its delimiters and flags, as the framework's Regex constraint takes it, as a JavaScript RegExp. */
|
|
66
|
+
function phpRegex(pattern: string): RegExp {
|
|
67
|
+
const match = pattern.match(/^(.)([\s\S]*)\1([a-zA-Z]*)$/)
|
|
68
|
+
|
|
69
|
+
if (!match) return new RegExp(pattern)
|
|
70
|
+
|
|
71
|
+
const flags = [...match[3]!].filter(f => 'gimsuy'.includes(f)).join('')
|
|
72
|
+
|
|
73
|
+
return new RegExp(match[2]!, flags)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* What a field's conditions decide under the form's current values: the
|
|
78
|
+
* server evaluates the same conditions the same way, comparing a
|
|
79
|
+
* relation by its id, a boolean as true/false, anything else as text.
|
|
80
|
+
*/
|
|
81
|
+
export function fieldState(field: VectorField, values: Record<string, unknown>): VectorFieldState {
|
|
82
|
+
const c = field.conditions
|
|
83
|
+
if (!c) return { visible: true, required: false, disabled: false }
|
|
84
|
+
return {
|
|
85
|
+
visible: holds(c.visible, values),
|
|
86
|
+
required: c.required.length > 0 && holds(c.required, values),
|
|
87
|
+
disabled: c.disabled.length > 0 && holds(c.disabled, values),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function holds(conditions: VectorCondition[], values: Record<string, unknown>): boolean {
|
|
92
|
+
return conditions.every((condition) => {
|
|
93
|
+
const actual = normalize(values[condition.field])
|
|
94
|
+
const list = Array.isArray(condition.value) ? condition.value.map(normalize) : [normalize(condition.value)]
|
|
95
|
+
switch (condition.op) {
|
|
96
|
+
case 'eq': return actual === normalize(condition.value)
|
|
97
|
+
case 'ne': return actual !== normalize(condition.value)
|
|
98
|
+
case 'in': return list.includes(actual)
|
|
99
|
+
case 'nin': return !list.includes(actual)
|
|
100
|
+
case 'null': return actual === null
|
|
101
|
+
case 'nnull': return actual !== null
|
|
102
|
+
default: return false
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalize(value: unknown): string | null {
|
|
108
|
+
if (value && typeof value === 'object' && 'id' in value) value = (value as { id: unknown }).id
|
|
109
|
+
if (value === null || value === undefined || value === '') return null
|
|
110
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
111
|
+
if (typeof value === 'number' || typeof value === 'string') return String(value)
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { I18n } from 'vue-i18n'
|
|
2
|
+
|
|
3
|
+
// The admin's translator, set once by the vector-i18n plugin. The UI runs in
|
|
4
|
+
// the browser only (ssr: false), one app per page, so a module-level instance
|
|
5
|
+
// is safe and lets utilities, stores and async handlers translate outside a
|
|
6
|
+
// component's setup.
|
|
7
|
+
let instance: VectorI18n | null = null
|
|
8
|
+
|
|
9
|
+
/** The admin's vue-i18n instance, in composition mode: what the plugin provides as `$vectorI18n`. */
|
|
10
|
+
export type VectorI18n = I18n<Record<string, unknown>, Record<string, unknown>, Record<string, unknown>, string, false>
|
|
11
|
+
|
|
12
|
+
export function setVectorI18n(i18n: VectorI18n): void {
|
|
13
|
+
instance = i18n
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Translates one of the admin's own strings: `vt('list.selected', { count }, count)`.
|
|
18
|
+
* `{name}` interpolates; `a | b | c` pluralizes on the count passed last.
|
|
19
|
+
* Before the plugin runs (never in practice) the key itself comes back.
|
|
20
|
+
*/
|
|
21
|
+
export function vt(key: string, params: Record<string, unknown> = {}, plural?: number): string {
|
|
22
|
+
if (!instance) return key
|
|
23
|
+
return plural === undefined ? instance.global.t(key, params) : instance.global.t(key, params, plural)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A resource label as it reads inside a sentence (`Create product`,
|
|
28
|
+
* `Delete 3 products?`): lower-cased in the current language, except
|
|
29
|
+
* where nouns keep their capital — German writes every noun with one.
|
|
30
|
+
*/
|
|
31
|
+
export function vnoun(label: string): string {
|
|
32
|
+
const locale = instance ? String(instance.global.locale.value) : 'en'
|
|
33
|
+
return locale === 'de' || locale.startsWith('de-') ? label : label.toLocaleLowerCase(locale)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// `$vectorI18n` is declared here rather than left to Nuxt's generated plugin
|
|
37
|
+
// types: NuxtApp inherits those from the type of every plugin module, a layer's
|
|
38
|
+
// own plugins among them, and resolving that is circular — it leaves
|
|
39
|
+
// `useNuxtApp().$vectorI18n` unknown. Kept out of the plugin module, which is one
|
|
40
|
+
// of those bases itself.
|
|
41
|
+
declare module '#app' {
|
|
42
|
+
interface NuxtApp {
|
|
43
|
+
$vectorI18n: VectorI18n
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Formats a widget value by its declared format: number, money:CUR, percent, duration. */
|
|
2
|
+
export function formatWidgetValue(value: number | null | undefined, format: string | undefined): string {
|
|
3
|
+
if (value === null || value === undefined) return vt('common.none')
|
|
4
|
+
if (format?.startsWith('money:')) return formatMoney(Math.round(value), format.slice(6))
|
|
5
|
+
if (format === 'percent') return vt('dashboard.percent', { value: formatNumber(value, 1) })
|
|
6
|
+
if (format === 'duration') {
|
|
7
|
+
const minutes = Math.round(value / 60)
|
|
8
|
+
return minutes < 60 ? vt('dashboard.duration.minutes', { value: minutes }) : vt('dashboard.duration.hours', { value: formatNumber(minutes / 60, 1) })
|
|
9
|
+
}
|
|
10
|
+
return formatNumber(value, Number.isInteger(value) ? undefined : 2)
|
|
11
|
+
}
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
// Paths inside a layer's config resolve against the consuming app, so
|
|
4
|
+
// anything that names a file in this package goes through this alias.
|
|
5
|
+
const layerDir = fileURLToPath(new URL('./', import.meta.url))
|
|
6
|
+
|
|
7
|
+
export default defineNuxtConfig({
|
|
8
|
+
$meta: {
|
|
9
|
+
name: 'vector',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
modules: ['vuetify-nuxt-module', '@pinia/nuxt', '@vueuse/nuxt'],
|
|
13
|
+
|
|
14
|
+
// A back-office application: rendered in the browser only, hostable
|
|
15
|
+
// as static files (standalone) or from the PHP application (hosted).
|
|
16
|
+
ssr: false,
|
|
17
|
+
|
|
18
|
+
runtimeConfig: {
|
|
19
|
+
public: {
|
|
20
|
+
vector: {
|
|
21
|
+
// Overridden by NUXT_PUBLIC_VECTOR_API_BASE.
|
|
22
|
+
apiBase: 'http://localhost:8080',
|
|
23
|
+
// Runtime UI extensions (ES-module URLs) in standalone mode; hosted mode reads VECTOR_UI_EXTENSIONS.
|
|
24
|
+
extensions: [] as string[],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
vuetify: {
|
|
30
|
+
moduleOptions: {
|
|
31
|
+
prefixComposables: true,
|
|
32
|
+
},
|
|
33
|
+
vuetifyOptions: {
|
|
34
|
+
theme: {
|
|
35
|
+
defaultTheme: 'system',
|
|
36
|
+
themes: {
|
|
37
|
+
// medium-emphasis text at 0.74 (Vuetify's 0.6 default) keeps subtitles and hints above WCAG AA contrast
|
|
38
|
+
light: {
|
|
39
|
+
dark: false,
|
|
40
|
+
colors: {
|
|
41
|
+
primary: '#1F5F8B',
|
|
42
|
+
secondary: '#546E7A',
|
|
43
|
+
// Status colours dark enough for white text at WCAG AA (4.5:1): Vuetify's
|
|
44
|
+
// defaults (#4CAF50, #FB8C00, #2196F3) fall below it on toasts and chips.
|
|
45
|
+
success: '#2E7D32',
|
|
46
|
+
warning: '#B45309',
|
|
47
|
+
info: '#1565C0',
|
|
48
|
+
error: '#C62828',
|
|
49
|
+
},
|
|
50
|
+
variables: { 'medium-emphasis-opacity': 0.74 },
|
|
51
|
+
},
|
|
52
|
+
dark: {
|
|
53
|
+
dark: true,
|
|
54
|
+
colors: {
|
|
55
|
+
primary: '#5AA9E6',
|
|
56
|
+
secondary: '#90A4AE',
|
|
57
|
+
},
|
|
58
|
+
variables: { 'medium-emphasis-opacity': 0.8 },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
// The house style every generated view shares; a consuming app
|
|
63
|
+
// overrides any entry in its own nuxt.config.
|
|
64
|
+
defaults: {
|
|
65
|
+
VTextField: { variant: 'outlined', density: 'comfortable' },
|
|
66
|
+
VTextarea: { variant: 'outlined', density: 'comfortable' },
|
|
67
|
+
VSelect: { variant: 'outlined', density: 'comfortable' },
|
|
68
|
+
VAutocomplete: { variant: 'outlined', density: 'comfortable' },
|
|
69
|
+
VNumberInput: { variant: 'outlined', density: 'comfortable' },
|
|
70
|
+
VBtn: { variant: 'flat' },
|
|
71
|
+
VCard: { variant: 'flat', border: true },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
typescript: {
|
|
77
|
+
strict: true,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
alias: {
|
|
81
|
+
'#vector': layerDir,
|
|
82
|
+
},
|
|
83
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@kinetis/vector-ui",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "The Kinetis Vector admin UI, as a Nuxt layer on Vuetify.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kinetis",
|
|
7
|
+
"vector",
|
|
8
|
+
"admin",
|
|
9
|
+
"nuxt",
|
|
10
|
+
"nuxt-layer",
|
|
11
|
+
"vuetify"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://kinetis.dev/vector/docs/",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/kinetis-dev/vector-dev/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Alon Noy",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kinetis-dev/vector-dev.git",
|
|
22
|
+
"directory": "packages/vector-ui"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./nuxt.config.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"nuxt.config.ts",
|
|
28
|
+
"app",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"provenance": true
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prepare": "nuxt prepare",
|
|
39
|
+
"typecheck": "nuxt prepare && vue-tsc --noEmit",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"test": "vitest run --passWithNoTests"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@mdi/font": "^7.4.47",
|
|
45
|
+
"@pinia/nuxt": "^1.0.2",
|
|
46
|
+
"@tiptap/core": "^3.31.3",
|
|
47
|
+
"@tiptap/extension-image": "^3.31.3",
|
|
48
|
+
"@tiptap/extension-link": "^3.31.3",
|
|
49
|
+
"@tiptap/extension-mention": "^3.31.3",
|
|
50
|
+
"@tiptap/extension-placeholder": "^3.31.3",
|
|
51
|
+
"@tiptap/extension-table": "^3.31.3",
|
|
52
|
+
"@tiptap/markdown": "^3.31.3",
|
|
53
|
+
"@tiptap/pm": "^3.31.3",
|
|
54
|
+
"@tiptap/starter-kit": "^3.31.3",
|
|
55
|
+
"@tiptap/vue-3": "^3.31.3",
|
|
56
|
+
"@vueuse/core": "^15.0.0",
|
|
57
|
+
"@vueuse/nuxt": "^15.0.0",
|
|
58
|
+
"dompurify": "^3.4.16",
|
|
59
|
+
"markdown-it": "^15.0.2",
|
|
60
|
+
"pinia": "^4.0.3",
|
|
61
|
+
"vue-i18n": "^11.4.12"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"nuxt": "^4.5.0",
|
|
65
|
+
"vue": "^3.5.0",
|
|
66
|
+
"vuetify": "^4.2.0",
|
|
67
|
+
"vuetify-nuxt-module": "^1.0.0-rc.6"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@nuxt/test-utils": "^4.3.0",
|
|
71
|
+
"@types/node": "^22.0.0",
|
|
72
|
+
"@vue/test-utils": "^2.5.1",
|
|
73
|
+
"happy-dom": "^20.14.5",
|
|
74
|
+
"nuxt": "^4.5.2",
|
|
75
|
+
"sass-embedded": "^1.90.0",
|
|
76
|
+
"typescript": "~6.0.3",
|
|
77
|
+
"vitest": "^4.0.2",
|
|
78
|
+
"vue": "^3.5.0",
|
|
79
|
+
"vue-tsc": "^3.0.0",
|
|
80
|
+
"vuetify": "^4.2.2",
|
|
81
|
+
"vuetify-nuxt-module": "^1.0.0-rc.6"
|
|
82
|
+
}
|
|
83
|
+
}
|