nuxt-ui-tools 1.25.0 → 1.25.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 (40) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/display/components/blocks/display-accordion.vue +4 -1
  3. package/dist/runtime/display/components/blocks/display-alert.vue +5 -2
  4. package/dist/runtime/display/components/blocks/display-breakdown.vue +13 -7
  5. package/dist/runtime/display/components/blocks/display-chart.vue +6 -2
  6. package/dist/runtime/display/components/blocks/display-code.vue +2 -1
  7. package/dist/runtime/display/components/blocks/display-date.vue +21 -16
  8. package/dist/runtime/display/components/blocks/display-diff.vue +19 -7
  9. package/dist/runtime/display/components/blocks/display-empty.vue +3 -2
  10. package/dist/runtime/display/components/blocks/display-facts.vue +24 -9
  11. package/dist/runtime/display/components/blocks/display-heading.vue +10 -3
  12. package/dist/runtime/display/components/blocks/display-list.vue +10 -4
  13. package/dist/runtime/display/components/blocks/display-media.vue +19 -9
  14. package/dist/runtime/display/components/blocks/display-meter.vue +12 -11
  15. package/dist/runtime/display/components/blocks/display-person.vue +12 -5
  16. package/dist/runtime/display/components/blocks/display-record.vue +78 -18
  17. package/dist/runtime/display/components/blocks/display-stat.vue +16 -5
  18. package/dist/runtime/display/components/blocks/display-table.vue +4 -0
  19. package/dist/runtime/display/components/blocks/display-text.vue +8 -2
  20. package/dist/runtime/display/components/blocks/display-timeline.vue +7 -1
  21. package/dist/runtime/display/components/parts/display-actions.vue +4 -1
  22. package/dist/runtime/display/components/parts/display-chip.vue +27 -1
  23. package/dist/runtime/display/components/parts/display-fact-value.vue +2 -1
  24. package/dist/runtime/display/components/parts/display-record-media.d.vue.ts +2 -0
  25. package/dist/runtime/display/components/parts/display-record-media.vue +5 -3
  26. package/dist/runtime/display/components/parts/display-record-media.vue.d.ts +2 -0
  27. package/dist/runtime/display/directives/reveal-truncated.d.ts +10 -0
  28. package/dist/runtime/display/directives/reveal-truncated.js +31 -0
  29. package/dist/runtime/display/types/blocks/diff.d.ts +1 -1
  30. package/dist/runtime/display/types/blocks/facts.d.ts +3 -1
  31. package/dist/runtime/display/types/blocks/person.d.ts +3 -1
  32. package/dist/runtime/display/types/blocks/record.d.ts +4 -2
  33. package/dist/runtime/display/types/config.d.ts +9 -2
  34. package/dist/runtime/display/types/shared.d.ts +9 -0
  35. package/dist/runtime/display/utils/config.d.ts +7 -2
  36. package/dist/runtime/display/utils/config.js +11 -1
  37. package/dist/runtime/display/utils/links.d.ts +12 -0
  38. package/dist/runtime/display/utils/links.js +9 -0
  39. package/dist/runtime/form/composables/use-form-root.js +15 -4
  40. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { computed } from "vue";
4
4
  import { isPlainObject } from "../../../shared/utils/predicate";
5
5
  import { useDisplayAppUi } from "../../composables/use-display-config";
6
6
  import { useDisplayNumber } from "../../composables/use-display-number";
7
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
7
8
  import { resolveDisplayClasses } from "../../utils/classes";
8
9
  import { displayContentText, isDisplayContentEmpty, resolveDisplayText } from "../../utils/content";
9
10
  import { meterShare, meterTone } from "../../utils/meter";
