@naptics/vue-collection 0.2.15 → 0.3.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 (171) hide show
  1. package/.github/workflows/build.yml +26 -0
  2. package/.github/workflows/deploy-demo.yml +46 -0
  3. package/.github/workflows/deploy-lib.yml +59 -0
  4. package/.gitlab-ci.yml +57 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierrc +8 -0
  7. package/.vscode/extensions.json +10 -0
  8. package/.vscode/launch.json +23 -0
  9. package/.vscode/settings.json +13 -0
  10. package/babel.config.json +3 -0
  11. package/components/NAlert.d.ts +1 -44
  12. package/components/NBadge.d.ts +1 -133
  13. package/components/NBreadcrub.d.ts +2 -106
  14. package/components/NBreadcrub.js +1 -1
  15. package/components/NButton.d.ts +2 -118
  16. package/components/NCheckbox.d.ts +1 -32
  17. package/components/NCheckboxLabel.d.ts +1 -45
  18. package/components/NCheckboxLabel.js +1 -1
  19. package/components/NCrudModal.d.ts +7 -251
  20. package/components/NCrudModal.js +1 -1
  21. package/components/NDialog.d.ts +1 -110
  22. package/components/NDialog.js +1 -1
  23. package/components/NDropdown.d.ts +1 -69
  24. package/components/NDropdown.js +1 -1
  25. package/components/NDropzone.d.ts +1 -115
  26. package/components/NDropzone.js +1 -1
  27. package/components/NForm.d.ts +1 -23
  28. package/components/NFormModal.d.ts +7 -151
  29. package/components/NIconButton.d.ts +3 -159
  30. package/components/NIconButton.js +1 -1
  31. package/components/NIconCircle.d.ts +1 -87
  32. package/components/NInput.d.ts +1 -164
  33. package/components/NInput.js +1 -1
  34. package/components/NInputPhone.d.ts +2 -114
  35. package/components/NInputPhone.js +1 -1
  36. package/components/NInputSelect.d.ts +2 -187
  37. package/components/NInputSelect.js +1 -1
  38. package/components/NInputSuggestion.d.ts +2 -155
  39. package/components/NInputSuggestion.js +1 -1
  40. package/components/NLink.d.ts +6 -70
  41. package/components/NLink.js +8 -1
  42. package/components/NList.d.ts +1 -43
  43. package/components/NList.js +1 -1
  44. package/components/NLoadingIndicator.d.ts +1 -49
  45. package/components/NModal.d.ts +12 -250
  46. package/components/NModal.js +15 -9
  47. package/components/NPagination.d.ts +1 -63
  48. package/components/NSearchbar.d.ts +1 -56
  49. package/components/NSearchbarList.d.ts +3 -63
  50. package/components/NSearchbarList.js +1 -1
  51. package/components/NSelect.d.ts +2 -148
  52. package/components/NSelect.js +1 -1
  53. package/components/NSuggestionList.d.ts +3 -126
  54. package/components/NSuggestionList.js +5 -2
  55. package/components/NTable.d.ts +1 -85
  56. package/components/NTable.js +12 -6
  57. package/components/NTableAction.d.ts +2 -46
  58. package/components/NTableAction.js +1 -1
  59. package/components/NTextArea.d.ts +2 -181
  60. package/components/NTextArea.js +1 -1
  61. package/components/NTooltip.d.ts +1 -105
  62. package/components/NTooltip.js +1 -1
  63. package/components/NValInput.d.ts +7 -182
  64. package/components/NValInput.js +1 -1
  65. package/env.d.ts +15 -0
  66. package/eslint.config.cjs +29 -0
  67. package/index.html +13 -0
  68. package/package.json +21 -19
  69. package/postcss.config.js +6 -0
  70. package/public/favicon.ico +0 -0
  71. package/scripts/build-lib.sh +52 -0
  72. package/scripts/sync-node-types.js +70 -0
  73. package/src/demo/App.css +9 -0
  74. package/src/demo/App.tsx +5 -0
  75. package/src/demo/components/ColorGrid.tsx +26 -0
  76. package/src/demo/components/ComponentGrid.tsx +26 -0
  77. package/src/demo/components/ComponentSection.tsx +30 -0
  78. package/src/demo/components/VariantSection.tsx +18 -0
  79. package/src/demo/i18n/de.ts +7 -0
  80. package/src/demo/i18n/en.ts +7 -0
  81. package/src/demo/i18n/index.ts +24 -0
  82. package/src/demo/main.ts +13 -0
  83. package/src/demo/router/index.ts +21 -0
  84. package/src/demo/views/HomeView.tsx +94 -0
  85. package/src/demo/views/NavigationView.tsx +43 -0
  86. package/src/demo/views/presentation/AlertView.tsx +40 -0
  87. package/src/demo/views/presentation/BadgeView.tsx +61 -0
  88. package/src/demo/views/presentation/BreadcrumbView.tsx +52 -0
  89. package/src/demo/views/presentation/ButtonView.tsx +49 -0
  90. package/src/demo/views/presentation/CheckboxView.tsx +59 -0
  91. package/src/demo/views/presentation/DropdownView.tsx +59 -0
  92. package/src/demo/views/presentation/DropzoneView.tsx +39 -0
  93. package/src/demo/views/presentation/IconButtonView.tsx +47 -0
  94. package/src/demo/views/presentation/IconCircleView.tsx +38 -0
  95. package/src/demo/views/presentation/InputView.tsx +179 -0
  96. package/src/demo/views/presentation/LinkView.tsx +60 -0
  97. package/src/demo/views/presentation/ListView.tsx +29 -0
  98. package/src/demo/views/presentation/LoadingIndicatorView.tsx +38 -0
  99. package/src/demo/views/presentation/ModalView.tsx +210 -0
  100. package/src/demo/views/presentation/PaginationView.tsx +25 -0
  101. package/src/demo/views/presentation/SearchbarView.tsx +80 -0
  102. package/src/demo/views/presentation/TableView.tsx +146 -0
  103. package/src/demo/views/presentation/TooltipView.tsx +86 -0
  104. package/src/lib/components/NAlert.tsx +85 -0
  105. package/src/lib/components/NBadge.tsx +75 -0
  106. package/src/lib/components/NBreadcrub.tsx +97 -0
  107. package/src/lib/components/NButton.tsx +80 -0
  108. package/src/lib/components/NCheckbox.tsx +55 -0
  109. package/src/lib/components/NCheckboxLabel.tsx +51 -0
  110. package/src/lib/components/NCrudModal.tsx +133 -0
  111. package/src/lib/components/NDialog.tsx +182 -0
  112. package/src/lib/components/NDropdown.tsx +167 -0
  113. package/src/lib/components/NDropzone.tsx +265 -0
  114. package/src/lib/components/NForm.tsx +32 -0
  115. package/src/lib/components/NFormModal.tsx +66 -0
  116. package/src/lib/components/NIconButton.tsx +92 -0
  117. package/src/lib/components/NIconCircle.tsx +78 -0
  118. package/src/lib/components/NInput.css +11 -0
  119. package/src/lib/components/NInput.tsx +139 -0
  120. package/src/lib/components/NInputPhone.tsx +53 -0
  121. package/src/lib/components/NInputSelect.tsx +126 -0
  122. package/src/lib/components/NInputSuggestion.tsx +80 -0
  123. package/src/lib/components/NLink.tsx +82 -0
  124. package/src/lib/components/NList.tsx +67 -0
  125. package/src/lib/components/NLoadingIndicator.css +46 -0
  126. package/src/lib/components/NLoadingIndicator.tsx +63 -0
  127. package/src/lib/components/NModal.tsx +243 -0
  128. package/src/lib/components/NPagination.css +15 -0
  129. package/src/lib/components/NPagination.tsx +131 -0
  130. package/src/lib/components/NSearchbar.tsx +78 -0
  131. package/src/lib/components/NSearchbarList.tsx +47 -0
  132. package/src/lib/components/NSelect.tsx +128 -0
  133. package/src/lib/components/NSuggestionList.tsx +216 -0
  134. package/src/lib/components/NTable.css +3 -0
  135. package/src/lib/components/NTable.tsx +247 -0
  136. package/src/lib/components/NTableAction.tsx +49 -0
  137. package/src/lib/components/NTextArea.tsx +159 -0
  138. package/src/lib/components/NTooltip.css +37 -0
  139. package/src/lib/components/NTooltip.tsx +250 -0
  140. package/src/lib/components/NValInput.tsx +163 -0
  141. package/src/lib/components/ValidatedForm.ts +71 -0
  142. package/src/lib/components/__tests__/NButton.spec.tsx +26 -0
  143. package/src/lib/components/__tests__/NCheckbox.spec.tsx +39 -0
  144. package/src/lib/i18n/de/vue-collection.json +58 -0
  145. package/src/lib/i18n/en/vue-collection.json +58 -0
  146. package/src/lib/i18n/index.ts +54 -0
  147. package/src/lib/index.ts +2 -0
  148. package/src/lib/jsx.d.ts +13 -0
  149. package/src/lib/utils/__tests__/identifiable.spec.ts +72 -0
  150. package/src/lib/utils/__tests__/validation.spec.ts +92 -0
  151. package/src/lib/utils/breakpoints.ts +47 -0
  152. package/src/lib/utils/component.tsx +131 -0
  153. package/src/lib/utils/deferred.ts +28 -0
  154. package/src/lib/utils/identifiable.ts +87 -0
  155. package/src/lib/utils/stringMaxLength.ts +25 -0
  156. package/src/lib/utils/tailwind.ts +41 -0
  157. package/src/lib/utils/utils.ts +90 -0
  158. package/src/lib/utils/vModel.ts +260 -0
  159. package/src/lib/utils/validation.ts +189 -0
  160. package/src/lib/utils/vue.ts +25 -0
  161. package/tailwind.config.js +38 -0
  162. package/tsconfig.config.json +9 -0
  163. package/tsconfig.demo.json +19 -0
  164. package/tsconfig.json +16 -0
  165. package/tsconfig.lib.json +18 -0
  166. package/tsconfig.vitest.json +8 -0
  167. package/utils/breakpoints.d.ts +1 -1
  168. package/utils/component.d.ts +3 -7
  169. package/utils/component.js +5 -2
  170. package/utils/identifiable.js +5 -1
  171. package/vite.config.ts +28 -0
