@nuxtseo/design-system 0.1.1 → 0.1.3
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/README.md +83 -17
- package/app/components/container/UiAlert.vue +4 -2
- package/app/components/container/UiErrorState.vue +5 -3
- package/app/components/container/UiSectionHeader.vue +12 -2
- package/app/components/container/UiToast.vue +1 -37
- package/app/components/container/UiToaster.vue +1 -11
- package/app/components/container/UiWidgetState.vue +7 -5
- package/app/components/data/UiDataList.vue +6 -4
- package/app/components/data/UiMetricLabel.vue +3 -1
- package/app/components/data/UiStat.vue +1 -60
- package/app/components/data/UiStats.vue +1 -1
- package/app/components/data/UiTable.vue +16 -52
- package/app/components/data/UiTableTd.vue +9 -1
- package/app/components/data/UiTableTh.vue +10 -2
- package/app/components/data/cells/UiTableHeaderCell.vue +1 -1
- package/app/components/element/UiAtmosphere.vue +22 -4
- package/app/components/element/UiConfirmDialog.vue +54 -7
- package/app/components/element/UiOnboardingAnswers.vue +78 -43
- package/app/components/element/UiSkeleton.vue +50 -1
- package/app/components/element/UiTooltip.vue +7 -2
- package/app/components/element/UiTrend.vue +16 -8
- package/app/composables/design-system-config.ts +29 -0
- package/app/composables/useConfirm.ts +31 -3
- package/app/shared/error-state.ts +13 -12
- package/app/shared/failure-copy.ts +53 -0
- package/app/shared/stat.ts +65 -0
- package/app/shared/table.ts +74 -1
- package/app/shared/toast.ts +52 -0
- package/app/shared/widget-failure.ts +3 -2
- package/build/design-system.ts +48 -12
- package/css/global.css +46 -9
- package/nuxt.config.ts +2 -20
- package/package.json +6 -5
- package/build/fonts.ts +0 -39
- package/public/fonts/README.md +0 -33
- package/public/fonts/fira-code-OFL.txt +0 -93
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +0 -93
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
package/README.md
CHANGED
|
@@ -39,13 +39,14 @@ export default defineNuxtConfig({
|
|
|
39
39
|
})
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
- Peers: `nuxt` 4.6+, `vue` 3.5+, `typescript` 5.8 to 6, `@nuxt/ui` 4.11+, `motion-v` 2.4+, `tailwindcss` 4.3+, and `zod` 4. The Vue SFC compiler resolves the layer's prop types through the TypeScript JS API, and TypeScript 7 has none.
|
|
43
|
-
- The layer shares your copy of each peer, so the app runs one Nuxt UI, one Tailwind, one motion-v context, and one zod. pnpm and npm install a missing peer for you. You need no `overrides` for the layer.
|
|
42
|
+
- Peers: `nuxt` 4.6+, `vue` 3.5+, `typescript` 5.8 to 6, `@nuxt/ui` 4.11+, `@nuxt/scripts` 1.3+ or 2, `@vueuse/core` 14 or 15, `motion-v` 2.4+, `tailwindcss` 4.3+, and `zod` 4. The Vue SFC compiler resolves the layer's prop types through the TypeScript JS API, and TypeScript 7 has none. A TypeScript 7 native typecheck of your app still passes over the layer's components.
|
|
43
|
+
- The layer shares your copy of each peer, so the app runs one Nuxt UI, one `@nuxt/scripts`, one [VueUse](https://vueuse.org), one Tailwind, one motion-v context, and one zod. pnpm and npm install a missing peer for you. You need no `overrides` for the layer.
|
|
44
44
|
- If [pnpm](https://pnpm.io) reports ignored build scripts, allow `esbuild` and `@parcel/watcher` under `allowBuilds` in `pnpm-workspace.yaml`.
|
|
45
|
-
- Import components from `#components`. The layer turns auto-imports off, in Nitro too.
|
|
45
|
+
- Import components from `#components`. The layer turns auto-imports off, in Nitro too. If your app turns them back on, the layer's composables and utils stay out of its scan, so a name such as `formatNumber` never shadows your own. Import them by path.
|
|
46
46
|
- Write `app.config.ts` as a plain object. `defineAppConfig` is an auto-import, so the server bundle has no such function.
|
|
47
47
|
- Import other modules by their path: `@nuxtseo/design-system/app/composables/formatting`. A directory with an `index.ts` imports by its name: `@nuxtseo/design-system/shared/icons`, `@nuxtseo/design-system/shared/contracts`, and `@nuxtseo/design-system/types`.
|
|
48
|
-
-
|
|
48
|
+
- To wrap a component, import its props from a `.ts` module, never from the `.vue` file: the SFC compiler cannot resolve a type inside a package `.vue` file. `UiTableProps` and `UiTableSlots` live in `@nuxtseo/design-system/app/shared/table`, `UiStatProps` in `app/shared/stat`, and `UiToastProps` and `UiToasterProps` in `app/shared/toast`.
|
|
49
|
+
- The layer ships no favicon and no font files. Set both through the brand seams below.
|
|
49
50
|
|
|
50
51
|
### Overrides
|
|
51
52
|
|
|
@@ -60,12 +61,12 @@ Same-name shadowing does not work. A component in your `app/components/` with a
|
|
|
60
61
|
|
|
61
62
|
### Releasing
|
|
62
63
|
|
|
63
|
-
The UI packages release on their own `ui-v*` tag, apart from the `public-v*` CLI cohort.
|
|
64
|
+
The UI packages, `@nuxtseo/design-system` and [`@nuxtseo/pro-shell`](../pro-shell/README.md), release together on their own `ui-v*` tag, apart from the `public-v*` CLI cohort. Both carry the same version, and the shell's peer range on this layer is `^X.Y.Z`.
|
|
64
65
|
|
|
65
|
-
1. Set
|
|
66
|
+
1. Set both versions: `pnpm --dir layers/design-system pkg set version=X.Y.Z`, `pnpm --dir layers/pro-shell pkg set version=X.Y.Z`, and the shell's `peerDependencies["@nuxtseo/design-system"]` to `^X.Y.Z`. Merge it to `main` through a pull request.
|
|
66
67
|
2. Check it: `pnpm release:ui:check ui-vX.Y.Z`.
|
|
67
|
-
3. Pack and smoke
|
|
68
|
-
4. Tag the merged commit `ui-vX.Y.Z` and push the tag. `.github/workflows/publish-ui-packages.yml` verifies, packs, smokes, and publishes. A prerelease such as `X.Y.Z-beta.1` goes to the `next` dist-tag.
|
|
68
|
+
3. Pack and smoke them: `pnpm --dir layers/design-system pack --pack-destination /tmp/ui`, `pnpm --dir layers/pro-shell pack --pack-destination /tmp/ui`, then `pnpm release:ui:smoke /tmp/ui/nuxtseo-design-system-X.Y.Z.tgz /tmp/ui/nuxtseo-pro-shell-X.Y.Z.tgz`.
|
|
69
|
+
4. Tag the merged commit `ui-vX.Y.Z` and push the tag. `.github/workflows/publish-ui-packages.yml` verifies, packs, smokes, and publishes both, the design system first. A prerelease such as `X.Y.Z-beta.1` goes to the `next` dist-tag.
|
|
69
70
|
|
|
70
71
|
## Brand seams
|
|
71
72
|
|
|
@@ -95,16 +96,33 @@ export default {
|
|
|
95
96
|
}
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
**Type.** Set `--font-sans`, `--font-display`, `--font-title`, and `--font-mono` in your stylesheet, and register the faces with `fonts.families`. `--font-title` carries page titles (`.text-title` and the `font-title` utility). Its default is the display family. The layer
|
|
99
|
+
**Type.** Set `--font-sans`, `--font-display`, `--font-title`, and `--font-mono` in your stylesheet, and register the faces with `fonts.families`. `--font-title` carries page titles (`.text-title` and the `font-title` utility). Its default is the display family. The layer ships no font files and registers no faces.
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
**Type scale.** Set these properties at `:root` to resize or recase the dense tiers. Unset, each keeps the value in the second column.
|
|
102
|
+
|
|
103
|
+
| Property | Default | Sets |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `--ui-label-size`, `--ui-label-weight`, `--ui-label-case`, `--ui-label-tracking` | `10px`, `600`, `uppercase`, `0.05em` | `.text-label` and `UiMetricLabel` |
|
|
106
|
+
| `--ui-table-cell-size`, `--ui-table-cell-leading` | `--text-xs` and its leading | Body cells of `UiTable` and `UiTableTd` |
|
|
107
|
+
| `--ui-table-head-size`, `--ui-table-head-leading` | `--ui-label-size`, inherited | Header labels of `UiTable` and `UiTableTh` |
|
|
108
|
+
| `--ui-trend-2xs-size`, `--ui-trend-xs-size`, `--ui-trend-xs-leading` | `10px`, `--text-xs` and its leading | `UiTrend` sizes `2xs` and `xs` |
|
|
109
|
+
| `--ui-trend-badge-case` | `uppercase` | `UiTrend` New and Lost |
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
:root {
|
|
113
|
+
--ui-label-size: 0.75rem;
|
|
114
|
+
--ui-label-weight: 500;
|
|
115
|
+
--ui-label-case: none;
|
|
116
|
+
--ui-label-tracking: normal;
|
|
117
|
+
--ui-table-cell-size: var(--text-sm);
|
|
118
|
+
--ui-table-cell-leading: var(--text-sm--line-height);
|
|
119
|
+
--ui-table-head-size: var(--text-sm);
|
|
120
|
+
--ui-trend-badge-case: none;
|
|
121
|
+
}
|
|
106
122
|
```
|
|
107
123
|
|
|
124
|
+
**Surfaces.** The layer sets every `--ui-bg*`, `--ui-text*` and `--ui-border*` token on `.light`, `.dark` and `.dashboard-theme` inside `:where()`, so those rules carry no specificity. Set a surface token in a plain `:root` rule and it wins in both modes; set it in a `.dark` rule to change dark mode only. Source order does not matter.
|
|
125
|
+
|
|
108
126
|
**Own-site links.** An absolute HTTP or HTTPS prose link gets an external-link arrow. List the hosts that count as your site, and links to them get none. Unset, the layer lists no host.
|
|
109
127
|
|
|
110
128
|
```ts
|
|
@@ -134,7 +152,7 @@ export default defineNuxtConfig({
|
|
|
134
152
|
})
|
|
135
153
|
```
|
|
136
154
|
|
|
137
|
-
**Text and dashboard neutrals.** Three light mode text tokens sit on Nuxt SEO's violet hue, off the neutral ramp. The dashboard (`.dashboard-theme`) swaps the neutral ramp to mauve. Set these properties to re-hue them
|
|
155
|
+
**Text and dashboard neutrals.** Three light mode text tokens sit on Nuxt SEO's violet hue, off the neutral ramp. The dashboard (`.dashboard-theme`) swaps the neutral ramp to mauve. Set these properties to re-hue them. Unset, each keeps Nuxt SEO's value.
|
|
138
156
|
|
|
139
157
|
| Property | Sets |
|
|
140
158
|
| --- | --- |
|
|
@@ -177,6 +195,54 @@ export default {
|
|
|
177
195
|
}
|
|
178
196
|
```
|
|
179
197
|
|
|
180
|
-
**Onboarding copy.** `UiOnboardingAnswers` takes an `intents` prop for the goal list and a `copy` prop for its strings: `roleQuestion`, `siteCountQuestion`, `seoExperienceQuestion`, `intentsQuestion`, `intentsHint`, `otherLabel`, and `otherPlaceholder`. Pass any of them. Unset keys keep the strings in `ONBOARDING_ANSWERS_COPY`. Onboarding Answers store the option ids, so the ids never change.
|
|
198
|
+
**Onboarding copy.** `UiOnboardingAnswers` takes an `intents` prop for the goal list and a `copy` prop for its strings: `roleQuestion`, `siteCountQuestion`, `seoExperienceQuestion`, `intentsQuestion`, `intentsHint`, `otherLabel`, and `otherPlaceholder`. Pass any of them. Unset keys keep the strings in `ONBOARDING_ANSWERS_COPY`. An empty `intentsHint` hides the hint. Onboarding Answers store the option ids, so the ids never change.
|
|
199
|
+
|
|
200
|
+
**Onboarding controls.** Two scoped slots draw `UiOnboardingAnswers` in your own look and keep its answer logic. `#option="{ question, option, selected, pick }"` draws one single-choice option; `pick()` selects it, or clears it when it is already picked. `#intents="{ intents, selected, toggle, other, setOther }"` draws the goals and the free-text box. Your heading, Skip, and Save sit around the component, and saving stays with your app: the layer never fetches.
|
|
201
|
+
|
|
202
|
+
```vue
|
|
203
|
+
<UiOnboardingAnswers v-model="answers" :copy="{ intentsHint: '' }">
|
|
204
|
+
<template #option="{ option, selected, pick }">
|
|
205
|
+
<UButton :label="option.label" :variant="selected ? 'soft' : 'outline'" :aria-pressed="selected" @click="pick" />
|
|
206
|
+
</template>
|
|
207
|
+
<template #intents="{ selected, toggle, other, setOther }">
|
|
208
|
+
<MyGoalsGrid :selected="selected" :other="other" @toggle="toggle" @update:other="setOther" />
|
|
209
|
+
</template>
|
|
210
|
+
</UiOnboardingAnswers>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Trend colour.** `UiTrend` draws a change in neutral text unless a caller passes `colored`, because Nuxt SEO spends colour on hero trends only. A site that colours every trend sets the default once:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// app.config.ts
|
|
217
|
+
export default {
|
|
218
|
+
designSystem: { trend: { colored: true } },
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Atmosphere.** `UiAtmosphere` draws no grain and leaves the `bloom` geometry unmasked unless asked. Pass `grain` or `bloom-mask` on one instance, or turn either on for the whole site with `designSystem: { atmosphere: { grain: true, bloomMask: true } }` in `app.config.ts`. Nuxt SEO's own brand layer turns both on.
|
|
223
|
+
|
|
224
|
+
**Failure copy.** `UiWidgetState`, `UiDataList` and `UiErrorState` state a failed read in copy the layer owns (COPY.md, Shared primitive assets). Reword any of it in `app.config.ts`. A failure keeps its retry behaviour; only the words change. Unset keys keep the defaults in `FAILURE_COPY` (`app/shared/failure-copy.ts`).
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// app.config.ts
|
|
228
|
+
export default {
|
|
229
|
+
designSystem: {
|
|
230
|
+
failureCopy: {
|
|
231
|
+
failures: { server: { title: 'We could not load this data' } },
|
|
232
|
+
retryLabel: 'Try again',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Component notes
|
|
239
|
+
|
|
240
|
+
- **`UiTable` cell slots.** A `#<column id>-cell="{ row, cell, value }"` slot draws that column's cells and wins over the column's `cell` renderer. Columns with no slot keep their renderer. `#empty` replaces the zero-rows line.
|
|
241
|
+
- **`UiTableTd` and `UiTableTh`** take `colspan` and `title`.
|
|
242
|
+
- **`UiConfirmDialog`** takes a `#body` slot; `useConfirm()` takes a `body` render function for the same place. Pass `action` to `useConfirm()` and the confirm button runs it with a loading state, the dialog cannot be dismissed meanwhile, and then it closes. A rejected action rejects the `useConfirm()` promise.
|
|
243
|
+
- **`UiAlert`** takes `:animated="false"` to skip its entrance.
|
|
244
|
+
- **`UiSectionHeader size="lg"`** is the long-form page tier: a 24 to 30px heading over a top rule.
|
|
245
|
+
- **`UiSkeleton`** draws whole loading shapes with `type="table-row"`, `"stat"`, `"chart"` or `"card"`; `count` sets how many.
|
|
246
|
+
- **`UiTopEntityStackChart`** has no `yMax` or `bucketTotal`. `scale="share"` fills each bar to 100%, and the tooltip states each bucket's total through `format`.
|
|
181
247
|
|
|
182
248
|
**Motion.** The layer provides motion-v's `domAnimation` features to the whole app, `error.vue` included. A site needs no `<LazyMotion>` wrapper.
|
|
@@ -8,7 +8,7 @@ import { semanticColors } from '../../composables/semanticColors'
|
|
|
8
8
|
import { useEntranceMotion } from '../../composables/useEntranceMotion'
|
|
9
9
|
import { entrancePresets } from '../../shared/motion'
|
|
10
10
|
|
|
11
|
-
const { status = 'info', icon, title } = defineProps<{
|
|
11
|
+
const { status = 'info', icon, title, animated = true } = defineProps<{
|
|
12
12
|
/** Semantic status — drives the accent icon + bar + corner bloom (surface
|
|
13
13
|
* stays neutral for every status). Shared name across the status family
|
|
14
14
|
* (UiChip, UiStatusBadge). */
|
|
@@ -21,6 +21,8 @@ const { status = 'info', icon, title } = defineProps<{
|
|
|
21
21
|
description?: string
|
|
22
22
|
/** Show close/dismiss button */
|
|
23
23
|
dismissible?: boolean
|
|
24
|
+
/** Fade the alert in when the client mounts it. Set false for an alert that must not move, such as one inside a dialog. */
|
|
25
|
+
animated?: boolean
|
|
24
26
|
}>()
|
|
25
27
|
|
|
26
28
|
const emit = defineEmits<{
|
|
@@ -51,7 +53,7 @@ const resolvedIcon = computed(() => icon || defaultIcons[status])
|
|
|
51
53
|
const isUrgent = computed(() => status === 'error' || status === 'warning')
|
|
52
54
|
const dismissLabel = computed(() => title ? `Dismiss alert: ${title}` : 'Dismiss alert')
|
|
53
55
|
|
|
54
|
-
const entrance = useEntranceMotion(entrancePresets.fadeUp)
|
|
56
|
+
const entrance = useEntranceMotion(entrancePresets.fadeUp, () => animated)
|
|
55
57
|
</script>
|
|
56
58
|
|
|
57
59
|
<template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { ErrorStateSource } from '../../shared/error-state'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { UiAlert, UiButton } from '#components'
|
|
5
|
+
import { readFailureCopy } from '../../composables/design-system-config'
|
|
5
6
|
import { resolveErrorState } from '../../shared/error-state'
|
|
6
7
|
|
|
7
8
|
const { error } = defineProps<{
|
|
@@ -16,7 +17,7 @@ const { error } = defineProps<{
|
|
|
16
17
|
* the caught message, which quotes the request URL.
|
|
17
18
|
*/
|
|
18
19
|
error: ErrorStateSource | null
|
|
19
|
-
/** Optional retry handler. When set (and no #action slot), renders a "Try again"
|
|
20
|
+
/** Optional retry handler. When set (and no #action slot), renders a retry button ("Try again" unless app.config rewords it). */
|
|
20
21
|
onRetry?: () => void
|
|
21
22
|
}>()
|
|
22
23
|
|
|
@@ -27,7 +28,8 @@ defineSlots<{
|
|
|
27
28
|
|
|
28
29
|
// `Date.now()` is read once per resolve, the same as the old inline reset
|
|
29
30
|
// maths. The reset hint counts down only when something else re-renders.
|
|
30
|
-
const
|
|
31
|
+
const copy = readFailureCopy()
|
|
32
|
+
const view = computed(() => error ? resolveErrorState(error, Date.now(), copy) : null)
|
|
31
33
|
</script>
|
|
32
34
|
|
|
33
35
|
<template>
|
|
@@ -53,7 +55,7 @@ const view = computed(() => error ? resolveErrorState(error, Date.now()) : null)
|
|
|
53
55
|
purpose="secondary"
|
|
54
56
|
size="xs"
|
|
55
57
|
icon="refresh"
|
|
56
|
-
label="
|
|
58
|
+
:label="copy.errorStateRetryLabel"
|
|
57
59
|
class="mt-2"
|
|
58
60
|
@click="onRetry()"
|
|
59
61
|
/>
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
chat,
|
|
20
20
|
mcp,
|
|
21
21
|
schema,
|
|
22
|
+
size = 'sm',
|
|
22
23
|
} = defineProps<{
|
|
23
24
|
title: string
|
|
24
25
|
description?: string
|
|
@@ -35,14 +36,23 @@ const {
|
|
|
35
36
|
chat?: EjectChat
|
|
36
37
|
mcp?: EjectMcp
|
|
37
38
|
schema?: EjectLink
|
|
39
|
+
/**
|
|
40
|
+
* `sm` is the dashboard tier: the `.text-heading` section heading. `lg` is
|
|
41
|
+
* the long-form page tier: a 24 to 30px heading over a top rule, so page
|
|
42
|
+
* sections keep their hierarchy above an on-page FAQ heading.
|
|
43
|
+
*/
|
|
44
|
+
size?: 'sm' | 'lg'
|
|
38
45
|
}>()
|
|
39
46
|
|
|
40
47
|
const slots = useSlots()
|
|
41
48
|
const hasEject = computed(() => !!(chat || mcp || schema))
|
|
49
|
+
const headingClass = computed(() => size === 'lg'
|
|
50
|
+
? 'font-sans text-2xl font-semibold tracking-[-0.025em] text-default md:text-3xl'
|
|
51
|
+
: 'text-heading text-default')
|
|
42
52
|
</script>
|
|
43
53
|
|
|
44
54
|
<template>
|
|
45
|
-
<div class="flex items-center justify-between gap-3 mb-3">
|
|
55
|
+
<div class="flex items-center justify-between gap-3" :class="size === 'lg' ? 'mb-6 border-t border-accented pt-5' : 'mb-3'">
|
|
46
56
|
<div class="flex items-center gap-2 min-w-0">
|
|
47
57
|
<!-- A section that reads through one Source passes its mark here, in
|
|
48
58
|
place of the decorative icon. -->
|
|
@@ -50,7 +60,7 @@ const hasEject = computed(() => !!(chat || mcp || schema))
|
|
|
50
60
|
<UiNavIcon v-if="icon" :icon="icon" />
|
|
51
61
|
</slot>
|
|
52
62
|
<div class="min-w-0">
|
|
53
|
-
<h2 class="
|
|
63
|
+
<h2 class="flex items-center gap-2" :class="headingClass">
|
|
54
64
|
<slot name="title">
|
|
55
65
|
{{ title }}
|
|
56
66
|
</slot>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Component } from 'vue'
|
|
3
2
|
import type { SemanticStatus } from '../../composables/semanticColors'
|
|
3
|
+
import type { ToastAction, UiToastProps } from '../../shared/toast'
|
|
4
4
|
import { useElementSize } from '@vueuse/core'
|
|
5
5
|
import { computed, useTemplateRef, watch } from 'vue'
|
|
6
6
|
import { UiButton, UiIcon } from '#components'
|
|
@@ -13,42 +13,6 @@ import { semanticColors } from '../../composables/semanticColors'
|
|
|
13
13
|
// renders this inside a `<motion.li>`. Rendered exclusively from the shared
|
|
14
14
|
// useToast() queue, so every existing `toast.add(...)` call site keeps working.
|
|
15
15
|
|
|
16
|
-
export interface ToastAction {
|
|
17
|
-
label: string
|
|
18
|
-
icon?: string
|
|
19
|
-
to?: string
|
|
20
|
-
onClick?: (e: MouseEvent) => void
|
|
21
|
-
/** Visual emphasis. `primary` = filled, `neutral` = quiet. */
|
|
22
|
-
color?: 'primary' | 'neutral'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface UiToastProps {
|
|
26
|
-
/** Heading text (string or a render component). */
|
|
27
|
-
title?: string | Component
|
|
28
|
-
/** Body text (string or a render component). */
|
|
29
|
-
description?: string | Component
|
|
30
|
-
/** Semantic status — drives accent + default icon. Wins over `color`. */
|
|
31
|
-
status?: SemanticStatus
|
|
32
|
-
/** Nuxt UI color compatibility for existing `toast.add({ color })` call sites. */
|
|
33
|
-
color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
|
|
34
|
-
/** Icon override. Defaults to the status icon. */
|
|
35
|
-
icon?: string
|
|
36
|
-
/** Inline action buttons. */
|
|
37
|
-
actions?: ToastAction[]
|
|
38
|
-
/**
|
|
39
|
-
* Bug-report handler. When set, failure toasts (error/warning) that carry no
|
|
40
|
-
* action of their own get a "Report bug" button. Wired by the app (the
|
|
41
|
-
* design system must not reach for the feedback drawer itself).
|
|
42
|
-
*/
|
|
43
|
-
reportBug?: (toast: { title?: string, description?: string }) => void
|
|
44
|
-
/** Show the close button. */
|
|
45
|
-
close?: boolean
|
|
46
|
-
/** Auto-dismiss progress 0..1 (remaining), or false to hide the bar. */
|
|
47
|
-
progress?: number | false
|
|
48
|
-
/** Collapsed back card — fade content so the deck reads as peeking cards. */
|
|
49
|
-
peek?: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
16
|
const {
|
|
53
17
|
title,
|
|
54
18
|
description,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { UiToastProps } from '
|
|
2
|
+
import type { UiToasterProps, UiToastProps } from '../../shared/toast'
|
|
3
3
|
import { useMounted, useRafFn } from '@vueuse/core'
|
|
4
4
|
import { AnimatePresence, motion, useReducedMotion } from 'motion-v'
|
|
5
5
|
import { computed, reactive, ref, watch } from 'vue'
|
|
@@ -15,16 +15,6 @@ import UiToast from './UiToast.vue'
|
|
|
15
15
|
// Nuxt UI's shared useToast() queue, so every existing `toast.add(...)` keeps
|
|
16
16
|
// working unchanged. (Interaction model: brand-kit /toasts → "Fire 5 at once".)
|
|
17
17
|
|
|
18
|
-
export interface UiToasterProps {
|
|
19
|
-
position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
20
|
-
/** Max cards rendered in the deck (older ones collapse into a "+N" pill). */
|
|
21
|
-
max?: number
|
|
22
|
-
/** Default auto-dismiss in ms; a per-toast `duration` overrides. 0 = sticky. */
|
|
23
|
-
duration?: number
|
|
24
|
-
/** Passed to every card: adds "Report bug" to failure toasts. See UiToast. */
|
|
25
|
-
reportBug?: UiToastProps['reportBug']
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
const {
|
|
29
19
|
position = 'bottom-right',
|
|
30
20
|
max = 3,
|
|
@@ -4,7 +4,8 @@ import type { ViewState } from '../../shared/view-state'
|
|
|
4
4
|
import type { WidgetFailure } from '../../shared/widget-failure'
|
|
5
5
|
import { computed, onMounted, ref } from 'vue'
|
|
6
6
|
import { UiButton, UiEmptyState, UiIcon, UiSkeleton } from '#components'
|
|
7
|
-
import {
|
|
7
|
+
import { readFailureCopy } from '../../composables/design-system-config'
|
|
8
|
+
import { toWidgetFailure } from '../../shared/widget-failure'
|
|
8
9
|
|
|
9
10
|
const {
|
|
10
11
|
state,
|
|
@@ -88,8 +89,9 @@ const failure = computed(() => toWidgetFailure(state?._tag === 'error' ? state.e
|
|
|
88
89
|
const data = computed(() => state?._tag === 'data' ? state.data : undefined)
|
|
89
90
|
// A refresh failed while an earlier answer is still on screen. The content
|
|
90
91
|
// stays and a notice says the refresh failed.
|
|
91
|
-
const
|
|
92
|
-
const
|
|
92
|
+
const copy = readFailureCopy()
|
|
93
|
+
const refreshFailure = computed(() => state?._tag === 'data' && state.refreshError ? copy.failures[toWidgetFailure(state.refreshError)._tag] : null)
|
|
94
|
+
const failureCopy = computed(() => copy.failures[failure.value._tag])
|
|
93
95
|
</script>
|
|
94
96
|
|
|
95
97
|
<template>
|
|
@@ -114,7 +116,7 @@ const failureCopy = computed(() => WIDGET_FAILURE_COPY[failure.value._tag])
|
|
|
114
116
|
{{ failureCopy.body }}
|
|
115
117
|
</p>
|
|
116
118
|
<UiButton v-if="failureCopy.retryable" size="xs" purpose="secondary" icon="refresh" @click="emit('retry')">
|
|
117
|
-
|
|
119
|
+
{{ copy.retryLabel }}
|
|
118
120
|
</UiButton>
|
|
119
121
|
</div>
|
|
120
122
|
</slot>
|
|
@@ -133,7 +135,7 @@ const failureCopy = computed(() => WIDGET_FAILURE_COPY[failure.value._tag])
|
|
|
133
135
|
<span class="font-medium text-default">{{ refreshFailure.title }}.</span> {{ refreshFailure.body }}
|
|
134
136
|
</p>
|
|
135
137
|
<UiButton v-if="refreshFailure.retryable" purpose="secondary" size="xs" icon="refresh" @click="emit('retry')">
|
|
136
|
-
|
|
138
|
+
{{ copy.retryLabel }}
|
|
137
139
|
</UiButton>
|
|
138
140
|
</div>
|
|
139
141
|
<slot :data="data" />
|
|
@@ -3,6 +3,7 @@ import type { UiIcon as UiIconName } from '../../shared/ui-icons'
|
|
|
3
3
|
import type { ViewState } from '../../shared/view-state'
|
|
4
4
|
import { computed, onMounted, ref } from 'vue'
|
|
5
5
|
import { NuxtLink, UiButton, UiHelpLabel, UiIcon, UiNavIcon, UiSkeleton } from '#components'
|
|
6
|
+
import { readFailureCopy } from '../../composables/design-system-config'
|
|
6
7
|
import { widgetFailureCopy } from '../../shared/widget-failure'
|
|
7
8
|
import { getIconColor } from '../../utils/icon-color'
|
|
8
9
|
|
|
@@ -113,11 +114,12 @@ const view = computed<ViewState<readonly T[]>>(() => {
|
|
|
113
114
|
})
|
|
114
115
|
const isLoading = computed(() => view.value._tag === 'loading')
|
|
115
116
|
const rows = computed<readonly T[]>(() => view.value._tag === 'data' ? view.value.data : [])
|
|
117
|
+
const copy = readFailureCopy()
|
|
116
118
|
const failure = computed(() => {
|
|
117
119
|
if (view.value._tag === 'error')
|
|
118
|
-
return widgetFailureCopy(view.value.error)
|
|
120
|
+
return widgetFailureCopy(view.value.error, copy.failures)
|
|
119
121
|
if (view.value._tag === 'data' && view.value.refreshError != null)
|
|
120
|
-
return widgetFailureCopy(view.value.refreshError)
|
|
122
|
+
return widgetFailureCopy(view.value.refreshError, copy.failures)
|
|
121
123
|
return null
|
|
122
124
|
})
|
|
123
125
|
|
|
@@ -207,7 +209,7 @@ function itemKey(item: T, index: number): string | number {
|
|
|
207
209
|
{{ failure.body }}
|
|
208
210
|
</p>
|
|
209
211
|
<UiButton v-if="onRetry && failure.retryable" purpose="secondary" size="xs" icon="refresh" class="mt-2" @click="onRetry()">
|
|
210
|
-
|
|
212
|
+
{{ copy.retryLabel }}
|
|
211
213
|
</UiButton>
|
|
212
214
|
</div>
|
|
213
215
|
</div>
|
|
@@ -238,7 +240,7 @@ function itemKey(item: T, index: number): string | number {
|
|
|
238
240
|
<span class="font-medium text-default">{{ failure.title }}.</span> {{ failure.body }}
|
|
239
241
|
</p>
|
|
240
242
|
<UiButton v-if="onRetry && failure.retryable" purpose="secondary" size="xs" icon="refresh" @click="onRetry()">
|
|
241
|
-
|
|
243
|
+
{{ copy.retryLabel }}
|
|
242
244
|
</UiButton>
|
|
243
245
|
</div>
|
|
244
246
|
<div v-if="view._tag === 'data'" class="flex-1 p-1 space-y-0.5">
|
|
@@ -8,9 +8,11 @@ const { size = 'xs', tone = 'dimmed' } = defineProps<{
|
|
|
8
8
|
|
|
9
9
|
// xs = the dense .text-label tier (10px); sm = a 12px label for popover/list
|
|
10
10
|
// section headings. (Both were 10px before — the sm tier was a dead no-op.)
|
|
11
|
+
// Both follow the label case and tracking seam (`--ui-label-case`,
|
|
12
|
+
// `--ui-label-tracking`), so a sentence-case site sets them once.
|
|
11
13
|
const sizeClass = {
|
|
12
14
|
xs: 'text-label',
|
|
13
|
-
sm: 'text-xs
|
|
15
|
+
sm: 'text-xs font-semibold [text-transform:var(--ui-label-case,uppercase)] [letter-spacing:var(--ui-label-tracking,0.05em)]',
|
|
14
16
|
} as const
|
|
15
17
|
|
|
16
18
|
const toneClass = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { UiStatProps } from '../../shared/stat'
|
|
3
3
|
import type { SlotTextOptions } from '../../utils/slot-text'
|
|
4
4
|
import { useElementHover } from '@vueuse/core'
|
|
5
5
|
import { computed, onMounted, ref, useSlots } from 'vue'
|
|
@@ -41,65 +41,6 @@ const {
|
|
|
41
41
|
// NuxtLink auto-registers globally; resolve by name so we don't depend on
|
|
42
42
|
// the `#components` virtual (which doesn't narrow nicely in a layer).
|
|
43
43
|
|
|
44
|
-
type Datum = Record<string, number | string>
|
|
45
|
-
|
|
46
|
-
export interface UiStatProps {
|
|
47
|
-
// link
|
|
48
|
-
to?: string
|
|
49
|
-
|
|
50
|
-
// label
|
|
51
|
-
icon?: UiIconName
|
|
52
|
-
title?: string
|
|
53
|
-
tooltip?: string
|
|
54
|
-
tooltipDescription?: string
|
|
55
|
-
|
|
56
|
-
// value
|
|
57
|
-
value?: string | number | null
|
|
58
|
-
suffix?: string
|
|
59
|
-
valueClass?: string
|
|
60
|
-
format?: (n: number) => string
|
|
61
|
-
|
|
62
|
-
// trend
|
|
63
|
-
trend?: number | null
|
|
64
|
-
/**
|
|
65
|
-
* Formats the trend magnitude. The value `format` never applies: the trend is
|
|
66
|
-
* a percent or a delta, so a currency formatter printed "+$12.5%".
|
|
67
|
-
*/
|
|
68
|
-
trendFormat?: (n: number) => string
|
|
69
|
-
trendSuffix?: string
|
|
70
|
-
trendLabel?: string
|
|
71
|
-
invertTrend?: boolean
|
|
72
|
-
/**
|
|
73
|
-
* Trend color budget (DESIGN.md Composition): colored success/error trends
|
|
74
|
-
* belong to the hero zone only. Default renders neutral; `UiStats
|
|
75
|
-
* variant="cards"` (the hero pattern) opts its stats in automatically.
|
|
76
|
-
*/
|
|
77
|
-
trendColored?: boolean
|
|
78
|
-
|
|
79
|
-
// sparkline
|
|
80
|
-
sparkline?: Datum[]
|
|
81
|
-
|
|
82
|
-
// sizing
|
|
83
|
-
size?: 'sm' | 'md' | 'lg'
|
|
84
|
-
/** Keep report-visible labels and deltas at the 14px readable tier. */
|
|
85
|
-
readableLabels?: boolean
|
|
86
|
-
|
|
87
|
-
// state
|
|
88
|
-
loading?: boolean
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Rolls compact metric values when they change. Defaults on for numeric-ish
|
|
92
|
-
* values; pass false for dense/realtime surfaces or object options to tune.
|
|
93
|
-
*/
|
|
94
|
-
animatedValue?: boolean | SlotTextOptions
|
|
95
|
-
|
|
96
|
-
// threshold alerting
|
|
97
|
-
status?: 'crisis' | 'warning' | 'good'
|
|
98
|
-
|
|
99
|
-
// render the value area inside a card, keeping the header outside
|
|
100
|
-
card?: boolean
|
|
101
|
-
}
|
|
102
|
-
|
|
103
44
|
const hydrated = ref(false)
|
|
104
45
|
onMounted(() => {
|
|
105
46
|
hydrated.value = true
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { UiStatProps } from '
|
|
2
|
+
import type { UiStatProps } from '../../shared/stat'
|
|
3
3
|
// Port of nuxtseo/core UiStats. Swaps tailwind-merge for a plain
|
|
4
4
|
// filter+join — the component doesn't generate conflicting Tailwind classes,
|
|
5
5
|
// and consumer `class` attrs cascade normally via the class attribute merge.
|
|
@@ -8,12 +8,12 @@ import type {
|
|
|
8
8
|
RowSelectionState,
|
|
9
9
|
SortingState,
|
|
10
10
|
} from '@tanstack/vue-table'
|
|
11
|
-
import type { UiTableColumn, UiTableFeatures,
|
|
11
|
+
import type { UiTableColumn, UiTableFeatures, UiTableProps, UiTableSlots } from '../../shared/table'
|
|
12
12
|
import { FlexRender, functionalUpdate, useTable } from '@tanstack/vue-table'
|
|
13
13
|
import { useIntersectionObserver } from '@vueuse/core'
|
|
14
|
-
import { computed, ref, toRef, useId,
|
|
14
|
+
import { computed, ref, toRef, useId, useTemplateRef, watch } from 'vue'
|
|
15
15
|
import { UiButton, UiSkeleton, UiTableExport, UiTableFrame, UiTableHeaderCell, UPagination } from '#components'
|
|
16
|
-
import { resolveUiTableRowId, uiTableCellSizeClass, uiTableFeatures, uiTableSkeletonSizeClass, uiTableVisibleFromClass } from '../../shared/table'
|
|
16
|
+
import { resolveUiTableRowId, uiTableCellSizeClass, uiTableCellSlotName, uiTableFeatures, uiTableSkeletonSizeClass, uiTableVisibleFromClass } from '../../shared/table'
|
|
17
17
|
import { uiTableExport } from '../../shared/table-export'
|
|
18
18
|
|
|
19
19
|
const {
|
|
@@ -51,6 +51,8 @@ const emit = defineEmits<{
|
|
|
51
51
|
'sortColumn': [column: string]
|
|
52
52
|
}>()
|
|
53
53
|
|
|
54
|
+
const slots = defineSlots<UiTableSlots<T>>()
|
|
55
|
+
|
|
54
56
|
const selectedModel = defineModel<RowSelectionState>('selected')
|
|
55
57
|
const pageModel = defineModel<number>('page', { default: 1 })
|
|
56
58
|
const sortingModel = defineModel<SortingState>('sorting', { default: () => [] })
|
|
@@ -170,8 +172,6 @@ function onRowKeydown(e: KeyboardEvent, row: T, tanstackRow: Row<UiTableFeatures
|
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
const slots = useSlots()
|
|
174
|
-
|
|
175
175
|
// Optional trailing actions column (row buttons/menus). Rendered outside the
|
|
176
176
|
// TanStack column model so callers express rich actions as a slot, not a cell
|
|
177
177
|
// render fn. `colSpan` accounts for it in the empty/skeleton rows.
|
|
@@ -283,49 +283,6 @@ if (import.meta.dev) {
|
|
|
283
283
|
}
|
|
284
284
|
</script>
|
|
285
285
|
|
|
286
|
-
<script lang="ts">
|
|
287
|
-
export interface UiTableProps<T extends object> {
|
|
288
|
-
data: T[]
|
|
289
|
-
columns: UiTableColumn<T>[]
|
|
290
|
-
selected?: RowSelectionState
|
|
291
|
-
controlledSelection?: boolean
|
|
292
|
-
rowHover?: boolean
|
|
293
|
-
rowClickable?: boolean
|
|
294
|
-
enableSorting?: boolean
|
|
295
|
-
/** Caller owns sort state; UiTable emits @sortColumn and bypasses client-side sorting. */
|
|
296
|
-
manualSorting?: boolean
|
|
297
|
-
pageSize?: number
|
|
298
|
-
ignoreHeader?: boolean
|
|
299
|
-
size?: UiTableSize
|
|
300
|
-
loading?: boolean
|
|
301
|
-
loadingRows?: number
|
|
302
|
-
rowId?: UiTableRowId<T>
|
|
303
|
-
manualPagination?: boolean
|
|
304
|
-
disablePagination?: boolean
|
|
305
|
-
total?: number
|
|
306
|
-
/** Accessible name for the table. Rendered as a visually-hidden <caption>. */
|
|
307
|
-
label: string
|
|
308
|
-
/** Draw the canonical rounded table surface. */
|
|
309
|
-
bordered?: boolean
|
|
310
|
-
/** Pagination density. Compact renders previous, page position, and next only. */
|
|
311
|
-
paginationPreset?: 'default' | 'compact'
|
|
312
|
-
/** Fill the caller's available height and keep pagination pinned to the bottom. */
|
|
313
|
-
fill?: boolean
|
|
314
|
-
/** Extra classes applied per data row — e.g. a selection highlight. */
|
|
315
|
-
rowClass?: (row: T) => string
|
|
316
|
-
/** Row id presented as the active item in a click-through table. */
|
|
317
|
-
activeRowId?: string
|
|
318
|
-
/** Row id to open when the table first receives deep-linked evidence. */
|
|
319
|
-
initialExpandedRowId?: string
|
|
320
|
-
/**
|
|
321
|
-
* Show Export CSV and Export JSON, saving to this file name without the
|
|
322
|
-
* extension. The export holds every filtered and sorted row, not one page.
|
|
323
|
-
* Ignored with `manualPagination`: mount `UiTableExport` with every row.
|
|
324
|
-
*/
|
|
325
|
-
exportFilename?: string
|
|
326
|
-
}
|
|
327
|
-
</script>
|
|
328
|
-
|
|
329
286
|
<template>
|
|
330
287
|
<div class="w-full" :class="fill && 'flex h-full min-h-0 flex-col'">
|
|
331
288
|
<UiTableExport
|
|
@@ -353,7 +310,7 @@ export interface UiTableProps<T extends object> {
|
|
|
353
310
|
<th
|
|
354
311
|
v-for="header in headerGroup.headers"
|
|
355
312
|
:key="header.id"
|
|
356
|
-
class="text-label text-left whitespace-nowrap border-b border-default bg-default"
|
|
313
|
+
class="text-label ui-table-head text-left whitespace-nowrap border-b border-default bg-default"
|
|
357
314
|
:class="[
|
|
358
315
|
uiColumnDef(header.column).noPadding ? '' : header.column.getCanSort() ? 'px-2' : 'px-3',
|
|
359
316
|
getTextAlignClass(uiColumnDef(header.column).align, uiColumnDef(header.column).numeric),
|
|
@@ -397,7 +354,7 @@ export interface UiTableProps<T extends object> {
|
|
|
397
354
|
v-for="cell in row.getVisibleCells()"
|
|
398
355
|
:key="cell.id"
|
|
399
356
|
:scope="uiColumnDef(cell.column).rowHeader ? 'row' : undefined"
|
|
400
|
-
class="
|
|
357
|
+
class="ui-table-cell font-normal text-default relative"
|
|
401
358
|
:class="[
|
|
402
359
|
uiTableCellSizeClass[size],
|
|
403
360
|
uiColumnDef(cell.column).noPadding ? '' : cell.column.getCanSort() ? 'px-2' : 'px-3',
|
|
@@ -422,6 +379,13 @@ export interface UiTableProps<T extends object> {
|
|
|
422
379
|
:base="60"
|
|
423
380
|
:range="50"
|
|
424
381
|
/>
|
|
382
|
+
<slot
|
|
383
|
+
v-else-if="slots[uiTableCellSlotName(cell.column.id)]"
|
|
384
|
+
:name="uiTableCellSlotName(cell.column.id)"
|
|
385
|
+
:row="row"
|
|
386
|
+
:cell="cell"
|
|
387
|
+
:value="cell.getValue()"
|
|
388
|
+
/>
|
|
425
389
|
<FlexRender v-else :cell="cell" />
|
|
426
390
|
</component>
|
|
427
391
|
<td v-if="hasActions" class="text-right whitespace-nowrap px-3" :class="uiTableCellSizeClass[size]" @click.stop>
|
|
@@ -444,7 +408,7 @@ export interface UiTableProps<T extends object> {
|
|
|
444
408
|
<td
|
|
445
409
|
v-for="(col, j) in columns"
|
|
446
410
|
:key="j"
|
|
447
|
-
class="
|
|
411
|
+
class="ui-table-cell px-3"
|
|
448
412
|
:class="[
|
|
449
413
|
uiTableCellSizeClass[size],
|
|
450
414
|
getTextAlignClass(col.align, col.numeric),
|
|
@@ -458,7 +422,7 @@ export interface UiTableProps<T extends object> {
|
|
|
458
422
|
|
|
459
423
|
<tr v-else>
|
|
460
424
|
<td :colspan="colSpan" class="h-24 text-center" role="status">
|
|
461
|
-
<slot name="empty
|
|
425
|
+
<slot name="empty">
|
|
462
426
|
<span class="text-xs font-mono text-dimmed">0 rows · adjust filters</span>
|
|
463
427
|
</slot>
|
|
464
428
|
</td>
|