@nuxt/ui 3.0.0-alpha.5 → 3.0.0-alpha.7

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 (49) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +19 -5
  3. package/dist/module.d.ts +19 -5
  4. package/dist/module.json +2 -1
  5. package/dist/module.mjs +514 -338
  6. package/dist/runtime/components/Accordion.vue +13 -5
  7. package/dist/runtime/components/Alert.vue +2 -2
  8. package/dist/runtime/components/Breadcrumb.vue +16 -8
  9. package/dist/runtime/components/Button.vue +4 -2
  10. package/dist/runtime/components/Carousel.vue +306 -0
  11. package/dist/runtime/components/Checkbox.vue +3 -6
  12. package/dist/runtime/components/CommandPalette.vue +27 -18
  13. package/dist/runtime/components/ContextMenu.vue +33 -9
  14. package/dist/runtime/components/ContextMenuContent.vue +36 -8
  15. package/dist/runtime/components/DropdownMenu.vue +33 -9
  16. package/dist/runtime/components/DropdownMenuContent.vue +38 -8
  17. package/dist/runtime/components/Form.vue +6 -2
  18. package/dist/runtime/components/Input.vue +6 -4
  19. package/dist/runtime/components/InputMenu.vue +40 -24
  20. package/dist/runtime/components/LinkBase.vue +4 -2
  21. package/dist/runtime/components/Modal.vue +2 -2
  22. package/dist/runtime/components/NavigationMenu.vue +80 -43
  23. package/dist/runtime/components/Pagination.vue +2 -2
  24. package/dist/runtime/components/RadioGroup.vue +22 -3
  25. package/dist/runtime/components/Select.vue +22 -10
  26. package/dist/runtime/components/SelectMenu.vue +43 -31
  27. package/dist/runtime/components/Slideover.vue +2 -2
  28. package/dist/runtime/components/Table.vue +232 -0
  29. package/dist/runtime/components/Tabs.vue +15 -7
  30. package/dist/runtime/components/Toast.vue +2 -2
  31. package/dist/runtime/components/Toaster.vue +1 -1
  32. package/dist/runtime/composables/defineShortcuts.js +1 -1
  33. package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
  34. package/dist/runtime/composables/useModal.d.ts +1 -1
  35. package/dist/runtime/composables/useSlideover.d.ts +1 -1
  36. package/dist/runtime/index.css +1 -1
  37. package/dist/runtime/keyframes.css +1 -0
  38. package/dist/runtime/plugins/colors.js +11 -7
  39. package/dist/runtime/types/form.d.ts +3 -1
  40. package/dist/runtime/types/index.d.ts +2 -0
  41. package/dist/runtime/types/index.js +2 -0
  42. package/dist/runtime/utils/form.d.ts +7 -1
  43. package/dist/runtime/utils/form.js +25 -1
  44. package/dist/runtime/utils/index.d.ts +1 -0
  45. package/dist/runtime/utils/index.js +3 -0
  46. package/dist/runtime/utils/link.d.ts +1 -1
  47. package/package.json +30 -14
  48. package/dist/runtime/types/component.d.ts +0 -17
  49. package/dist/runtime/types/component.js +0 -0
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { tv, type VariantProps } from 'tailwind-variants'
3
- import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxItemProps, ComboboxArrowProps } from 'radix-vue'
3
+ import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
4
4
  import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/select-menu'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof t
12
12
 
13
13
  const selectMenu = tv({ extend: tv(theme), ...(appConfig.ui?.selectMenu || {}) })
14
14
 
15
- export interface SelectMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
15
+ export interface SelectMenuItem {
16
16
  label?: string
17
17
  icon?: string
18
18
  avatar?: AvatarProps
@@ -22,7 +22,8 @@ export interface SelectMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
22
22
  * @defaultValue 'item'
23
23
  */
24
24
  type?: 'label' | 'separator' | 'item'
25
- select?(e?: Event): void
25
+ disabled?: boolean
26
+ onSelect?(e?: Event): void
26
27
  }
27
28
 
28
29
  type SelectMenuVariants = VariantProps<typeof selectMenu>
@@ -77,6 +78,11 @@ export interface SelectMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelVal
77
78
  * @defaultValue undefined
78
79
  */
79
80
  valueKey?: keyof T
81
+ /**
82
+ * When `items` is an array of objects, select the field to use as the label.
83
+ * @defaultValue 'label'
84
+ */
85
+ labelKey?: keyof T
80
86
  items?: T[] | T[][]
81
87
  /** Highlight the ring color like a focus state. */
82
88
  highlight?: boolean