@@ -0,0 +1,167 @@
1
+ import { createComponentWithSlots } from '../utils/component'
2
+ import { computed, Transition, type PropType } from 'vue'
3
+ import { RouterLink, type RouteLocationRaw } from 'vue-router'
4
+ import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
5
+ import { ChevronDownIcon } from '@heroicons/vue/24/solid'
6
+ import type { HeroIcon } from '../utils/tailwind'
7
+
8
+ export const nDropdownProps = {
9
+ /**
10
+ * The title of the dropdown-button.
11
+ */
12
+ title: String,
13
+ /**
14
+ * The items of the dropdown.
15
+ * The second dimension of the array is used
16
+ * to create groups of items, which are visually seperated.
17
+ */
18
+ items: {
19
+ type: Array as PropType<DropdownItem[] | DropdownItem[][]>,
20
+ default: () => [],
21
+ },
22
+ /**
23
+ * If set to `true` the panel is right-aligned to the button.
24
+ */
25
+ right: Boolean,
26
+ /**
27
+ * If set to `true` the dropdown-button is disabled and no interaction is possible.
28
+ */
29
+ disabled: Boolean,
30
+ /**
31
+ * Adds the classes to the Button of the dropdown.
32
+ */
33
+ buttonClass: String,
34
+ /**
35
+ * A slot to replace the button of the dropdown.
36
+ * The passed parameter is the HeadlessUI `MenuButton` which should be
37
+ * used to create the button for the Dropdown to work properly.
38
+ */
39
+ button: Function as PropType<(button: typeof MenuButton) => JSX.Element>,
40
+ } as const
41
+
42
+ export type DropdownItem = {
43
+ /**
44
+ * The label of the dropdown-item.
45
+ */
46
+ label: string
47
+ /**
48
+ * The icon of the dropdown-item. Is displayed to the left of the text.
49
+ */
50
+ icon?: HeroIcon
51
+ /**
52
+ * The route of the dropdown-item. If this is set, the dropdown-item is a {@link RouterLink}.
53
+ */
54
+ route?: RouteLocationRaw
55
+ /**
56
+ * If set to `true` the dropdown-item is disabled and no interaction is possible.
57
+ * The other dropdown-items can still be clicked.
58
+ */
59
+ disabled?: boolean
60
+ /**
61
+ * This is called when the dropdown-item is clicked.
62
+ * It is only called when the `route` option is not set on the item.
63
+ */
64
+ onClick?: () => void
65
+ }
66
+
67
+ /**
68
+ * The `NDropdown` consists of a button and a panel with multiple actions.
69
+ * It is useful to group multiple actions together in one place.
70
+ */
71
+ const Component = createComponentWithSlots('NDropdown', nDropdownProps, ['button'], (props, { slots }) => {
72
+ const items = computed<DropdownItem[][]>(() => {
73
+ if (props.items.length == 0) return []
74
+ if (Array.isArray(props.items[0])) return props.items as DropdownItem[][]
75
+ else return [props.items] as DropdownItem[][]
76
+ })
77
+
78
+ const itemWithIcon = (item: DropdownItem) => (
79
+ <div class="flex space-x-3 items-center">
80
+ {item.icon && <item.icon class={['h-5 w-5', item.disabled ? 'text-default-300' : 'text-default-400']} />}
81
+ <span>{item.label}</span>
82
+ </div>
83
+ )
84
+
85
+ return () => (
86
+ <Menu as="div" class={`relative inline-block text-left`}>
87
+ <div class="flex">
88
+ {props.button?.(MenuButton) || (
89
+ <MenuButton
90
+ disabled={props.disabled}
91
+ class={[
92
+ 'shadow w-full flex justify-between items-center text-default-700 rounded-md border bg-white border-default-300 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-offset-2 focus-visible:ring-2 focus-visible:ring-primary-500',
93
+ props.disabled ? 'text-opacity-20 cursor-default' : 'hover:bg-default-100',
94
+ props.buttonClass,
95
+ ]}
96
+ >
97
+ <span>{props.title}</span>
98
+ <ChevronDownIcon class="-mr-1 ml-2 h-5 w-5 flex-shrink-0" aria-hidden="true" />
99
+ </MenuButton>
100
+ )}
101
+ </div>
102
+
103
+ <Transition
104
+ enterActiveClass="transition ease-out duration-100"
105
+ enterFromClass="transform opacity-0 scale-95"
106
+ enterToClass="transform opacity-100 scale-100"
107
+ leaveActiveClass="transition ease-in duration-75"
108
+ leaveFromClass="transform opacity-100 scale-100"
109
+ leaveToClass="transform opacity-0 scale-95"
110
+ >
111
+ <MenuItems
112
+ class={[
113
+ 'z-10 absolute w-56 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
114
+ props.right ? 'origin-top-right right-0' : 'origin-top-left left-0',
115
+ ]}
116
+ >
117
+ {slots.default?.() || (
118
+ <div class="divide-y divide-default-200">
119
+ {items.value.map((group, index) => (
120
+ <div key={`group-${index}`} class="py-1">
121
+ {group.map((item, index) => (
122
+ <MenuItem key={`item-${index}`} disabled={item.disabled}>
123
+ {({ active }: { active: boolean }) =>
124
+ item.disabled ? (
125
+ <div class="block px-4 py-2 text-sm text-default-300">
126
+ {itemWithIcon(item)}
127
+ </div>
128
+ ) : item.route ? (
129
+ <RouterLink
130
+ to={item.route}
131
+ class={[
132
+ 'block px-4 py-2 text-sm',
133
+ active
134
+ ? 'bg-default-100 text-default-900'
135
+ : 'text-default-700',
136
+ ]}
137
+ >
138
+ {itemWithIcon(item)}
139
+ </RouterLink>
140
+ ) : (
141
+ <button
142
+ type="button"
143
+ onClick={item.onClick}
144
+ class={[
145
+ 'w-full text-left px-4 py-2 text-sm',
146
+ active
147
+ ? 'bg-default-100 text-default-900'
148
+ : 'text-default-700',
149
+ ]}
150
+ >
151
+ {itemWithIcon(item)}
152
+ </button>
153
+ )
154
+ }
155
+ </MenuItem>
156
+ ))}
157
+ </div>
158
+ ))}
159
+ </div>
160
+ )}
161
+ </MenuItems>
162
+ </Transition>
163
+ </Menu>
164
+ )
165
+ })
166
+
167
+ export { Component as NDropdown, Component as default }
@@ -0,0 +1,265 @@
1
+ import { trslc } from '../i18n'
2
+ import { createComponent } from '../utils/component'
3
+ import { maxLengthSplitCenter } from '../utils/stringMaxLength'
4
+ import { notNullish } from '../utils/utils'
5
+ import { XMarkIcon } from '@heroicons/vue/24/solid'
6
+ import { computed, ref } from 'vue'
7
+ import NBadge from './NBadge'
8
+ import NIconButton from './NIconButton'
9
+ import NLink from './NLink'
10
+ import { vModelProps } from '../utils/vModel'
11
+
12
+ export const nDropzoneProps = {
13
+ ...vModelProps<File[]>(Array),
14
+ /**
15
+ * A description which files are allowed for this dropzone.
16
+ * This should include everything the user needs to know about
17
+ * the file type / the extensions and the maximum size of the file.
18
+ * @see {@link nDropzoneProps.accept}
19
+ * @see {@link nDropzoneProps.maxFileSize}
20
+ */
21
+ description: String,
22
+ /**
23
+ * The maximum amount of files which can be added to the dropzone.
24
+ */
25
+ maxFiles: {
26
+ type: Number,
27
+ default: 1,
28
+ },
29
+ /**
30
+ * Specifies which file types are accepted. The same syntax as with inputs of type file is used.
31
+ * Make sure to explain the requirements to the user in the {@link nDropzoneProps.description}.
32
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
33
+ */
34
+ accept: String,
35
+ /**
36
+ * Specifies the maximum size of an individual file in bytes.
37
+ * Make sure to explain the max size to the user in the {@link nDropzoneProps.description}.
38
+ */
39
+ maxFileSize: {
40
+ type: Number,
41
+ default: 100 * 1024 * 1024,
42
+ },
43
+ /**
44
+ * The maximum length of the file names.
45
+ * Files names longer than the specified amount of characters will be shortened.
46
+ * @see {@link maxLengthSplitCenter}
47
+ */
48
+ maxLengthFileNames: {
49
+ type: Number,
50
+ default: 35,
51
+ },
52
+ /**
53
+ * A tailwind height class, which is applied to the dropzone area.
54
+ * It is recommended to use `min-h-*` classes,
55
+ * so the box is always large enough to display it's text.
56
+ */
57
+ height: {
58
+ type: String,
59
+ default: 'min-h-36',
60
+ },
61
+ /**
62
+ * If set to `true`, the dropzone is disabled and does not accept input anymore.
63
+ */
64
+ disabled: Boolean,
65
+ } as const
66
+
67
+ /**
68
+ * The `NDropzone` is an area where files can be added by the user by drag & drop.
69
+ * Files can also be selected with a file chooser by clicking on the dropzone.
70
+ */
71
+ const Component = createComponent('NDropzone', nDropzoneProps, props => {
72
+ const fileError = ref<string>()
73
+
74
+ const filterAndUpdateFiles = (files: File[]) => {
75
+ // filter for mime type and max size
76
+ const fileTypeFilteredFiles = files.filter(
77
+ file => !props.accept || testFileWithAcceptString(props.accept, file)
78
+ )
79
+ const filteredFiles = fileTypeFilteredFiles.filter(file => file.size <= props.maxFileSize)
80
+
81
+ // filter for already existing files
82
+ const currentFiles = props.value || []
83
+ filteredFiles.forEach(file => {
84
+ if (currentFiles.filter(currFile => currFile.name == file.name).length == 0) currentFiles.push(file)
85
+ })
86
+
87
+ // slice down to max amount of files
88
+ const newFiles = currentFiles.slice(0, props.maxFiles)
89
+
90
+ // error handling
91
+ const fileTypeFilterDiff = files.length - fileTypeFilteredFiles.length
92
+ const fileSizeFilterDiff = fileTypeFilteredFiles.length - filteredFiles.length
93
+ if (newFiles.length < currentFiles.length)
94
+ fileError.value = trslc('vue-collection.error.too-many-files', props.maxFiles, { max: props.maxFiles })
95
+ else if (fileSizeFilterDiff > 0) {
96
+ fileError.value = trslc('vue-collection.error.file-size', fileSizeFilterDiff, { n: fileSizeFilterDiff })
97
+ } else if (fileTypeFilterDiff > 0)
98
+ fileError.value = trslc('vue-collection.error.file-type', fileTypeFilterDiff, { n: fileTypeFilterDiff })
99
+ else fileError.value = undefined
100
+
101
+ // update new value
102
+ props.onUpdateValue?.(newFiles)
103
+ }
104
+
105
+ const files = computed(
106
+ () =>
107
+ props.value?.map((file, index) => ({
108
+ index,
109
+ name: maxLengthSplitCenter(file.name, props.maxLengthFileNames),
110
+ })) || []
111
+ )
112
+
113
+ const removeFile = (index: number) => {
114
+ const newFiles = [...(props.value || [])]
115
+ newFiles.splice(index, 1)
116
+ fileError.value = undefined
117
+ props.onUpdateValue?.(newFiles)
118
+ }
119
+
120
+ const clearFiles = () => {
121
+ fileError.value = undefined
122
+ props.onUpdateValue?.([])
123
+ }
124
+
125
+ const isDragOver = ref(false)
126
+
127
+ const onDrop = (event: DragEvent) => {
128
+ event.preventDefault()
129
+ isDragOver.value = false
130
+
131
+ if (props.disabled) return
132
+ const transfer = event.dataTransfer
133
+ if (transfer == null) return
134
+
135
+ if (transfer.items.length > 0) {
136
+ const items = [...transfer.items]
137
+ const files = items.map(item => (item.kind === 'file' ? item.getAsFile() : null)).filter(notNullish)
138
+ filterAndUpdateFiles(files)
139
+ } else {
140
+ filterAndUpdateFiles([...transfer.files])
141
+ }
142
+ }
143
+
144
+ const onDragOver = (event: DragEvent) => {
145
+ event.preventDefault()
146
+ isDragOver.value = true
147
+ }
148
+
149
+ const onDragLeave = () => {
150
+ isDragOver.value = false
151
+ }
152
+
153
+ const onClick = () => {
154
+ fileInput.value?.click()
155
+ }
156
+
157
+ const fileInput = ref<HTMLInputElement>()
158
+
159
+ const onInputFilesChanged = () => {
160
+ const input = fileInput.value
161
+ if (input != null) {
162
+ if (input.files != null) filterAndUpdateFiles([...input.files])
163
+ input.value = ''
164
+ }
165
+ }
166
+
167
+ return () => (
168
+ <div>
169
+ <button
170
+ class={[
171
+ 'block w-full rounded-md border-dashed border-2 focus-visible:border-primary-500 focus:outline-none ',
172
+ 'flex flex-col items-center justify-center text-center text-sm select-none p-4',
173
+ !props.disabled ? 'hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700' : '',
174
+ props.disabled
175
+ ? 'border-default-300 bg-default-50/50 text-default-500/30'
176
+ : isDragOver.value
177
+ ? 'border-primary-300 bg-primary-50 text-primary-700'
178
+ : 'border-default-300 bg-default-50 text-default-500',
179
+ props.height,
180
+ ]}
181
+ onDrop={onDrop}
182
+ onDragover={onDragOver}
183
+ onDragleave={onDragLeave}
184
+ onClick={onClick}
185
+ type="button"
186
+ disabled={props.disabled}
187
+ >
188
+ <input
189
+ type="file"
190
+ class="hidden"
191
+ ref={fileInput}
192
+ multiple={props.maxFiles > 1}
193
+ accept={props.accept}
194
+ onChange={onInputFilesChanged}
195
+ onClick={event => event.stopPropagation()}
196
+ />
197
+
198
+ {/* Counterweight */}
199
+ <div class="flex-grow mb-2" />
200
+
201
+ <span class="font-medium">
202
+ {trslc('vue-collection.text.drag-n-drop-files', props.maxFiles, { n: props.maxFiles })}
203
+ </span>
204
+ <span>{props.description}</span>
205
+
206
+ <div class="flex-grow mt-2 flex items-end justify-center text-red-500 font-medium">
207
+ <span>{fileError.value}</span>
208
+ </div>
209
+ </button>
210
+
211
+ <div class="mt-2 space-y-1">
212
+ <div class="flex flex-wrap gap-2 ">
213
+ {files.value.map(file => (
214
+ <NBadge key={file.index} color="default" allCaps={false} textSize="text-xs">
215
+ <div class="flex items-center space-x-2">
216
+ <span>{file.name}</span>
217
+ <NIconButton
218
+ icon={XMarkIcon}
219
+ shade={900}
220
+ size={3}
221
+ onClick={() => removeFile(file.index)}
222
+ />
223
+ </div>
224
+ </NBadge>
225
+ ))}
226
+
227
+ <div class="flex-grow text-sm text-default-500 flex items-end justify-end text-right">
228
+ <span>
229
+ <span>
230
+ {trslc('vue-collection.text.files-selected', files.value.length, {
231
+ n: files.value.length,
232
+ })}
233
+ </span>
234
+ {files.value.length > 0 && (
235
+ <>
236
+ <span> </span>
237
+ <NLink color="default" onClick={clearFiles}>
238
+ {trslc('vue-collection.action.clear-files', files.value.length)}
239
+ </NLink>
240
+ </>
241
+ )}
242
+ </span>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ )
248
+ })
249
+
250
+ export { Component as NDropzone, Component as default }
251
+
252
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
253
+ const MIME_FORMAT = /^(image|audio|application|video|text)\/\*$/
254
+ const EXTENSION_FORMAT = /^\.\w{2,20}$/
255
+ function testFileWithAcceptString(accept: string, file: File): boolean {
256
+ const splitted = accept.split(',').map(pattern => pattern.trim())
257
+ for (const pattern of splitted) {
258
+ if (MIME_FORMAT.test(pattern)) {
259
+ if (RegExp(`^${pattern.substring(0, pattern.length - 2)}\\/.{2,}$`).test(file.type)) return true
260
+ } else if (EXTENSION_FORMAT.test(pattern)) {
261
+ if (RegExp(`^.*${pattern}$`).test(file.name)) return true
262
+ }
263
+ }
264
+ return false
265
+ }
@@ -0,0 +1,32 @@
1
+ import { createComponent } from '../utils/component'
2
+ import type { PropType } from 'vue'
3
+ import type { ValidatedForm } from './ValidatedForm'
4
+
5
+ export const nFormProps = {
6
+ /**
7
+ * The {@link ValidatedForm} which will be used to validate the inputs.
8
+ * All inputs in this forms hierarchy should be added to the {@link ValidatedForm}.
9
+ */
10
+ form: Object as PropType<ValidatedForm>,
11
+ /**
12
+ * This is called, when a button of type `submit` in the hierarchy of this view is clicked
13
+ * and when the validation of the `form` was successful.
14
+ */
15
+ onSubmit: Function as PropType<() => void>,
16
+ } as const
17
+
18
+ /**
19
+ * The `NForm` should be used to wrap multiple inputs.
20
+ * If it contains a button of type `submit` in it's hierarchy,
21
+ * it catches the submit event and passes it to the {@link ValidatedForm} in its `form` prop.
22
+ */
23
+ const Component = createComponent('NForm', nFormProps, (props, context) => {
24
+ const onSubmit = (event: Event) => {
25
+ event.preventDefault()
26
+ if (!props.form || props.form.validate().isValid) props.onSubmit?.()
27
+ }
28
+
29
+ return () => <form onSubmit={onSubmit}>{context.slots.default?.()}</form>
30
+ })
31
+
32
+ export { Component as NForm, Component as default }
@@ -0,0 +1,66 @@
1
+ import { createComponentWithSlots } from '../utils/component'
2
+ import type { TWMaxWidth } from '../utils/tailwind'
3
+ import { type PropType, computed } from 'vue'
4
+ import NForm from './NForm'
5
+ import NModal, { nModalProps } from './NModal'
6
+ import type { ValidatedForm } from './ValidatedForm'
7
+
8
+ export const nFormModalProps = {
9
+ ...nModalProps,
10
+ /**
11
+ * The maximum width of the modal. A regular tailwind class.
12
+ */
13
+ maxWidth: {
14
+ type: String as PropType<TWMaxWidth>,
15
+ default: 'max-w-lg',
16
+ },
17
+ /**
18
+ * If set to `true` the modal closes when clicking on the background.
19
+ * Default is `false` as the accidental reseting of the whole form is very annoying.
20
+ */
21
+ closeOnBackground: {
22
+ type: Boolean,
23
+ default: false,
24
+ },
25
+ /**
26
+ * The {@link ValidatedForm} to validate the inputs.
27
+ * All inputs should be added to the form.
28
+ */
29
+ form: Object as PropType<ValidatedForm>,
30
+ } as const
31
+
32
+ /**
33
+ * The `NFormModal` is a {@link NModal} with an integrated form.
34
+ * When submitting a `NFormModal` the form is first validated and
35
+ * only if the validation is succesful the `onOk` event is called.
36
+ */
37
+ const Component = createComponentWithSlots(
38
+ 'NFormModal',
39
+ nFormModalProps,
40
+ ['modal', 'header', 'footer'],
41
+ (props, { slots }) => {
42
+ const onOk = () => {
43
+ if (!props.form || props.form.validate().isValid) {
44
+ props.onOk?.()
45
+ if (props.closeOnOk) props.onUpdateValue?.(false)
46
+ }
47
+ }
48
+
49
+ const childProps = computed(() => ({
50
+ ...props,
51
+ onOk,
52
+ closeOnOk: false,
53
+ onKeydown: (event: KeyboardEvent) => {
54
+ if (event.metaKey && event.key === 'Enter') onOk()
55
+ },
56
+ }))
57
+
58
+ return () => (
59
+ <NModal {...childProps.value}>
60
+ <NForm form={props.form}>{slots.default?.()}</NForm>
61
+ </NModal>
62
+ )
63
+ }
64
+ )
65
+
66
+ export { Component as NFormModal, Component as default }
@@ -0,0 +1,92 @@
1
+ import type { HeroIcon } from '../utils/tailwind'
2
+ import { createComponent } from '../utils/component'
3
+ import type { PropType } from 'vue'
4
+ import { RouterLink, type RouteLocationRaw } from 'vue-router'
5
+ import { nButtonProps } from './NButton'
6
+ import NTooltip, { mapTooltipProps, nToolTipPropsForImplementor } from './NTooltip'
7
+
8
+ export const nIconButtonProps = {
9
+ /**
10
+ * The icon of the icon-button.
11
+ */
12
+ icon: {
13
+ type: Function as PropType<HeroIcon>,
14
+ required: true,
15
+ },
16
+ /**
17
+ * The route of the icon-button. If this is set, the icon-button becomes a {@link RouterLink}.
18
+ */
19
+ route: [String, Object] as PropType<RouteLocationRaw>,
20
+ /**
21
+ * The color of the icon-button.
22
+ */
23
+ color: {
24
+ type: String,
25
+ default: 'default',
26
+ },
27
+ /**
28
+ * The shade of the icon-button.
29
+ */
30
+ shade: {
31
+ type: Number,
32
+ default: 500,
33
+ },
34
+ /**
35
+ * The size of the icon in tailwind-units.
36
+ */
37
+ size: {
38
+ type: Number,
39
+ default: 5,
40
+ },
41
+ /**
42
+ * The html attribute, which indicates the type of the button.
43
+ */
44
+ type: nButtonProps.type,
45
+ /**
46
+ * If set to `true` the icon-button is disabled and no interaction is possible.
47
+ */
48
+ disabled: Boolean,
49
+ /**
50
+ * Adds the classes to the icon-button.
51
+ * Use this instead of `class` to style the button, because the button is wrapped inside
52
+ * a div for the tooltip and `class` would be applied to the wrapping div.
53
+ */
54
+ buttonClass: String,
55
+ /**
56
+ * This is called when the icon-button is clicked.
57
+ * It is only called when the `route` prop is not set on the icon-button.
58
+ */
59
+ onClick: Function as PropType<() => void>,
60
+ ...nToolTipPropsForImplementor,
61
+ } as const
62
+
63
+ /**
64
+ * The `NIconButton` is a regular button which does not have any text but an icon instead.
65
+ */
66
+ const Component = createComponent('NIconButton', nIconButtonProps, props => {
67
+ const classes = () => [
68
+ 'block p-0.5 rounded-md focus:outline-none focus-visible:ring-2 -m-1',
69
+ props.disabled
70
+ ? `text-${props.color}-200 cursor-default`
71
+ : `hover:bg-${props.color}-${props.shade} hover:bg-opacity-10 text-${props.color}-${props.shade} focus-visible:ring-${props.color}-${props.shade} cursor-pointer`,
72
+ props.buttonClass,
73
+ ]
74
+
75
+ const content = () => <props.icon class={`w-${props.size} h-${props.size}`} />
76
+
77
+ return () => (
78
+ <NTooltip {...mapTooltipProps(props)}>
79
+ {props.route ? (
80
+ <RouterLink to={props.route} class={classes()}>
81
+ {content()}
82
+ </RouterLink>
83
+ ) : (
84
+ <button type={props.type} disabled={props.disabled} class={classes()} onClick={props.onClick}>
85
+ {content()}
86
+ </button>
87
+ )}
88
+ </NTooltip>
89
+ )
90
+ })
91
+
92
+ export { Component as NIconButton, Component as default }