@@ -171,16 +172,16 @@ function partTone(partToneName) {
171
172
  </text>
172
173
  </svg>
173
174
  <div :class="classes.parts">
174
- <div v-if="!isDisplayContentEmpty(label)" :class="classes.label">
175
+ <div v-if="!isDisplayContentEmpty(label)" v-reveal-truncated :class="classes.label">
175
176
  <DisplayRender :content="label" />
176
177
  </div>
177
178
  <div v-for="(part, index) in resolvedParts" :key="index" :class="partTone(part.tone)">
178
179
  <div :class="classes.header">
179
- <span :class="classes.label">
180
+ <span v-reveal-truncated :class="classes.label">
180
181
  <UIcon v-if="part.icon" :name="part.icon" class="size-3.5" />
181
182
  <DisplayRender :content="part.label" />
182
183
  </span>
183
- <span :class="classes.value"
184
+ <span v-reveal-truncated :class="classes.value"
184
185
  ><DisplayRender :content="part.valueLabel ?? format(part.value)"
185
186
  /></span>
186
187
  </div>
@@ -191,15 +192,15 @@ function partTone(partToneName) {
191
192
  />
192
193
  </div>
193
194
  </div>
194
- <div v-if="!isDisplayContentEmpty(caption)" :class="classes.caption">
195
+ <div v-if="!isDisplayContentEmpty(caption)" v-reveal-truncated :class="classes.caption">
195
196
  <DisplayRender :content="caption" />
196
197
  </div>
197
198
  </div>
198
199
  </div>
199
200
  <template v-else-if="variant === 'scale'">
200
201
  <div :class="classes.header">
201
- <span :class="classes.label"><DisplayRender :content="label" /></span>
202
- <span v-if="!isDisplayContentEmpty(valueLabel)" :class="classes.value"
202
+ <span v-reveal-truncated :class="classes.label"><DisplayRender :content="label" /></span>
203
+ <span v-if="!isDisplayContentEmpty(valueLabel)" v-reveal-truncated :class="classes.value"
203
204
  ><DisplayRender :content="valueLabel"
204
205
  /></span>
205
206
  </div>
@@ -227,7 +228,7 @@ function partTone(partToneName) {
227
228
  {{ resolveDisplayText(level.caption) }}
228
229
  </span>
229
230
  </div>
230
- <div v-if="!isDisplayContentEmpty(caption)" :class="classes.caption">
231
+ <div v-if="!isDisplayContentEmpty(caption)" v-reveal-truncated :class="classes.caption">
231
232
  <DisplayRender :content="caption" />
232
233
  </div>
233
234
  </template>
@@ -252,14 +253,14 @@ function partTone(partToneName) {
252
253
  :style="{ width: `${part.width}%` }"
253
254
  />
254
255
  </div>
255
- <div v-if="!isDisplayContentEmpty(caption)" :class="classes.caption">
256
+ <div v-if="!isDisplayContentEmpty(caption)" v-reveal-truncated :class="classes.caption">
256
257
  <DisplayRender :content="caption" />
257
258
  </div>
258
259
  </template>
259
260
  <template v-else>
260
261
  <div v-if="!isDisplayContentEmpty(label) || variant === 'bar'" :class="classes.header">
261
- <span :class="classes.label"><DisplayRender :content="label" /></span>
262
- <span :class="classes.value"
262
+ <span v-reveal-truncated :class="classes.label"><DisplayRender :content="label" /></span>
263
+ <span v-reveal-truncated :class="classes.value"
263
264
  ><DisplayRender :content="variant === 'bar' ? headerValue : valueLabel"
264
265
  /></span>
265
266
  </div>
@@ -293,7 +294,7 @@ function partTone(partToneName) {
293
294
  /></b>
294
295
  </span>
295
296
  </div>
296
- <div v-if="!isDisplayContentEmpty(caption)" :class="classes.caption">
297
+ <div v-if="!isDisplayContentEmpty(caption)" v-reveal-truncated :class="classes.caption">
297
298
  <DisplayRender :content="caption" />
298
299
  </div>
299
300
  </template>
@@ -4,8 +4,10 @@ import UIcon from "@nuxt/ui/components/Icon.vue";
4
4
  import ULink from "@nuxt/ui/components/Link.vue";
5
5
  import { computed } from "vue";
6
6
  import { useDisplayAppUi } from "../../composables/use-display-config";
7
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
7
8
  import { resolveDisplayClasses } from "../../utils/classes";
8
9
  import { isDisplayContentEmpty, resolveDisplayText } from "../../utils/content";
10
+ import { displayLinkAttrs } from "../../utils/links";
9
11
  import {
10
12
  DISPLAY_SURFACE_CLASSES,
11
13
  displayToneClass,
@@ -110,7 +112,7 @@ function avatarOf(entry) {
110
112
  :ui="{ fallback: 'font-semibold text-toned', root: 'bg-elevated' }"
111
113
  />
112
114
  </div>
113
- <span v-if="!isDisplayContentEmpty(label)" :class="classes.label"
115
+ <span v-if="!isDisplayContentEmpty(label)" v-reveal-truncated :class="classes.label"
114
116
  ><DisplayRender :content="label"
115
117
  /></span>
116
118
  </div>
@@ -121,8 +123,12 @@ function avatarOf(entry) {
121
123
  :size="AVATAR_SIZES[variant === 'contact' ? 'contact' : 'user'][size]"
122
124
  />
123
125
  <div :class="classes.body">
124
- <div :class="classes.name">{{ resolveDisplayText(self.name) }}</div>
125
- <div v-if="!isDisplayContentEmpty(self.description)" :class="classes.description">
126
+ <div v-reveal-truncated :class="classes.name">{{ resolveDisplayText(self.name) }}</div>
127
+ <div
128
+ v-if="!isDisplayContentEmpty(self.description)"
129
+ v-reveal-truncated
130
+ :class="classes.description"
131
+ >
126
132
  <DisplayRender :content="self.description" />
127
133
  </div>
128
134
  </div>
@@ -133,12 +139,13 @@ function avatarOf(entry) {
133
139
  <UIcon v-if="contact.icon" :name="contact.icon" :class="classes.contactIcon" />
134
140
  <ULink
135
141
  v-if="contact.href"
136
- :to="contact.href"
142
+ v-bind="displayLinkAttrs(contact.href, contact.target)"
143
+ v-reveal-truncated
137
144
  :class="[classes.contactValue, 'hover:text-highlighted']"
138
145
  >
139
146
  {{ contact.value }}
140
147
  </ULink>
141
- <span v-else :class="classes.contactValue">{{ contact.value }}</span>
148
+ <span v-else v-reveal-truncated :class="classes.contactValue">{{ contact.value }}</span>
142
149
  <DisplayCopy v-if="contact.copy !== false" :value="contact.value" class="-my-1" />
143
150
  </div>
144
151
  </div>
@@ -4,8 +4,10 @@ import ULink from "@nuxt/ui/components/Link.vue";
4
4
  import { computed } from "vue";
5
5
  import { useDisplayAppUi } from "../../composables/use-display-config";
6
6
  import { useDisplayReveal } from "../../composables/use-display-reveal";
7
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
7
8
  import { resolveDisplayClasses } from "../../utils/classes";
8
9
  import { isDisplayContentEmpty } from "../../utils/content";
10
+ import { displayLinkAttrs } from "../../utils/links";
9
11
  import { DISPLAY_SURFACE_CLASSES } from "../../utils/tone";
10
12
  import DisplayChip from "../parts/display-chip.vue";
11
13
  import DisplayEmptyLine from "../parts/display-empty-line.vue";
@@ -47,6 +49,7 @@ const {
47
49
  trailingCaption: { type: [String, Number, Function, Object, Boolean, null, Array], required: false, skipCheck: true },
48
50
  chip: { type: Object, required: false },
49
51
  to: { type: String, required: false },
52
+ target: { type: [String, Object, null], required: false },
50
53
  onClick: { type: Function, required: false },
51
54
  max: { type: Number, required: false },
52
55
  step: { type: Number, required: false },
@@ -65,6 +68,7 @@ const classes = computed(
65
68
  header: "flex items-center gap-2 border-b border-default bg-elevated px-3 py-2 text-sm font-semibold text-toned",
66
69
  icon: "size-4 shrink-0 text-dimmed",
67
70
  item: "flex items-center gap-2.5 border-t border-muted px-3 py-2.5 first:border-t-0",
71
+ avatar: "",
68
72
  list: "",
69
73
  media: "",
70
74
  meta: "mt-0.5 flex min-w-0 items-center gap-1 text-xs text-muted",
@@ -81,7 +85,7 @@ const classes = computed(
81
85
  )
82
86
  );
83
87
  function linkProps(item) {
84
- if (item.to) return { is: ULink, to: item.to };
88
+ if (item.to) return { is: ULink, ...displayLinkAttrs(item.to, item.target) };
85
89
  if (item.onClick) return { is: "button", type: "button", onClick: item.onClick };
86
90
  return { is: "div" };
87
91
  }
@@ -116,17 +120,54 @@ function linkProps(item) {
116
120
  item.to || item.onClick ? 'w-full text-start hover:bg-elevated' : ''
117
121
  ]"
118
122
  >
119
- <DisplayRecordMedia :item="item" size="sm" />
123
+ <DisplayRecordMedia
124
+ :item="item"
125
+ size="sm"
126
+ :class="classes.media"
127
+ :avatar-class="classes.avatar"
128
+ />
120
129
  <div :class="classes.body">
121
- <div :class="[classes.title, 'text-sm']"><DisplayRender :content="item.title" /></div>
122
- <div v-if="!isDisplayContentEmpty(item.meta)" :class="classes.meta">
130
+ <div
131
+ v-if="!isDisplayContentEmpty(item.reference)"
132
+ v-reveal-truncated
133
+ :class="classes.reference"
134
+ >
135
+ <DisplayRender :content="item.reference" />
136
+ </div>
137
+ <div v-reveal-truncated :class="[classes.title, 'text-sm']">
138
+ <DisplayRender :content="item.title" />
139
+ </div>
140
+ <div v-if="!isDisplayContentEmpty(item.meta)" v-reveal-truncated :class="classes.meta">
123
141
  <UIcon v-if="item.metaIcon" :name="item.metaIcon" class="size-3 shrink-0" />
124
- <span class="truncate"><DisplayRender :content="item.meta" /></span>
142
+ <span v-reveal-truncated class="truncate"><DisplayRender :content="item.meta" /></span>
143
+ </div>
144
+ <div
145
+ v-if="!isDisplayContentEmpty(item.description)"
146
+ v-reveal-truncated
147
+ :class="classes.description"
148
+ >
149
+ <DisplayRender :content="item.description" />
150
+ </div>
151
+ <div v-if="item.badges?.length" :class="classes.badges">
152
+ <DisplayChip
153
+ v-for="(badge, badgeIndex) in item.badges"
154
+ :key="badgeIndex"
155
+ :chip="badge"
156
+ />
125
157
  </div>
126
158
  </div>
127
159
  <DisplayChip v-if="item.chip" :chip="item.chip" />
128
- <div v-else-if="!isDisplayContentEmpty(item.trailing)" :class="classes.trailing">
129
- <div :class="classes.trailingValue"><DisplayRender :content="item.trailing" /></div>
160
+ <div v-if="!isDisplayContentEmpty(item.trailing)" :class="classes.trailing">
161
+ <div v-reveal-truncated :class="classes.trailingValue">
162
+ <DisplayRender :content="item.trailing" />
163
+ </div>
164
+ <div
165
+ v-if="!isDisplayContentEmpty(item.trailingCaption)"
166
+ v-reveal-truncated
167
+ :class="classes.trailingCaption"
168
+ >
169
+ <DisplayRender :content="item.trailingCaption" />
170
+ </div>
130
171
  </div>
131
172
  </component>
132
173
  <DisplayMore
@@ -147,18 +188,26 @@ function linkProps(item) {
147
188
  >
148
189
  <DisplayRecordMedia :item="single" banner :class="classes.media" />
149
190
  <div class="p-3.5">
150
- <div v-if="!isDisplayContentEmpty(single.reference)" :class="classes.reference">
191
+ <div
192
+ v-if="!isDisplayContentEmpty(single.reference)"
193
+ v-reveal-truncated
194
+ :class="classes.reference"
195
+ >
151
196
  <DisplayRender :content="single.reference" />
152
197
  </div>
153
- <div :class="classes.title"><DisplayRender :content="single.title" /></div>
154
- <div v-if="!isDisplayContentEmpty(single.meta)" :class="classes.meta">
198
+ <div v-reveal-truncated :class="classes.title"><DisplayRender :content="single.title" /></div>
199
+ <div v-if="!isDisplayContentEmpty(single.meta)" v-reveal-truncated :class="classes.meta">
155
200
  <UIcon v-if="single.metaIcon" :name="single.metaIcon" class="size-3 shrink-0" />
156
- <span class="truncate"><DisplayRender :content="single.meta" /></span>
201
+ <span v-reveal-truncated class="truncate"><DisplayRender :content="single.meta" /></span>
157
202
  </div>
158
203
  <div v-if="single.badges?.length" :class="classes.badges">
159
204
  <DisplayChip v-for="(badge, index) in single.badges" :key="index" :chip="badge" />
160
205
  </div>
161
- <div v-if="!isDisplayContentEmpty(single.description)" :class="classes.description">
206
+ <div
207
+ v-if="!isDisplayContentEmpty(single.description)"
208
+ v-reveal-truncated
209
+ :class="classes.description"
210
+ >
162
211
  <DisplayRender :content="single.description" />
163
212
  </div>
164
213
  </div>
@@ -177,16 +226,21 @@ function linkProps(item) {
177
226
  <DisplayRecordMedia
178
227
  :item="single"
179
228
  :size="variant === 'compact' ? 'sm' : 'lg'"
229
+ :avatar-class="classes.avatar"
180
230
  :class="classes.media"
181
231
  />
182
232
  <div :class="classes.body">
183
- <div v-if="!isDisplayContentEmpty(single.reference)" :class="classes.reference">
233
+ <div
234
+ v-if="!isDisplayContentEmpty(single.reference)"
235
+ v-reveal-truncated
236
+ :class="classes.reference"
237
+ >
184
238
  <DisplayRender :content="single.reference" />
185
239
  </div>
186
- <div :class="classes.title"><DisplayRender :content="single.title" /></div>
187
- <div v-if="!isDisplayContentEmpty(single.meta)" :class="classes.meta">
240
+ <div v-reveal-truncated :class="classes.title"><DisplayRender :content="single.title" /></div>
241
+ <div v-if="!isDisplayContentEmpty(single.meta)" v-reveal-truncated :class="classes.meta">
188
242
  <UIcon v-if="single.metaIcon" :name="single.metaIcon" class="size-3 shrink-0" />
189
- <span class="truncate"><DisplayRender :content="single.meta" /></span>
243
+ <span v-reveal-truncated class="truncate"><DisplayRender :content="single.meta" /></span>
190
244
  </div>
191
245
  <div v-if="single.badges?.length && variant === 'row'" :class="classes.badges">
192
246
  <DisplayChip v-for="(badge, index) in single.badges" :key="index" :chip="badge" />
@@ -194,8 +248,14 @@ function linkProps(item) {
194
248
  </div>
195
249
  <DisplayChip v-if="single.chip" :chip="single.chip" />
196
250
  <div v-if="!isDisplayContentEmpty(single.trailing)" :class="classes.trailing">
197
- <div :class="classes.trailingValue"><DisplayRender :content="single.trailing" /></div>
198
- <div v-if="!isDisplayContentEmpty(single.trailingCaption)" :class="classes.trailingCaption">
251
+ <div v-reveal-truncated :class="classes.trailingValue">
252
+ <DisplayRender :content="single.trailing" />
253
+ </div>
254
+ <div
255
+ v-if="!isDisplayContentEmpty(single.trailingCaption)"
256
+ v-reveal-truncated
257
+ :class="classes.trailingCaption"
258
+ >
199
259
  <DisplayRender :content="single.trailingCaption" />
200
260
  </div>
201
261
  </div>
@@ -3,6 +3,7 @@ import UIcon from "@nuxt/ui/components/Icon.vue";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import { computed } from "vue";
5
5
  import { useDisplayAppUi } from "../../composables/use-display-config";
6
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
6
7
  import { resolveDisplayClasses } from "../../utils/classes";
7
8
  import { isDisplayContentEmpty, resolveDisplayText } from "../../utils/content";
8
9
  import {
@@ -117,11 +118,17 @@ const classes = computed(
117
118
  :key="index"
118
119
  :class="[classes.item, item.tone ? displayToneClass(item.tone) : '']"
119
120
  >
120
- <div :class="classes.itemLabel"><DisplayRender :content="item.label" /></div>
121
+ <div v-reveal-truncated :class="classes.itemLabel">
122
+ <DisplayRender :content="item.label" />
123
+ </div>
121
124
  <div :class="twMerge(classes.itemValue, item.tone ? 'text-[var(--nut-tone-ink)]' : '')">
122
125
  <DisplayRender :content="item.value" />
123
126
  </div>
124
- <div v-if="!isDisplayContentEmpty(item.caption)" :class="classes.itemCaption">
127
+ <div
128
+ v-if="!isDisplayContentEmpty(item.caption)"
129
+ v-reveal-truncated
130
+ :class="classes.itemCaption"
131
+ >
125
132
  <DisplayRender :content="item.caption" />
126
133
  </div>
127
134
  </div>
@@ -134,11 +141,11 @@ const classes = computed(
134
141
  :data-variant="variant"
135
142
  >
136
143
  <div :class="variant === 'trend' ? 'min-w-0' : 'contents'">
137
- <div v-if="!isDisplayContentEmpty(label)" :class="classes.label">
144
+ <div v-if="!isDisplayContentEmpty(label)" v-reveal-truncated :class="classes.label">
138
145
  <DisplayRender :content="label" />
139
146
  </div>
140
147
  <div :class="variant === 'trend' ? 'flex items-center gap-2' : 'contents'">
141
- <div :class="classes.value">
148
+ <div v-reveal-truncated :class="classes.value">
142
149
  <DisplayRender :content="value" />
143
150
  <span v-if="!isDisplayContentEmpty(unit)" :class="classes.unit"
144
151
  ><DisplayRender :content="unit"
@@ -152,7 +159,11 @@ const classes = computed(
152
159
  <UIcon v-if="status.icon" :name="status.icon" class="size-4 shrink-0" />
153
160
  <DisplayRender :content="status.text" />
154
161
  </div>
155
- <div v-if="!isDisplayContentEmpty(caption) || chip" :class="classes.caption">
162
+ <div
163
+ v-if="!isDisplayContentEmpty(caption) || chip"
164
+ v-reveal-truncated
165
+ :class="classes.caption"
166
+ >
156
167
  <DisplayChip v-if="chip" :chip="chip" />
157
168
  <DisplayRender :content="caption" />
158
169
  </div>
@@ -3,6 +3,7 @@ import { computed } from "vue";
3
3
  import { isNumber } from "../../../shared/utils/predicate";
4
4
  import { useDisplayAppUi } from "../../composables/use-display-config";
5
5
  import { useDisplayReveal } from "../../composables/use-display-reveal";
6
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
6
7
  import { resolveDisplayClasses } from "../../utils/classes";
7
8
  import { isDisplayContentEmpty } from "../../utils/content";
8
9
  import { DISPLAY_SURFACE_CLASSES } from "../../utils/tone";
@@ -84,6 +85,7 @@ function alignClass(column) {
84
85
  v-for="column in columns"
85
86
  :key="column.key"
86
87
  scope="col"
88
+ v-reveal-truncated
87
89
  :class="[classes.headCell, ...alignClass(column)]"
88
90
  :style="column.width ? { width: column.width } : void 0"
89
91
  >
@@ -96,6 +98,7 @@ function alignClass(column) {
96
98
  <td
97
99
  v-for="column in columns"
98
100
  :key="column.key"
101
+ v-reveal-truncated
99
102
  :class="[classes.cell, ...alignClass(column)]"
100
103
  >
101
104
  <DisplayRender :content="row[column.key]" />
@@ -123,6 +126,7 @@ function alignClass(column) {
123
126
  />
124
127
  <div
125
128
  v-if="!isDisplayContentEmpty(caption) || !isDisplayContentEmpty(note)"
129
+ v-reveal-truncated
126
130
  :class="classes.meta"
127
131
  >
128
132
  <span><DisplayRender :content="caption" /></span>
@@ -4,6 +4,7 @@ import { useResizeObserver } from "@vueuse/core";
4
4
  import { computed, ref, useTemplateRef } from "vue";
5
5
  import { useDisplayAppUi } from "../../composables/use-display-config";
6
6
  import { useDisplayLocale } from "../../composables/use-display-locale";
7
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
7
8
  import { resolveDisplayClasses } from "../../utils/classes";
8
9
  import { isDisplayContentEmpty } from "../../utils/content";
9
10
  import { displayToneClass } from "../../utils/tone";
@@ -104,11 +105,16 @@ useResizeObserver(body, () => {
104
105
  :class="classes.header"
105
106
  >
106
107
  <UIcon v-if="icon" :name="icon" :class="classes.icon" />
107
- <span :class="classes.title"><DisplayRender :content="title" /></span>
108
+ <span v-reveal-truncated :class="classes.title"><DisplayRender :content="title" /></span>
108
109
  </div>
109
110
  <UIcon v-if="appearance === 'caption' && icon" :name="icon" :class="classes.icon" />
110
111
  <div ref="body" :style="clampStyle">
111
- <div v-for="(content, index) in blocks" :key="index" :class="classes.paragraph">
112
+ <div
113
+ v-for="(content, index) in blocks"
114
+ :key="index"
115
+ v-reveal-truncated
116
+ :class="classes.paragraph"
117
+ >
112
118
  <DisplayRender :content="content" />
113
119
  </div>
114
120
  </div>
@@ -6,6 +6,7 @@ import { useDisplayAppUi } from "../../composables/use-display-config";
6
6
  import { useDisplayIcons } from "../../composables/use-display-icons";
7
7
  import { useDisplayLocale } from "../../composables/use-display-locale";
8
8
  import { useDisplayReveal } from "../../composables/use-display-reveal";
9
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
9
10
  import { resolveDisplayClasses } from "../../utils/classes";
10
11
  import { isDisplayContentEmpty, resolveDisplayText } from "../../utils/content";
11
12
  import { formatDisplayDate, toDisplayDate } from "../../utils/dates";
@@ -171,6 +172,7 @@ function actorAvatar(name) {
171
172
  <template v-else>{{ index + 1 }}</template>
172
173
  </span>
173
174
  <span
175
+ v-reveal-truncated
174
176
  :class="[classes.title, entry.resolvedState === 'upcoming' ? 'font-medium text-muted' : '']"
175
177
  >
176
178
  <DisplayRender :content="entry.title" />
@@ -198,12 +200,16 @@ function actorAvatar(name) {
198
200
  />
199
201
  <span :class="[classes.marker, dotClass(entry.resolvedState)]" aria-hidden="true" />
200
202
  <div :class="classes.body">
201
- <div :class="[classes.title, entry.resolvedState === 'upcoming' ? 'text-muted' : '']">
203
+ <div
204
+ v-reveal-truncated
205
+ :class="[classes.title, entry.resolvedState === 'upcoming' ? 'text-muted' : '']"
206
+ >
202
207
  <span class="min-w-0"><DisplayRender :content="entry.title" /></span>
203
208
  <time v-if="entry.timeLabel" :class="classes.time">{{ entry.timeLabel }}</time>
204
209
  </div>
205
210
  <div
206
211
  v-if="entry.actor || !isDisplayContentEmpty(entry.description)"
212
+ v-reveal-truncated
207
213
  :class="classes.description"
208
214
  >
209
215
  <span v-if="entry.actor" :class="classes.actor">
@@ -2,6 +2,7 @@
2
2
  import UButton from "@nuxt/ui/components/Button.vue";
3
3
  import { computed } from "vue";
4
4
  import { resolveDisplayText } from "../../utils/content";
5
+ import { isExternalLink } from "../../utils/links";
5
6
  const { actions, size = "sm" } = defineProps({
6
7
  actions: { type: Array, required: true },
7
8
  size: { type: null, required: false }
@@ -12,7 +13,9 @@ const buttons = computed(
12
13
  size,
13
14
  variant: "outline",
14
15
  ...action,
15
- label: resolveDisplayText(label)
16
+ label: resolveDisplayText(label),
17
+ // A link out of the app opens in a new tab unless the action sets its own target.
18
+ target: action.target === void 0 && isExternalLink(action.to) ? "_blank" : action.target
16
19
  }))
17
20
  );
18
21
  </script>
@@ -1,6 +1,10 @@
1
1
  <script setup>
2
2
  import UBadge from "@nuxt/ui/components/Badge.vue";
3
+ import { twMerge } from "tailwind-merge";
3
4
  import { computed } from "vue";
5
+ import { isString } from "../../../shared/utils/predicate";
6
+ import { useDisplayConfig } from "../../composables/use-display-config";
7
+ import { vRevealTruncated } from "../../directives/reveal-truncated";
4
8
  import { resolveDisplayText } from "../../utils/content";
5
9
  import { displayToneClass, isBuiltinTone } from "../../utils/tone";
6
10
  import DisplayRender from "./display-render";
@@ -20,6 +24,15 @@ const VARIANT_CLASSES = [
20
24
  "bg-[var(--nut-tone-soft)] text-[var(--nut-tone-ink)] ring ring-inset ring-[var(--nut-tone-line)]"
21
25
  ]
22
26
  ];
27
+ const CHIP_SLOTS = [
28
+ "base",
29
+ "label",
30
+ "leadingIcon",
31
+ "leadingAvatar",
32
+ "leadingAvatarSize",
33
+ "trailingIcon"
34
+ ];
35
+ const config = useDisplayConfig();
23
36
  const badge = computed(() => {
24
37
  const { label, tone, ...rest } = chip;
25
38
  const variant = rest.variant ?? "soft";
@@ -30,16 +43,29 @@ const badge = computed(() => {
30
43
  color: custom ? "neutral" : rest.color ?? tone ?? "neutral",
31
44
  label: content === void 0 ? resolveDisplayText(label) : void 0,
32
45
  size: rest.size ?? "sm",
46
+ ui: chipUi(rest.ui),
33
47
  variant
34
48
  };
35
49
  });
50
+ function chipUi(own) {
51
+ const app = config.value.ui?.chip;
52
+ if (!app) return own;
53
+ const merged = { ...own };
54
+ for (const slot of CHIP_SLOTS) {
55
+ const appClass = app[slot];
56
+ const ownClass = own?.[slot];
57
+ if (appClass !== void 0 && (ownClass === void 0 || isString(ownClass)))
58
+ merged[slot] = twMerge(appClass, ownClass);
59
+ }
60
+ return merged;
61
+ }
36
62
  function variantClass(variant) {
37
63
  return VARIANT_CLASSES.find(([name]) => name === variant)?.[1] ?? SOFT;
38
64
  }
39
65
  </script>
40
66
 
41
67
  <template>
42
- <UBadge v-bind="badge" :data-tone="chip.tone">
68
+ <UBadge v-reveal-truncated v-bind="badge" :data-tone="chip.tone">
43
69
  <DisplayRender v-if="content !== void 0" :content="content" />
44
70
  </UBadge>
45
71
  </template>
@@ -3,6 +3,7 @@ import ULink from "@nuxt/ui/components/Link.vue";
3
3
  import { computed } from "vue";
4
4
  import { isString } from "../../../shared/utils/predicate";
5
5
  import { displayContentText, isDisplayContentEmpty } from "../../utils/content";
6
+ import { displayLinkAttrs } from "../../utils/links";
6
7
  import DisplayChip from "./display-chip.vue";
7
8
  import DisplayCopy from "./display-copy.vue";
8
9
  import DisplayRender from "./display-render";
@@ -25,7 +26,7 @@ const copyValue = computed(
25
26
  <span v-else-if="isDisplayContentEmpty(fact.value)" class="font-medium text-dimmed">—</span>
26
27
  <ULink
27
28
  v-else-if="fact.to"
28
- :to="fact.to"
29
+ v-bind="displayLinkAttrs(fact.to, fact.target)"
29
30
  class="underline decoration-[var(--ui-border-accented)] underline-offset-2 hover:decoration-current"
30
31
  >
31
32
  <DisplayRender :content="fact.value" />
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  item: DisplayRecordItem;
4
4
  size?: 'sm' | 'md' | 'lg';
5
5
  banner?: boolean;
6
+ /** Classes of the avatar, from the record's `ui.avatar`. */
7
+ avatarClass?: string;
6
8
  };
7
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
10
  declare const _default: typeof __VLS_export;
@@ -6,11 +6,13 @@ import { displayToneClass } from "../../utils/tone";
6
6
  const {
7
7
  item,
8
8
  size = "md",
9
- banner = false
9
+ banner = false,
10
+ avatarClass = void 0
10
11
  } = defineProps({
11
12
  item: { type: Object, required: true },
12
13
  size: { type: String, required: false },
13
- banner: { type: Boolean, required: false }
14
+ banner: { type: Boolean, required: false },
15
+ avatarClass: { type: String, required: false }
14
16
  });
15
17
  const BOX = { lg: "size-13", md: "size-10", sm: "size-8" };
16
18
  const ICON = { lg: "size-6", md: "size-5", sm: "size-4" };
@@ -40,7 +42,7 @@ watch(
40
42
  v-else-if="item.avatar && !banner"
41
43
  v-bind="item.avatar"
42
44
  :size="item.avatar.size ?? AVATAR[size]"
43
- class="shrink-0"
45
+ :class="['shrink-0', avatarClass]"
44
46
  />
45
47
  <span
46
48
  v-else-if="item.icon || item.image || banner"
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  item: DisplayRecordItem;
4
4
  size?: 'sm' | 'md' | 'lg';
5
5
  banner?: boolean;
6
+ /** Classes of the avatar, from the record's `ui.avatar`. */
7
+ avatarClass?: string;
6
8
  };
7
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
10
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,10 @@
1
+ import type { Directive } from 'vue';
2
+ /**
3
+ * Shows the full text of a clipped element on hover and focus. When the element or one of its
4
+ * descendants cuts its text (`truncate`, `line-clamp`, or any `ui` class that clips), the element
5
+ * gets a `title` with the whole text; the title goes away once nothing is cut. An authored `title`
6
+ * is kept.
7
+ */
8
+ export declare const vRevealTruncated: Directive<HTMLElement>;
9
+ /** Sets or clears the element's `title` from whether its text is cut right now. */
10
+ export declare function revealTruncated(element: HTMLElement): void;
@@ -0,0 +1,31 @@
1
+ const OWNED = "data-display-title";
2
+ export const vRevealTruncated = {
3
+ beforeUnmount(element) {
4
+ element.removeEventListener("pointerenter", onReveal);
5
+ element.removeEventListener("focusin", onReveal);
6
+ },
7
+ mounted(element) {
8
+ element.addEventListener("pointerenter", onReveal);
9
+ element.addEventListener("focusin", onReveal);
10
+ }
11
+ };
12
+ function onReveal(event) {
13
+ if (event.currentTarget instanceof HTMLElement) revealTruncated(event.currentTarget);
14
+ }
15
+ export function revealTruncated(element) {
16
+ if (element.hasAttribute("title") && !element.hasAttribute(OWNED)) return;
17
+ const text = element.textContent?.replaceAll(/\s+/gu, " ").trim() ?? "";
18
+ if (text && [element, ...element.querySelectorAll("*")].some(isClipped)) {
19
+ element.setAttribute("title", text);
20
+ element.setAttribute(OWNED, "");
21
+ return;
22
+ }
23
+ if (!element.hasAttribute(OWNED)) return;
24
+ element.removeAttribute("title");
25
+ element.removeAttribute(OWNED);
26
+ }
27
+ function isClipped(element) {
28
+ const style = getComputedStyle(element);
29
+ const cuts = style.textOverflow === "ellipsis" || style.webkitLineClamp !== "" && style.webkitLineClamp !== "none" || style.overflowX === "hidden" || style.overflowY === "hidden";
30
+ return cuts && (element.scrollWidth > element.clientWidth + 1 || element.scrollHeight > element.clientHeight + 1);
31
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DisplayBlockBase, DisplayChip, DisplayContent } from '../shared.js';
2
- export type DisplayDiffSlot = 'root' | 'title' | 'item' | 'label' | 'values' | 'before' | 'after' | 'arrow' | 'note' | 'unchanged' | 'table' | 'headCell' | 'cell';
2
+ export type DisplayDiffSlot = 'root' | 'title' | 'item' | 'label' | 'values' | 'before' | 'after' | 'arrow' | 'note' | 'unchanged' | 'frame' | 'table' | 'headCell' | 'cell';
3
3
  export interface DisplayDiffItem {
4
4
  label: DisplayContent;
5
5
  before?: DisplayContent;