@@ -108,22 +114,24 @@ export interface SelectMenuSlots<T> {
108
114
  import { computed, toRef } from 'vue'
109
115
  import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
110
116
  import { defu } from 'defu'
117
+ import isEqual from 'fast-deep-equal'
111
118
  import { reactivePick } from '@vueuse/core'
112
119
  import { useAppConfig } from '#imports'
113
120
  import { useButtonGroup } from '../composables/useButtonGroup'
114
121
  import { useComponentIcons } from '../composables/useComponentIcons'
115
122
  import { useFormField } from '../composables/useFormField'
123
+ import { get, escapeRegExp } from '../utils'
116
124
  import UIcon from './Icon.vue'
117
125
  import UAvatar from './Avatar.vue'
118
126
  import UChip from './Chip.vue'
119
- import { get } from '../utils'
120
127
 
121
128
  const props = withDefaults(defineProps<SelectMenuProps<T>>(), {
122
129
  search: true,
123
130
  portal: true,
124
131
  autofocusDelay: 0,
125
132
  searchInput: () => ({ placeholder: 'Search...' }),
126
- filter: () => ['label']
133
+ filter: () => ['label'],
134
+ labelKey: 'label' as keyof T
127
135
  })
128
136
  const emits = defineEmits<SelectMenuEmits<T>>()
129
137
  const slots = defineSlots<SelectMenuSlots<T>>()
@@ -145,21 +153,19 @@ const ui = computed(() => selectMenu({
145
153
  size: selectSize?.value,
146
154
  loading: props.loading,
147
155
  highlight: highlight.value,
148
- leading: isLeading.value || !!slots.leading,
156
+ leading: isLeading.value || !!props.avatar || !!slots.leading,
149
157
  trailing: isTrailing.value || !!slots.trailing,
150
158
  buttonGroup: orientation.value
151
159
  }))
152
160
 
153
- function displayValue(val: T, multiple?: boolean): string {
154
- if (multiple && Array.isArray(val)) {
155
- return val.map(v => displayValue(v)).join(', ')
161
+ function displayValue(value: T): string {
162
+ if (props.multiple && Array.isArray(value)) {
163
+ return value.map(v => displayValue(v)).join(', ')
156
164
  }
157
165
 
158
- if (typeof val === 'object') {
159
- return val.label
160
- }
166
+ const item = items.value.find(item => props.valueKey ? isEqual(get(item as Record<string, any>, props.valueKey as string), value) : isEqual(item, value))
161
167
 
162
- return val && String(val)
168
+ return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
163
169
  }
164
170
 
165
171
  function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: string): ArrayOrWrapped<AcceptableValue> {
@@ -167,22 +173,25 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
167
173
  return items
168
174
  }
169
175
 
170
- const fields = Array.isArray(props.filter) ? props.filter : ['label']
176
+ const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
177
+ const escapedSearchTerm = escapeRegExp(searchTerm)
171
178
 
172
179
  return items.filter((item) => {
173
180
  if (typeof item !== 'object') {
174
- return String(item).search(new RegExp(searchTerm, 'i')) !== -1
181
+ return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
175
182
  }
176
183
 
177
184
  return fields.some((field) => {
178
- const child = get(item, field)
185
+ const child = get(item, field as string)
179
186
 
180
- return child !== null && child !== undefined && String(child).search(new RegExp(searchTerm, 'i')) !== -1
187
+ return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
181
188
  })
182
189
  }) as ArrayOrWrapped<T>
183
190
  }
184
191
 
185
192
  const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
193
+ // eslint-disable-next-line vue/no-dupe-keys
194
+ const items = computed(() => groups.value.flatMap(group => group) as T[])
186
195
 
187
196
  function onUpdate(value: any) {
188
197
  // @ts-expect-error - 'target' does not exist in type 'EventInit'
@@ -220,19 +229,22 @@ function onUpdateOpen(value: boolean) {
220
229
  >
221
230
  <ComboboxAnchor as-child>
222
231
  <ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
223
- <span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
232
+ <span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
224
233
  <slot name="leading" :model-value="(modelValue as T)" :open="open" :ui="ui">
225
- <UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
234
+ <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
235
+ <UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
226
236
  </slot>
227
237
  </span>
228
238
 
229
239
  <slot :model-value="(modelValue as T)" :open="open">
230
- <span v-if="multiple ? modelValue?.length : modelValue !== undefined" :class="ui.value({ class: props.ui?.value })">
231
- {{ displayValue(modelValue as T, multiple) }}
232
- </span>
233
- <span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
234
- {{ placeholder ?? '&nbsp;' }}
235
- </span>
240
+ <template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
241
+ <span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
242
+ {{ displayedModelValue }}
243
+ </span>
244
+ <span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
245
+ {{ placeholder ?? '&nbsp;' }}
246
+ </span>
247
+ </template>
236
248
  </slot>
237
249
 
238
250
  <span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
@@ -263,7 +275,7 @@ function onUpdateOpen(value: boolean) {
263
275
  <ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
264
276
  <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
265
277
  <ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
266
- {{ item.label }}
278
+ {{ get(item, props.labelKey as string) }}
267
279
  </ComboboxLabel>
268
280
 
269
281
  <ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
@@ -272,13 +284,13 @@ function onUpdateOpen(value: boolean) {
272
284
  v-else
273
285
  :class="ui.item({ class: props.ui?.item })"
274
286
  :disabled="item.disabled"
275
- :value="valueKey && typeof item === 'object' ? (item[valueKey as keyof SelectMenuItem]) as AcceptableValue : item"
276
- @select="item.select"
287
+ :value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
288
+ @select="item.onSelect"
277
289
  >
278
290
  <slot name="item" :item="(item as T)" :index="index">
279
291
  <slot name="item-leading" :item="(item as T)" :index="index">
280
- <UAvatar v-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
281
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
292
+ <UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
293
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
282
294
  <UChip
283
295
  v-else-if="item.chip"
284
296
  :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
@@ -291,7 +303,7 @@ function onUpdateOpen(value: boolean) {
291
303
 
292
304
  <span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
293
305
  <slot name="item-label" :item="(item as T)" :index="index">
294
- {{ displayValue(item as T) }}
306
+ {{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
295
307
  </slot>
296
308
  </span>
297
309
 
@@ -35,7 +35,7 @@ export interface SlideoverProps extends DialogRootProps {
35
35
  portal?: boolean
36
36
  /**
37
37
  * Display a close button to dismiss the slideover.
38
- * `{ size: 'md', color: 'gray', variant: 'ghost' }`{lang="ts-type"}
38
+ * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
39
39
  * @defaultValue true
40
40
  */
41
41
  close?: ButtonProps | boolean
@@ -137,7 +137,7 @@ const ui = computed(() => slideover({
137
137
  v-if="close"
138
138
  :icon="closeIcon || appConfig.ui.icons.close"
139
139
  size="md"
140
- color="gray"
140
+ color="neutral"
141
141
  variant="ghost"
142
142
  aria-label="Close"
143
143
  v-bind="typeof close === 'object' ? close : undefined"
@@ -0,0 +1,232 @@
1
+ <!-- eslint-disable vue/block-tag-newline -->
2
+ <script lang="ts">
3
+ import type { Ref } from 'vue'
4
+ import { tv, type VariantProps } from 'tailwind-variants'
5
+ import type { AppConfig } from '@nuxt/schema'
6
+ import type {
7
+ Row,
8
+ ColumnDef,
9
+ ColumnFiltersState,
10
+ ColumnPinningState,
11
+ RowSelectionState,
12
+ SortingState,
13
+ ExpandedState,
14
+ VisibilityState,
15
+ GlobalFilterOptions,
16
+ ColumnFiltersOptions,
17
+ ColumnPinningOptions,
18
+ VisibilityOptions,
19
+ ExpandedOptions,
20
+ SortingOptions,
21
+ RowSelectionOptions,
22
+ Updater
23
+ } from '@tanstack/vue-table'
24
+ import _appConfig from '#build/app.config'
25
+ import theme from '#build/ui/table'
26
+
27
+ const appConfig = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
28
+
29
+ const table = tv({ extend: tv(theme), ...(appConfig.ui?.table || {}) })
30
+
31
+ type TableVariants = VariantProps<typeof table>
32
+
33
+ export type TableColumn<T> = ColumnDef<T>
34
+
35
+ export interface TableData {
36
+ [key: string]: any
37
+ }
38
+
39
+ export interface TableProps<T> {
40
+ data?: T[]
41
+ columns?: TableColumn<T>[]
42
+ /**
43
+ * Whether the table should have a sticky header.
44
+ * @defaultValue false
45
+ */
46
+ sticky?: boolean
47
+ /** Whether the table should be in loading state. */
48
+ loading?: boolean
49
+ loadingColor?: TableVariants['loadingColor']
50
+ loadingAnimation?: TableVariants['loadingAnimation']
51
+ /**
52
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/global-filtering#table-options)
53
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/global-filtering)
54
+ */
55
+ globalFilterOptions?: Omit<GlobalFilterOptions<T>, 'onGlobalFilterChange'>
56
+ /**
57
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-filtering#table-options)
58
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-filtering)
59
+ */
60
+ columnFiltersOptions?: Omit<ColumnFiltersOptions<T>, 'getFilteredRowModel' | 'onColumnFiltersChange'>
61
+ /**
62
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-pinning#table-options)
63
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-pinning)
64
+ */
65
+ columnPinningOptions?: Omit<ColumnPinningOptions, 'onColumnPinningChange'>
66
+ /**
67
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-visibility#table-options)
68
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-visibility)
69
+ */
70
+ visibilityOptions?: Omit<VisibilityOptions, 'onColumnVisibilityChange'>
71
+ /**
72
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#table-options)
73
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting)
74
+ */
75
+ sortingOptions?: Omit<SortingOptions<T>, 'getSortedRowModel' | 'onSortingChange'>
76
+ /**
77
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/expanding#table-options)
78
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/expanding)
79
+ */
80
+ expandedOptions?: Omit<ExpandedOptions<T>, 'getExpandedRowModel' | 'onExpandedChange'>
81
+ /**
82
+ * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#table-options)
83
+ * @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
84
+ */
85
+ rowSelectionOptions?: Omit<RowSelectionOptions<T>, 'onRowSelectionChange'>
86
+ class?: any
87
+ ui?: Partial<typeof table.slots>
88
+ }
89
+
90
+ export interface TableSlots<T> {
91
+ expanded(props: { row: Row<T> }): any
92
+ empty(props?: {}): any
93
+ }
94
+
95
+ </script>
96
+
97
+ <script setup lang="ts" generic="T extends TableData">
98
+ import { computed } from 'vue'
99
+ import {
100
+ FlexRender,
101
+ getCoreRowModel,
102
+ getFilteredRowModel,
103
+ getSortedRowModel,
104
+ getExpandedRowModel,
105
+ useVueTable
106
+ } from '@tanstack/vue-table'
107
+ import { upperFirst } from 'scule'
108
+
109
+ const props = defineProps<TableProps<T>>()
110
+ defineSlots<TableSlots<T>>()
111
+
112
+ const data = computed(() => props.data ?? [])
113
+ const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
114
+
115
+ const ui = computed(() => table({
116
+ sticky: props.sticky,
117
+ loading: props.loading,
118
+ loadingColor: props.loadingColor,
119
+ loadingAnimation: props.loadingAnimation
120
+ }))
121
+
122
+ const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
123
+ const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] })
124
+ const columnVisibilityState = defineModel<VisibilityState>('columnVisibility', { default: {} })
125
+ const columnPinningState = defineModel<ColumnPinningState>('columnPinning', { default: {} })
126
+ const rowSelectionState = defineModel<RowSelectionState>('rowSelection', { default: {} })
127
+ const sortingState = defineModel<SortingState>('sorting', { default: [] })
128
+ const expandedState = defineModel<ExpandedState>('expanded', { default: {} })
129
+
130
+ const tableApi = useVueTable({
131
+ data,
132
+ columns: columns.value,
133
+ getCoreRowModel: getCoreRowModel(),
134
+ ...(props.globalFilterOptions || {}),
135
+ onGlobalFilterChange: updaterOrValue => valueUpdater(updaterOrValue, globalFilterState),
136
+ ...(props.columnFiltersOptions || {}),
137
+ getFilteredRowModel: getFilteredRowModel(),
138
+ onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFiltersState),
139
+ ...(props.visibilityOptions || {}),
140
+ onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibilityState),
141
+ ...(props.columnPinningOptions || {}),
142
+ onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinningState),
143
+ ...(props.rowSelectionOptions || {}),
144
+ onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelectionState),
145
+ ...(props.sortingOptions || {}),
146
+ getSortedRowModel: getSortedRowModel(),
147
+ onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sortingState),
148
+ ...(props.expandedOptions || {}),
149
+ getExpandedRowModel: getExpandedRowModel(),
150
+ onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expandedState),
151
+ state: {
152
+ get globalFilter() {
153
+ return globalFilterState.value
154
+ },
155
+ get columnFilters() {
156
+ return columnFiltersState.value
157
+ },
158
+ get columnVisibility() {
159
+ return columnVisibilityState.value
160
+ },
161
+ get columnPinning() {
162
+ return columnPinningState.value
163
+ },
164
+ get expanded() {
165
+ return expandedState.value
166
+ },
167
+ get rowSelection() {
168
+ return rowSelectionState.value
169
+ },
170
+ get sorting() {
171
+ return sortingState.value
172
+ }
173
+ }
174
+ })
175
+
176
+ function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
177
+ ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
178
+ }
179
+
180
+ defineExpose({
181
+ tableApi
182
+ })
183
+ </script>
184
+
185
+ <template>
186
+ <div :class="ui.root({ class: [props.class, props.ui?.root] })">
187
+ <table :class="ui.base({ class: [props.ui?.base] })">
188
+ <thead :class="ui.thead({ class: [props.ui?.thead] })">
189
+ <tr v-for="headerGroup in tableApi.getHeaderGroups()" :key="headerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
190
+ <th
191
+ v-for="header in headerGroup.headers"
192
+ :key="header.id"
193
+ :data-pinned="header.column.getIsPinned()"
194
+ :class="ui.th({ class: [props.ui?.th], pinned: !!header.column.getIsPinned() })"
195
+ >
196
+ <FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
197
+ </th>
198
+ </tr>
199
+ </thead>
200
+
201
+ <tbody :class="ui.tbody({ class: [props.ui?.tbody] })">
202
+ <template v-if="tableApi.getRowModel().rows?.length">
203
+ <template v-for="row in tableApi.getRowModel().rows" :key="row.id">
204
+ <tr :data-selected="row.getIsSelected()" :data-expanded="row.getIsExpanded()" :class="ui.tr({ class: [props.ui?.tr] })">
205
+ <td
206
+ v-for="cell in row.getVisibleCells()"
207
+ :key="cell.id"
208
+ :data-pinned="cell.column.getIsPinned()"
209
+ :class="ui.td({ class: [props.ui?.td], pinned: !!cell.column.getIsPinned() })"
210
+ >
211
+ <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
212
+ </td>
213
+ </tr>
214
+ <tr v-if="row.getIsExpanded()" :class="ui.tr({ class: [props.ui?.tr] })">
215
+ <td :colspan="row.getAllCells().length" :class="ui.td({ class: [props.ui?.td] })">
216
+ <slot name="expanded" :row="row" />
217
+ </td>
218
+ </tr>
219
+ </template>
220
+ </template>
221
+
222
+ <tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
223
+ <td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
224
+ <slot name="empty">
225
+ No results
226
+ </slot>
227
+ </td>
228
+ </tr>
229
+ </tbody>
230
+ </table>
231
+ </div>
232
+ </template>
@@ -1,7 +1,7 @@
1
1
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv, type VariantProps } from 'tailwind-variants'
4
- import type { TabsRootProps, TabsRootEmits, TabsContentProps, TabsTriggerProps } from 'radix-vue'
4
+ import type { TabsRootProps, TabsRootEmits, TabsContentProps } from 'radix-vue'
5
5
  import type { AppConfig } from '@nuxt/schema'
6
6
  import _appConfig from '#build/app.config'
7
7
  import theme from '#build/ui/tabs'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { tabs: Partial<typeof theme>
12
12
 
13
13
  const tabs = tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {}) })
