@nexxtmove/ui 1.13.0 → 1.15.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/dist/nuxt.js CHANGED
@@ -63,7 +63,8 @@ var s = {
63
63
  NexxtTimelinePhaseblock: "components/organisms/TimelinePhaseblock/TimelinePhaseblock.vue",
64
64
  NexxtTimelineTrack: "components/organisms/TimelineTrack/TimelineTrack.vue",
65
65
  NexxtToaster: "components/organisms/Toaster/Toaster.vue",
66
- NexxtVideoPlayer: "components/organisms/VideoPlayer/VideoPlayer.vue"
66
+ NexxtVideoPlayer: "components/organisms/VideoPlayer/VideoPlayer.vue",
67
+ NexxtEditorSettingsBlock: "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue"
67
68
  }, c = ["@nexxtmove/ui/ui.css", "@nexxtmove/ui/dist/ui.css"], l = (e, t) => e.some((e) => e === t || c.includes(e)), u = i({
68
69
  meta: {
69
70
  name: "@nexxtmove/ui",
@@ -85,8 +86,8 @@ var s = {
85
86
  });
86
87
  let d = u("./assets");
87
88
  if (a(d)) {
88
- let e = `/${[c.options.app.buildAssetsDir, "assets"].join("/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "")}/`;
89
- c.options.nitro.publicAssets ??= [], c.options.nitro.publicAssets.push({
89
+ let e = `/${[c.options.app.buildAssetsDir, "assets"].join("/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "")}/`, t = c.options;
90
+ t.nitro.publicAssets ??= [], t.nitro.publicAssets.push({
90
91
  dir: d,
91
92
  baseURL: e,
92
93
  maxAge: 31536e3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.13.0",
4
+ "version": "1.15.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -37,12 +37,12 @@
37
37
  "@eslint/js": "^10.0.1",
38
38
  "@nuxt/kit": "^4.5.2",
39
39
  "@nuxt/schema": "^4.5.2",
40
- "@storybook/addon-a11y": "^10.5.10",
40
+ "@storybook/addon-a11y": "^10.6.0",
41
41
  "@storybook/addon-designs": "^11.1.4",
42
- "@storybook/addon-docs": "^10.5.10",
43
- "@storybook/addon-vitest": "^10.5.10",
44
- "@storybook/builder-vite": "^10.5.10",
45
- "@storybook/vue3-vite": "^10.5.10",
42
+ "@storybook/addon-docs": "^10.6.0",
43
+ "@storybook/addon-vitest": "^10.6.0",
44
+ "@storybook/builder-vite": "^10.6.0",
45
+ "@storybook/vue3-vite": "^10.6.0",
46
46
  "@tailwindcss/vite": "^4.3.3",
47
47
  "@types/node": "^26.2.0",
48
48
  "@types/react": "^19.2.18",
@@ -59,7 +59,7 @@
59
59
  "eslint-config-prettier": "^10.1.8",
60
60
  "eslint-plugin-better-tailwindcss": "4.7.0",
61
61
  "eslint-plugin-prettier": "^5.5.6",
62
- "eslint-plugin-storybook": "10.5.10",
62
+ "eslint-plugin-storybook": "10.6.0",
63
63
  "eslint-plugin-vue": "^10.10.0",
64
64
  "flag-icons": "^7.5.0",
65
65
  "globals": "^17.11.0",
@@ -70,7 +70,7 @@
70
70
  "react": "^19.2.8",
71
71
  "react-dom": "^19.2.8",
72
72
  "remark-frontmatter": "^5.0.0",
73
- "storybook": "^10.5.10",
73
+ "storybook": "^10.6.0",
74
74
  "typescript-eslint": "^8.67.0",
75
75
  "vite": "^8.2.1",
76
76
  "vite-plugin-dts": "^5.0.3",
@@ -61,11 +61,18 @@ const iconSizeClasses: Record<SwitchSize, string> = {
61
61
  <template>
62
62
  <label class="inline-flex cursor-pointer items-center gap-2">
63
63
  <slot v-if="labelPosition === 'before'" />
64
- <span :class="['relative inline-block', trackClasses[size]]">
64
+ <!--
65
+ `shrink-0`: the track has a fixed pixel size, so letting flex shrink it
66
+ squashes the toggle the moment a sibling label wraps onto a second line.
67
+ -->
68
+ <span :class="['relative inline-block shrink-0', trackClasses[size]]">
65
69
  <input v-bind="$attrs" type="checkbox" class="peer sr-only" v-model="model" />
66
70
  <span
67
71
  :class="[
68
72
  'absolute inset-0 overflow-hidden rounded-[30px] border transition-colors duration-200',
73
+ // The input is `sr-only`, so the track has to render the focus ring.
74
+ // `outline` draws outside the box and is not clipped by `overflow-hidden`.
75
+ 'peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-cornflower-blue-500',
69
76
  model ? checkedColorClasses[color] : 'border-gray-500 bg-gray-200',
70
77
  ]"
71
78
  >
@@ -0,0 +1,83 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+ import Switch from '../../../atoms/Switch/Switch.vue'
4
+
5
+ interface NexxtEditorSettingsBlockProps {
6
+ /** Header text of the block. Wraps over multiple lines when it is too long. */
7
+ label: string
8
+ /**
9
+ * Render a switch next to the label that collapses and expands the content.
10
+ * When `false` the content is always visible and the model is ignored.
11
+ */
12
+ switchable?: boolean
13
+ }
14
+
15
+ defineOptions({ name: 'NexxtEditorSettingsBlock' })
16
+
17
+ const { label, switchable = false } = defineProps<NexxtEditorSettingsBlockProps>()
18
+
19
+ /**
20
+ * Open state of the block, driven by the switch. Only has an effect when
21
+ * `switchable` is `true`; the parent owns the state.
22
+ */
23
+ const model = defineModel<boolean>({ default: true })
24
+
25
+ const open = computed(() => !switchable || model.value)
26
+
27
+ // Vue only drops an attribute on `false` for a fixed list of boolean
28
+ // attributes, and `inert` is not on that list: `:inert="false"` renders
29
+ // `inert="false"`, which a browser still treats as inert. `undefined` is the
30
+ // only value that removes the attribute again.
31
+ const inert = computed(() => (open.value ? undefined : true))
32
+ </script>
33
+
34
+ <template>
35
+ <div class="border-b border-gray-200 py-4 last:border-b-0">
36
+ <!--
37
+ Switch.vue sets `inheritAttrs: false` and binds `$attrs` on its sr-only
38
+ input, so a class on <Switch> never reaches its root <label>. The child
39
+ variant below styles that label from the header instead, which keeps the
40
+ whole row clickable and the accessible name on the real <label>. The gap
41
+ has to go on that label too: the header has a single child in both modes,
42
+ so its own `gap` would never apply and the spacing would silently fall
43
+ back to the `gap-2` that Switch.vue sets for itself.
44
+ -->
45
+ <div class="flex items-center [&>label]:flex [&>label]:w-full [&>label]:gap-3">
46
+ <Switch v-if="switchable" v-model="model" size="sm" color="blue" label-position="before">
47
+ <span class="grow text-left small-normal text-gray-900">{{ label }}</span>
48
+ </Switch>
49
+ <p v-else class="small-normal text-gray-900">{{ label }}</p>
50
+ </div>
51
+
52
+ <div
53
+ v-if="$slots.default"
54
+ :inert="inert"
55
+ :class="[
56
+ 'grid',
57
+ open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
58
+ switchable &&
59
+ 'transition-[grid-template-rows] duration-300 ease-in-out motion-reduce:transition-none',
60
+ ]"
61
+ >
62
+ <!--
63
+ `overflow-hidden` is what makes the 0fr collapse clip, but it also clips
64
+ a focus ring on a full-width control in the slot, and the block has no
65
+ horizontal padding of its own. `-mx-1` widens the clipping box by 4px on
66
+ each side and the `px-1` below pushes the content back, so the ring has
67
+ room while the content keeps the same x position as the label.
68
+ -->
69
+ <div
70
+ :class="[
71
+ '-mx-1 min-h-0 overflow-hidden',
72
+ open ? 'opacity-100' : 'opacity-0',
73
+ switchable && 'transition-opacity duration-200 ease-in-out motion-reduce:transition-none',
74
+ ]"
75
+ >
76
+ <!-- Padding sits on the inner element so nothing is left over at 0fr. -->
77
+ <div class="px-1 pt-3 pb-1">
78
+ <slot />
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </template>
@@ -7,11 +7,31 @@ import NexxtTooltip from '../../atoms/Tooltip/Tooltip.vue'
7
7
  export type NexxtTemplateCardContentFeature = 'references' | 'valuation' | 'reviews' | 'packages'
8
8
 
9
9
  export interface NexxtTemplateCardProps {
10
+ /** Preview image of the template. Falls back to a placeholder of the same height when omitted. */
10
11
  image?: string
12
+ /** Template name, shown as the card heading. Truncated on one line. */
11
13
  title: string
14
+ /** Secondary line under the title, e.g. when the template was last changed. Truncated on one line. */
12
15
  subtitle?: string
16
+ /** Content blocks the template contains; each renders an icon with a tooltip. */
13
17
  contentFeatures?: NexxtTemplateCardContentFeature[]
18
+ /** Overrides the default Dutch tooltip text per content feature. */
14
19
  messages?: Partial<Record<NexxtTemplateCardContentFeature, string>>
20
+ /** Lays a full-card button over the content that emits `select`. Off keeps the card inert, so a read-only grid adds no empty tabstop per card. */
21
+ selectable?: boolean
22
+ /** Whether this card is currently chosen: draws the blue border and, with `checkmark`, the badge. Only does something with `selectable`. A prop plus the `select` emit rather than a `defineModel`, because the card owns no state: the parent grid decides which single card may be chosen. */
23
+ selected?: boolean
24
+ /** Shows a check badge in the top-right corner while selected. Only does something with `selectable`. */
25
+ checkmark?: boolean
26
+ /** Disables the select button, dims the card content and drops the overlay. Only does something with `selectable`. */
27
+ disabled?: boolean
28
+ }
29
+
30
+ export type NexxtTemplateCardSlots = {
31
+ /** Covers the whole card and fades in on hover or keyboard focus; the content underneath dims. Not rendered while the card is selectable and disabled. On a coarse pointer it shrinks to the image band, without veil or blur, so the title stays readable and the feature icons stay tappable. The layer is click-through, but `*:pointer-events-auto` only reaches its direct children: an own `absolute inset-0` wrapper inside the slot swallows the select click. */
32
+ overlay?(props: { selected: boolean }): unknown
33
+ /** Top-right corner of the card, for a preview or menu button. Stays interactive while `disabled`, which makes it the place for e.g. a preview button: the overlay is not rendered then. */
34
+ actions?(props: { selected: boolean }): unknown
15
35
  }
16
36
 
17
37
  type TemplateCardIcon = {
@@ -45,46 +65,152 @@ const props = withDefaults(defineProps<NexxtTemplateCardProps>(), {
45
65
  subtitle: undefined,
46
66
  contentFeatures: () => [],
47
67
  messages: () => ({}),
68
+ selectable: false,
69
+ selected: false,
70
+ checkmark: false,
71
+ disabled: false,
48
72
  })
49
73
 
74
+ const emit = defineEmits<{
75
+ /** The card was chosen. No payload: the consumer knows which template it rendered. */
76
+ select: []
77
+ }>()
78
+
79
+ // The root of the template stays a plain `<div>` and must stay single-root: the heading, the
80
+ // paragraph and the tooltip triggers are flow content, which is invalid inside a `<button>`, and
81
+ // wrapping them also dropped the heading role and turned the accessible name of the button into
82
+ // the whole card as one long string. A leading comment in the template would make it a fragment,
83
+ // after which class and attributes no longer reach the card, so the notes live here.
84
+ // Selection, hover and the dim are driven from the props and from the root's own `:hover`, not
85
+ // from the button. The slot checks are read inline rather than through a computed -- slot presence
86
+ // is not reactive, so a consumer toggling `<template v-if>` on a slot would never re-evaluate a
87
+ // cached computed.
88
+ //
89
+ // The overlay and the actions are siblings of the select button, never children: a button inside a
90
+ // button is invalid markup and swallows the inner click, and both layers stack above it so their
91
+ // own buttons get the click. They are handed the selection because their content often styles
92
+ // itself by it.
93
+ defineSlots<NexxtTemplateCardSlots>()
94
+
50
95
  const featureMessages = computed<Record<NexxtTemplateCardContentFeature, string>>(() => ({
51
96
  ...DEFAULT_MESSAGES,
52
97
  ...props.messages,
53
98
  }))
99
+
100
+ const onSelect = () => {
101
+ if (props.selectable && !props.disabled) emit('select')
102
+ }
54
103
  </script>
55
104
 
56
105
  <template>
57
- <div class="w-full max-w-105 rounded-xl border border-gray-200">
58
- <picture class="h-38 w-full">
59
- <img
60
- v-if="image"
61
- :src="image"
62
- :alt="title"
63
- loading="lazy"
64
- class="w-full rounded-t-xl object-cover"
65
- />
66
- <div v-else class="flex h-38 w-full items-center justify-center rounded-t-xl bg-gray-100">
67
- <NexxtIcon name="image" class="text-4xl text-gray-400" />
106
+ <div
107
+ class="group relative size-full max-w-105 rounded-xl border border-gray-200 transition-colors duration-200 ease-out"
108
+ :class="[
109
+ selectable && selected && 'border-cornflower-blue-500',
110
+ selectable && !selected && !disabled && 'hover:border-gray-300',
111
+ ]"
112
+ >
113
+ <div
114
+ class="relative z-10 transition-opacity duration-200 ease-out"
115
+ :class="[
116
+ // Lets a click anywhere on the card reach the stretched button underneath; only the
117
+ // tooltip triggers claim their own pointer events back.
118
+ selectable && 'pointer-events-none',
119
+ selectable && disabled && 'opacity-50',
120
+ $slots.overlay &&
121
+ !(selectable && disabled) &&
122
+ 'group-focus-within:opacity-40 group-hover:opacity-40 pointer-coarse:opacity-100!',
123
+ ]"
124
+ >
125
+ <picture class="block h-38 w-full">
126
+ <!-- Decorative: the template name is right below it as text. -->
127
+ <img
128
+ v-if="image"
129
+ :src="image"
130
+ alt=""
131
+ loading="lazy"
132
+ class="h-38 w-full rounded-t-xl object-cover"
133
+ />
134
+ <div v-else class="flex h-38 w-full items-center justify-center rounded-t-xl bg-gray-100">
135
+ <NexxtIcon name="image" class="text-4xl text-gray-400" />
136
+ </div>
137
+ </picture>
138
+ <div class="rounded-b-xl px-8 pt-3.5 pb-4">
139
+ <h3 class="truncate heading-4 text-gray-900">{{ title }}</h3>
140
+ <p v-if="subtitle" class="truncate body-normal text-gray-700">{{ subtitle }}</p>
141
+ <ul v-if="contentFeatures.length" class="mt-3 flex flex-wrap gap-2">
142
+ <li v-for="feature in contentFeatures" :key="feature">
143
+ <NexxtTooltip>
144
+ <!--
145
+ Claims its pointer events back from the content layer so the tooltip still opens on
146
+ hover, and forwards the click to the card so the icon is no dead zone on a
147
+ selectable card. `onSelect` guards on selectable and disabled itself.
148
+ NexxtIcon is aria-hidden and the trigger is not focusable, so the label is only
149
+ there for screen readers.
150
+ -->
151
+ <div
152
+ :class="[ICON_MAP[feature].backgroundColor, ICON_MAP[feature].textColor]"
153
+ class="pointer-events-auto flex size-8 items-center justify-center rounded-full"
154
+ @click="onSelect"
155
+ >
156
+ <NexxtIcon :name="ICON_MAP[feature].icon" />
157
+ <span class="sr-only">{{ featureMessages[feature] }}</span>
158
+ </div>
159
+ <template #tooltip>
160
+ <span class="small-normal text-gray-700">{{ featureMessages[feature] }}</span>
161
+ </template>
162
+ </NexxtTooltip>
163
+ </li>
164
+ </ul>
68
165
  </div>
69
- </picture>
70
- <div class="rounded-b-xl px-8 pt-3.5 pb-4">
71
- <h3 class="truncate heading-4 text-gray-900">{{ title }}</h3>
72
- <p v-if="subtitle" class="truncate body-normal text-gray-700">{{ subtitle }}</p>
73
- <ul v-if="contentFeatures.length" class="mt-3 flex flex-wrap gap-2">
74
- <li v-for="feature in contentFeatures" :key="feature">
75
- <NexxtTooltip>
76
- <div
77
- :class="[ICON_MAP[feature].backgroundColor, ICON_MAP[feature].textColor]"
78
- class="flex size-8 items-center justify-center rounded-full"
79
- >
80
- <NexxtIcon :name="ICON_MAP[feature].icon" />
81
- </div>
82
- <template #tooltip>
83
- <span class="small-normal text-gray-700">{{ featureMessages[feature] }}</span>
84
- </template>
85
- </NexxtTooltip>
86
- </li>
87
- </ul>
88
166
  </div>
167
+
168
+ <!--
169
+ A stretched button instead of a wrapper: it carries the click, the pressed state and the
170
+ focus ring, while the card content above it stays real document structure. It holds the
171
+ lowest stacking level of the layers, so a click on an actions or overlay button lands there
172
+ and not here. The focus ring follows Button.vue (`outline-3` plus offset), so a selected card
173
+ with keyboard focus shows two distinguishable indicators: blue border and ring.
174
+ -->
175
+ <button
176
+ v-if="selectable"
177
+ type="button"
178
+ :aria-label="title"
179
+ :aria-pressed="selected"
180
+ :disabled="disabled || undefined"
181
+ class="absolute inset-0 cursor-pointer rounded-xl focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-500 disabled:cursor-not-allowed"
182
+ @click="onSelect"
183
+ />
184
+
185
+ <div v-if="$slots.actions" class="absolute top-4 right-4 z-20">
186
+ <slot name="actions" :selected="selected" />
187
+ </div>
188
+
189
+ <!--
190
+ Dropped while selectable and disabled: an invisible layer would still hold tabbable buttons,
191
+ and a card that cannot be chosen reveals nothing. A read-only card (no `selectable`) keeps
192
+ its overlay, disabled or not.
193
+ `pointer-events-none` on the layer with `*:pointer-events-auto` on its children lets a click
194
+ on the veil fall through to the select button while the overlay's own buttons keep theirs.
195
+ On a coarse pointer there is no hover to reveal it, so it is always there -- but then without
196
+ veil or blur and shrunk to the image band at the top, because a permanent veil over the title
197
+ and the subtitle drops their contrast below 4.5:1 and a full-height layer would swallow the
198
+ taps on the feature icons.
199
+ jsdom resolves no Tailwind and the storybook project cannot hit-test, so the layering itself
200
+ is asserted by neither suite; only the DOM order is.
201
+ -->
202
+ <div
203
+ v-if="$slots.overlay && !(selectable && disabled)"
204
+ class="pointer-events-none absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 rounded-xl bg-white/60 opacity-0 backdrop-blur-xs transition-opacity duration-200 ease-out *:pointer-events-auto group-focus-within:opacity-100 group-hover:opacity-100 pointer-coarse:inset-x-0 pointer-coarse:top-0 pointer-coarse:bottom-auto pointer-coarse:h-38 pointer-coarse:justify-end pointer-coarse:bg-transparent pointer-coarse:p-4 pointer-coarse:opacity-100 pointer-coarse:backdrop-blur-none"
205
+ >
206
+ <slot name="overlay" :selected="selected" />
207
+ </div>
208
+
209
+ <span
210
+ v-if="selectable && selected && checkmark"
211
+ class="absolute -top-2 -right-2 z-30 flex size-6 items-center justify-center rounded-full bg-cornflower-blue-500 text-white shadow-sm"
212
+ >
213
+ <NexxtIcon name="check" type="solid" class="text-xs" />
214
+ </span>
89
215
  </div>
90
216
  </template>
@@ -60,5 +60,6 @@
60
60
  "NexxtTimelinePhaseblock": "components/organisms/TimelinePhaseblock/TimelinePhaseblock.vue",
61
61
  "NexxtTimelineTrack": "components/organisms/TimelineTrack/TimelineTrack.vue",
62
62
  "NexxtToaster": "components/organisms/Toaster/Toaster.vue",
63
- "NexxtVideoPlayer": "components/organisms/VideoPlayer/VideoPlayer.vue"
63
+ "NexxtVideoPlayer": "components/organisms/VideoPlayer/VideoPlayer.vue",
64
+ "NexxtEditorSettingsBlock": "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue"
64
65
  }
package/src/index.ts CHANGED
@@ -13,6 +13,11 @@ export type { EnergyLabelLetter } from './components/atoms/EnergyLabel/EnergyLab
13
13
  export type { IconType, NexxtIconName } from './components/atoms/Icon/Icon.vue'
14
14
  export type { CustomIconName } from './components/atoms/Icon/customIcons'
15
15
  export type { NexxtTabItem } from './components/molecules/Tabs/Tabs.vue'
16
+ export type {
17
+ NexxtTemplateCardContentFeature,
18
+ NexxtTemplateCardProps,
19
+ NexxtTemplateCardSlots,
20
+ } from './components/molecules/TemplateCard/TemplateCard.vue'
16
21
 
17
22
  export { default as NexxtAnimatedNumber } from './components/atoms/AnimatedNumber/AnimatedNumber.vue'
18
23
  export { default as NexxtAnnouncement } from './components/molecules/Announcement/Announcement.vue'
@@ -31,6 +36,7 @@ export { default as NexxtCustomerJourneyTile } from './components/molecules/Cust
31
36
  export { default as NexxtDatePicker } from './components/organisms/DatePicker/DatePicker.vue'
32
37
  export { default as NexxtDoughnutChart } from './components/molecules/DoughnutChart/DoughnutChart.vue'
33
38
  export { default as NexxtDropdownButton } from './components/organisms/DropdownButton/DropdownButton.vue'
39
+ export { default as NexxtEditorSettingsBlock } from './components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
34
40
  export { default as NexxtEnergyLabel } from './components/atoms/EnergyLabel/EnergyLabel.vue'
35
41
  export { default as NexxtFloatingPanel } from './components/atoms/FloatingPanel/FloatingPanel.vue'
36
42
  export { default as NexxtHeader } from './components/organisms/Header/Header.vue'
package/src/nuxt.ts CHANGED
@@ -92,8 +92,21 @@ const module: NuxtModule<ModuleOptions> = defineNuxtModule<ModuleOptions>({
92
92
  .replace(/\/+/g, '/')
93
93
  .replace(/^\/|\/$/g, '')}/`
94
94
 
95
- nuxt.options.nitro.publicAssets ??= []
96
- nuxt.options.nitro.publicAssets.push({
95
+ // @nuxt/schema 4.x no longer declares `nitro` on NuxtOptions; the typing
96
+ // arrives through module augmentation in @nuxt/nitro-server, which ships
97
+ // with `nuxt` itself. This package depends on @nuxt/kit and @nuxt/schema
98
+ // only — deliberately, to stay free of app coupling — so the augmentation
99
+ // is absent here and the property has to be described locally. At runtime
100
+ // in a consumer the object is always present. Intersecting rather than
101
+ // replacing keeps this compatible with an installation that does have the
102
+ // augmentation: `options` stays the same object, so the mutation below
103
+ // still targets `nuxt.options.nitro`.
104
+ const options = nuxt.options as typeof nuxt.options & {
105
+ nitro: { publicAssets?: { dir: string; baseURL?: string; maxAge?: number }[] }
106
+ }
107
+
108
+ options.nitro.publicAssets ??= []
109
+ options.nitro.publicAssets.push({
97
110
  dir: assetsDir,
98
111
  baseURL,
99
112
  // Content-hashed filenames, so they are safe to cache indefinitely.