14
14
 
15
- export interface TabsItem extends Pick<TabsTriggerProps, 'disabled'> {
15
+ export interface TabsItem {
16
16
  label?: string
17
17
  icon?: string
18
18
  avatar?: AvatarProps
@@ -20,6 +20,7 @@ export interface TabsItem extends Pick<TabsTriggerProps, 'disabled'> {
20
20
  content?: string
21
21
  /** A unique value for the tab item. Defaults to the index. */
22
22
  value?: string | number
23
+ disabled?: boolean
23
24
  }
24
25
 
25
26
  type TabsVariants = VariantProps<typeof tabs>
@@ -44,6 +45,11 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
44
45
  * @defaultValue true
45
46
  */
46
47
  content?: boolean | Omit<TabsContentProps, 'as' | 'asChild' | 'value'>
48
+ /**
49
+ * The key used to get the label from the item.
50
+ * @defaultValue 'label'
51
+ */
52
+ labelKey?: string
47
53
  class?: any
48
54
  ui?: PartialString<typeof tabs.slots>
49
55
  }
@@ -66,11 +72,13 @@ import { computed, toRef } from 'vue'
66
72
  import { defu } from 'defu'
67
73
  import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'radix-vue'
68
74
  import { reactivePick } from '@vueuse/core'
75
+ import { get } from '../utils'
69
76
 
70
77
  const props = withDefaults(defineProps<TabsProps<T>>(), {
71
78
  content: true,
72
79
  defaultValue: '0',
73
- orientation: 'horizontal'
80
+ orientation: 'horizontal',
81
+ labelKey: 'label'
74
82
  })
75
83
  const emits = defineEmits<TabsEmits>()
76
84
  const slots = defineSlots<TabsSlots<T>>()
@@ -93,12 +101,12 @@ const ui = computed(() => tabs({
93
101
 
94
102
  <TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: props.ui?.trigger })">
95
103
  <slot name="leading" :item="item" :index="index">
96
- <UAvatar v-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
97
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
104
+ <UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
105
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
98
106
  </slot>
99
107
 
100
- <span v-if="item.label || !!slots.default" :class="ui.label({ class: props.ui?.label })">
101
- <slot :item="item" :index="index">{{ item.label }}</slot>
108
+ <span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
109
+ <slot :item="item" :index="index">{{ get(item, props.labelKey as string) }}</slot>
102
110
  </span>
103
111
 
104
112
  <slot name="trailing" :item="item" :index="index" />
@@ -31,7 +31,7 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
31
31
  actions?: ButtonProps[]
32
32
  /**
33
33
  * Display a close button to dismiss the toast.
34
- * `{ size: 'md', color: 'gray', variant: 'link' }`{lang="ts-type"}
34
+ * `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
35
35
  * @defaultValue true
36
36
  */
37
37
  close?: ButtonProps | boolean
@@ -145,7 +145,7 @@ defineExpose({
145
145
  v-if="close"
146
146
  :icon="closeIcon || appConfig.ui.icons.close"
147
147
  size="md"
148
- color="gray"
148
+ color="neutral"
149
149
  variant="link"
150
150
  aria-label="Close"
151
151
  v-bind="typeof close === 'object' ? close : undefined"
@@ -32,8 +32,8 @@ import { ref, computed } from 'vue'
32
32
  import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue'
33
33
  import { reactivePick } from '@vueuse/core'
34
34
  import { useToast } from '../composables/useToast'
35
- import UToast from './Toast.vue'
36
35
  import { omit } from '../utils'
36
+ import UToast from './Toast.vue'
37
37
 
38
38
  const props = withDefaults(defineProps<ToasterProps>(), {
39
39
  expand: true,
@@ -9,7 +9,7 @@ export function extractShortcuts(items) {
9
9
  items2.forEach((item) => {
10
10
  if (item.kbds?.length && (item.select || item.click)) {
11
11
  const shortcutKey = item.kbds.join("_");
12
- shortcuts[shortcutKey] = item.select || item.click;
12
+ shortcuts[shortcutKey] = item.onSelect || item.onClick;
13
13
  }
14
14
  if (item.children) {
15
15
  traverse(item.children.flat());
@@ -1,7 +1,10 @@
1
1
  import { type MaybeRefOrGetter } from 'vue';
2
+ import type { AvatarProps } from '../types';
2
3
  export interface UseComponentIconsProps {
3
4
  /** Display an icon based on the `leading` and `trailing` props. */
4
5
  icon?: string;
6
+ /** Display an avatar on the left side. */
7
+ avatar?: AvatarProps;
5
8
  /** When `true`, the icon will be displayed on the left side. */
6
9
  leading?: boolean;
7
10
  /** Display an icon on the left side. */
@@ -1,6 +1,6 @@
1
1
  import type { ShallowRef, Component, InjectionKey } from 'vue';
2
+ import type { ComponentProps } from 'vue-component-type-helpers';
2
3
  import type { ModalProps } from '../types';
3
- import type { ComponentProps } from '../types/component';
4
4
  export interface ModalState {
5
5
  component: Component | string;
6
6
  props: ModalProps;
@@ -1,6 +1,6 @@
1
1
  import type { ShallowRef, Component, InjectionKey } from 'vue';
2
+ import type { ComponentProps } from 'vue-component-type-helpers';
2
3
  import type { SlideoverProps } from '../types';
3
- import type { ComponentProps } from '../types/component';
4
4
  export interface SlideoverState {
5
5
  component: Component | string;
6
6
  props: SlideoverProps;
@@ -1 +1 @@
1
- @theme{--color-gray-*:initial;--color-cool-50:#f9fafb;--color-cool-100:#f3f4f6;--color-cool-200:#e5e7eb;--color-cool-300:#d1d5db;--color-cool-400:#9ca3af;--color-cool-500:#6b7280;--color-cool-600:#4b5563;--color-cool-700:#374151;--color-cool-800:#1f2937;--color-cool-900:#111827;--color-cool-950:#030712;--spacing-4_5:1.125rem;--color-primary-50:var(--color-primary-50);--color-primary-100:var(--color-primary-100);--color-primary-200:var(--color-primary-200);--color-primary-300:var(--color-primary-300);--color-primary-400:var(--color-primary-400);--color-primary-500:var(--color-primary-500);--color-primary-600:var(--color-primary-600);--color-primary-700:var(--color-primary-700);--color-primary-800:var(--color-primary-800);--color-primary-900:var(--color-primary-900);--color-primary-950:var(--color-primary-950);--color-error-50:var(--color-error-50);--color-error-100:var(--color-error-100);--color-error-200:var(--color-error-200);--color-error-300:var(--color-error-300);--color-error-400:var(--color-error-400);--color-error-500:var(--color-error-500);--color-error-600:var(--color-error-600);--color-error-700:var(--color-error-700);--color-error-800:var(--color-error-800);--color-error-900:var(--color-error-900);--color-error-950:var(--color-error-950);--color-gray-50:var(--color-gray-50);--color-gray-100:var(--color-gray-100);--color-gray-200:var(--color-gray-200);--color-gray-300:var(--color-gray-300);--color-gray-400:var(--color-gray-400);--color-gray-500:var(--color-gray-500);--color-gray-600:var(--color-gray-600);--color-gray-700:var(--color-gray-700);--color-gray-800:var(--color-gray-800);--color-gray-900:var(--color-gray-900);--color-gray-950:var(--color-gray-950)}@variant dark (&:where(.dark, .dark *));@layer base{:root{color-scheme:light dark}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height)}to{height:0}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height)}}@keyframes collapsible-up{0%{height:var(--radix-collapsible-content-height)}to{height:0}}@keyframes collapsible-down{0%{height:0}to{height:var(--radix-collapsible-content-height)}}@keyframes toast-collapsed-closed{0%{transform:var(--transform)}to{transform:translateY(calc((var(--before) - var(--height))*var(--gap))) scale(var(--scale))}}@keyframes toast-closed{0%{transform:var(--transform)}to{transform:translateY(calc((var(--offset) - var(--height))*var(--translate-factor)))}}@keyframes toast-slide-left{0%{transform:translateX(0) translateY(var(--translate))}to{transform:translateX(-100%) translateY(var(--translate))}}@keyframes toast-slide-right{0%{transform:translateX(0) translateY(var(--translate))}to{transform:translateX(100%) translateY(var(--translate))}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes scale-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slide-in-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes slide-out-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes slide-in-from-right{0%{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes slide-out-to-right{0%{transform:translateX(0)}to{transform:translateX(100%)}}@keyframes slide-in-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes slide-out-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes slide-in-from-left{0%{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes slide-out-to-left{0%{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes slide-in-from-top-and-fade{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-out-to-top-and-fade{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}@keyframes slide-in-from-right-and-fade{0%{opacity:0;transform:translateX(4px)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-to-right-and-fade{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(4px)}}@keyframes slide-in-from-bottom-and-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-out-to-bottom-and-fade{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(4px)}}@keyframes slide-in-from-left-and-fade{0%{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-to-left-and-fade{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-4px)}}@keyframes enter-from-right{0%{opacity:0;transform:translateX(200px)}to{opacity:1;transform:translateX(0)}}@keyframes enter-from-left{0%{opacity:0;transform:translateX(-200px)}to{opacity:1;transform:translateX(0)}}@keyframes exit-to-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(200px)}}@keyframes exit-to-left{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-200px)}}@keyframes carousel{0%,to{width:50%}0%{transform:translateX(-100%)}to{transform:translateX(200%)}}@keyframes carousel-vertical{0%,to{height:50%}0%{transform:translateY(-100%)}to{transform:translateY(200%)}}@keyframes carousel-inverse{0%,to{width:50%}0%{transform:translateX(200%)}to{transform:translateX(-100%)}}@keyframes carousel-inverse-vertical{0%,to{height:50%}0%{transform:translateY(200%)}to{transform:translateY(-100%)}}@keyframes swing{0%,to{width:50%}0%,to{transform:translateX(-25%)}50%{transform:translateX(125%)}}@keyframes swing-vertical{0%,to{height:50%}0%,to{transform:translateY(-25%)}50%{transform:translateY(125%)}}@keyframes elastic{0%,to{margin-left:25%;width:50%}50%{margin-left:5%;width:90%}}@keyframes elastic-vertical{0%,to{height:50%;margin-top:25%}50%{height:90%;margin-top:5%}}}
1
+ @import "./keyframes.css";@theme default{--spacing-4_5:1.125rem}@variant dark (&:where(.dark, .dark *));@layer base{body{@apply antialiased text-[var(--ui-text)] bg-[var(--ui-bg)]}:root{color-scheme:light;--ui-text-dimmed:var(--ui-color-neutral-400);--ui-text-muted:var(--ui-color-neutral-500);--ui-text-toned:var(--ui-color-neutral-600);--ui-text:var(--ui-color-neutral-700);--ui-text-highlighted:var(--ui-color-neutral-900);--ui-bg:var(--color-white);--ui-bg-elevated:var(--ui-color-neutral-100);--ui-bg-accented:var(--ui-color-neutral-200);--ui-bg-inverted:var(--ui-color-neutral-900);--ui-border:var(--ui-color-neutral-200);--ui-border-accented:var(--ui-color-neutral-300);--ui-border-inverted:var(--ui-color-neutral-900);--ui-radius:var(--radius)}.dark{color-scheme:dark;--ui-text-dimmed:var(--ui-color-neutral-500);--ui-text-muted:var(--ui-color-neutral-400);--ui-text-toned:var(--ui-color-neutral-300);--ui-text:var(--ui-color-neutral-200);--ui-text-highlighted:var(--color-white);--ui-bg:var(--ui-color-neutral-900);--ui-bg-elevated:var(--ui-color-neutral-800);--ui-bg-accented:var(--ui-color-neutral-700);--ui-bg-inverted:var(--color-white);--ui-border:var(--ui-color-neutral-800);--ui-border-accented:var(--ui-color-neutral-700);--ui-border-inverted:var(--color-white)}}
@@ -0,0 +1 @@
1
+ @keyframes accordion-up{0%{height:var(--radix-accordion-content-height)}to{height:0}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height)}}@keyframes collapsible-up{0%{height:var(--radix-collapsible-content-height)}to{height:0}}@keyframes collapsible-down{0%{height:0}to{height:var(--radix-collapsible-content-height)}}@keyframes toast-collapsed-closed{0%{transform:var(--transform)}to{transform:translateY(calc((var(--before) - var(--height))*var(--gap))) scale(var(--scale))}}@keyframes toast-closed{0%{transform:var(--transform)}to{transform:translateY(calc((var(--offset) - var(--height))*var(--translate-factor)))}}@keyframes toast-slide-left{0%{transform:translateX(0) translateY(var(--translate))}to{transform:translateX(-100%) translateY(var(--translate))}}@keyframes toast-slide-right{0%{transform:translateX(0) translateY(var(--translate))}to{transform:translateX(100%) translateY(var(--translate))}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes scale-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slide-in-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes slide-out-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes slide-in-from-right{0%{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes slide-out-to-right{0%{transform:translateX(0)}to{transform:translateX(100%)}}@keyframes slide-in-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes slide-out-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes slide-in-from-left{0%{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes slide-out-to-left{0%{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes slide-in-from-top-and-fade{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-out-to-top-and-fade{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}@keyframes slide-in-from-right-and-fade{0%{opacity:0;transform:translateX(4px)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-to-right-and-fade{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(4px)}}@keyframes slide-in-from-bottom-and-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-out-to-bottom-and-fade{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(4px)}}@keyframes slide-in-from-left-and-fade{0%{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:translateX(0)}}@keyframes slide-out-to-left-and-fade{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-4px)}}@keyframes enter-from-right{0%{opacity:0;transform:translateX(200px)}to{opacity:1;transform:translateX(0)}}@keyframes enter-from-left{0%{opacity:0;transform:translateX(-200px)}to{opacity:1;transform:translateX(0)}}@keyframes exit-to-right{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(200px)}}@keyframes exit-to-left{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-200px)}}@keyframes carousel{0%,to{width:50%}0%{transform:translateX(-100%)}to{transform:translateX(200%)}}@keyframes carousel-vertical{0%,to{height:50%}0%{transform:translateY(-100%)}to{transform:translateY(200%)}}@keyframes carousel-inverse{0%,to{width:50%}0%{transform:translateX(200%)}to{transform:translateX(-100%)}}@keyframes carousel-inverse-vertical{0%,to{height:50%}0%{transform:translateY(200%)}to{transform:translateY(-100%)}}@keyframes swing{0%,to{width:50%}0%,to{transform:translateX(-25%)}50%{transform:translateX(125%)}}@keyframes swing-vertical{0%,to{height:50%}0%,to{transform:translateY(-25%)}50%{transform:translateY(125%)}}@keyframes elastic{0%,to{margin-left:25%;width:50%}50%{margin-left:5%;width:90%}}@keyframes elastic-vertical{0%,to{height:50%;margin-top:25%}50%{height:90%;margin-top:5%}}
@@ -4,18 +4,22 @@ export default defineNuxtPlugin(() => {
4
4
  const appConfig = useAppConfig();
5
5
  const nuxtApp = useNuxtApp();
6
6
  const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
7
- function generateColor(key, value) {
8
- return `${shades.map((shade) => `--color-${key}-${shade}: var(--color-${value}-${shade});`).join("\n ")}`;
7
+ function generateShades(key, value) {
8
+ return `${shades.map((shade) => `--ui-color-${key}-${shade}: var(--color-${value}-${shade});`).join("\n ")}`;
9
+ }
10
+ function generateColor(key, shade) {
11
+ return `--ui-${key}: var(--ui-color-${key}-${shade});`;
9
12
  }
10
13
  const root = computed(() => {
14
+ const { neutral, ...colors } = appConfig.ui.colors;
11
15
  return `:root {
12
- ${Object.entries(appConfig.ui.colors).map(([key, value]) => generateColor(key, value)).join("\n ")}
13
- --color-primary-DEFAULT: var(--color-primary-500);
16
+ ${Object.entries(appConfig.ui.colors).map(([key, value]) => generateShades(key, value)).join("\n ")}
17
+
18
+ ${Object.keys(colors).map((key) => generateColor(key, 500)).join("\n ")}
14
19
  }
15
20
  .dark {
16
- --color-primary-DEFAULT: var(--color-primary-400);
17
- }
18
- `;
21
+ ${Object.keys(colors).map((key) => generateColor(key, 400)).join("\n ")}
22
+ }`;
19
23
  });
20
24
  const headData = {
21
25
  style: [{