@nuxtseo/design-system 0.1.0 → 0.1.2
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 +153 -7
- package/app/app.config.ts +6 -10
- 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/UiDataTableSection.vue +1 -0
- 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 +95 -58
- package/app/components/element/UiSelect.vue +15 -2
- 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/components/element/onboarding-answers.ts +25 -0
- package/app/composables/design-system-config.ts +29 -0
- package/app/composables/useConfirm.ts +31 -3
- package/app/plugins/favicon.ts +7 -21
- 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/app/utils/app-favicon.ts +64 -0
- package/build/design-system.ts +58 -0
- package/build/external-links.ts +38 -0
- package/css/global.css +112 -71
- package/nuxt.config.ts +19 -26
- package/package.json +22 -14
- package/public/brand-icon-dark.png +0 -0
- package/public/brand-icon-light.png +0 -0
- package/public/favicon-dark.png +0 -0
- package/public/favicon-light.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/README.md +0 -31
- 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/public/icon-dark.png +0 -0
- package/public/icon.png +0 -0
- package/public/touch-icon-dark.png +0 -0
- package/public/touch-icon.png +0 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Design system layer: design language extended by every app. It owns UI primitive
|
|
|
15
15
|
|
|
16
16
|
**Depends on:** `nuxtseo-shared` (brand marks) and the published Nuxt modules in `package.json`. No workspace package.
|
|
17
17
|
|
|
18
|
-
**Consumed by:** `apps/site`, `apps/pro`, `apps/admin`, `apps/brand-kit
|
|
18
|
+
**Consumed by:** `layers/brand`, which every Nuxt SEO app (`apps/site`, `apps/pro`, `apps/admin`, `apps/brand-kit`) extends in its place, and outside sites through the [npm](https://npmjs.com) package `@nuxtseo/design-system`.
|
|
19
19
|
|
|
20
20
|
## Rules
|
|
21
21
|
|
|
@@ -39,16 +39,21 @@ export default defineNuxtConfig({
|
|
|
39
39
|
})
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
- Peers: `nuxt` 4.6+, `vue` 3.5+,
|
|
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.
|
|
43
44
|
- If [pnpm](https://pnpm.io) reports ignored build scripts, allow `esbuild` and `@parcel/watcher` under `allowBuilds` in `pnpm-workspace.yaml`.
|
|
44
|
-
- Import components from `#components`. The layer turns auto-imports off.
|
|
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
|
+
- Write `app.config.ts` as a plain object. `defineAppConfig` is an auto-import, so the server bundle has no such function.
|
|
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
|
+
- 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.
|
|
45
50
|
|
|
46
51
|
### Overrides
|
|
47
52
|
|
|
48
53
|
| What | How |
|
|
49
54
|
| --- | --- |
|
|
50
55
|
| Nuxt UI colours, slots, icons, toaster, favicon paths | `app.config.ts` in your app. It merges over the layer's `app/app.config.ts`. |
|
|
51
|
-
| Tokens: colours, fonts, density, elevation | CSS custom properties in your own stylesheet, loaded after the layer's `css/global.css`. Override `--ui-*`, `--density-*`, and `--elevation-*`; never `--_depth-*`. |
|
|
56
|
+
| Tokens: colours, fonts, density, elevation | CSS custom properties in your own stylesheet, loaded after the layer's `css/global.css`. Override `--ui-*`, `--font-*`, `--density-*`, and `--elevation-*`; never `--_depth-*` or `--_viz-*`. |
|
|
52
57
|
| One instance | Props and slots on the component. |
|
|
53
58
|
| Icon set, chart colours, icon colour roles | The brand seams below. |
|
|
54
59
|
|
|
@@ -77,12 +82,103 @@ export default defineNuxtConfig({
|
|
|
77
82
|
})
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
**
|
|
85
|
+
**Favicon.** The layer's favicon plugin emits a theme-aware icon link and an Apple touch icon link from `favicon` in `app.config.ts`. Unset, it emits neither. Each link's MIME type follows the file extension, so an SVG declares `image/svg+xml`. A mode without its own path uses the other mode's path. The links carry `id: 'favicon'` and `id: 'apple-touch-icon'`.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// app.config.ts
|
|
89
|
+
export default {
|
|
90
|
+
favicon: {
|
|
91
|
+
light: '/icon-light.svg',
|
|
92
|
+
dark: '/icon-dark.svg',
|
|
93
|
+
appleTouchLight: '/touch-icon.png',
|
|
94
|
+
appleTouchDark: '/touch-icon-dark.png',
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
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.
|
|
100
|
+
|
|
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
|
+
}
|
|
122
|
+
```
|
|
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
|
+
|
|
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.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// nuxt.config.ts
|
|
130
|
+
export default defineNuxtConfig({
|
|
131
|
+
designSystem: { internalLinkHosts: ['example.com', 'www.example.com'] },
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**OG images.** [nuxt-og-image](https://nuxtseo.com/docs/og-image) compiles OG images against one Tailwind entry: the `#tailwindcss` alias, or else the first CSS entry that imports Tailwind, which is the layer's. Runtime `:root` overrides never reach it. To give OG images your theme, write an entry that imports the layer's stylesheet and restates your theme values. Then point the alias at it. Browsers never load this file.
|
|
136
|
+
|
|
137
|
+
```css
|
|
138
|
+
/* app/assets/css/og-theme.css */
|
|
139
|
+
@import '@nuxtseo/design-system/css/global.css';
|
|
140
|
+
|
|
141
|
+
@theme {
|
|
142
|
+
--font-sans: 'DM Sans', system-ui, sans-serif;
|
|
143
|
+
--font-display: 'Poppins', system-ui, sans-serif;
|
|
144
|
+
--font-title: 'Poppins', system-ui, sans-serif;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// nuxt.config.ts
|
|
150
|
+
export default defineNuxtConfig({
|
|
151
|
+
alias: { '#tailwindcss': fileURLToPath(new URL('./app/assets/css/og-theme.css', import.meta.url)) },
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
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.
|
|
156
|
+
|
|
157
|
+
| Property | Sets |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `--ui-light-text`, `--ui-light-text-dimmed`, `--ui-light-text-highlighted` | `--ui-text`, `--ui-text-dimmed`, `--ui-text-highlighted` in light mode |
|
|
160
|
+
| `--ui-dashboard-neutral-50` to `--ui-dashboard-neutral-950` | The dashboard's neutral ramp, both modes |
|
|
161
|
+
| `--ui-dashboard-light-text-dimmed` | Dashboard `--ui-text-dimmed`, light mode |
|
|
162
|
+
| `--ui-dashboard-dark-text-dimmed`, `--ui-dashboard-dark-text-muted` | Dashboard `--ui-text-dimmed` and `--ui-text-muted`, dark mode |
|
|
163
|
+
|
|
164
|
+
```css
|
|
165
|
+
:root {
|
|
166
|
+
--ui-light-text: var(--color-olive-800);
|
|
167
|
+
--ui-dashboard-neutral-50: var(--color-olive-50);
|
|
168
|
+
/* and each step to 950 */
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A `var()` inside a custom property resolves on the element that declares it. So `:root { --ui-dashboard-dark-text-muted: var(--ui-color-neutral-400) }` reads the marketing ramp's 400 step. Name a fixed colour or a `--color-*` step instead.
|
|
173
|
+
|
|
174
|
+
**Chart colours.** Every chart identity slot in `dataVizColors.ts` reads a `--ui-viz-<slot>` custom property: `clicks`, `impressions`, `ctr`, `position`, `sessions`, `users`, `pageviews`, `lcp`, `inp`, `cls`, `current`, `comparison`, `device-desktop`, `device-mobile`, `device-tablet`, `brand`, `non-brand`, `rank-1` to `rank-10`, `resource-document`, `resource-script`, `resource-stylesheet`, `resource-image`, `resource-font`, `category-1` to `category-5`, and `neutral`. The SVG paint, the `bg-viz-*` and `text-viz-*` classes, and the `/15` fills all follow it. Unset, light mode reads a deeper default for any slot whose stock hue falls under 3:1 on white; dark mode keeps the stock hue. Text classes use the mark colour unless `--ui-viz-<slot>-text` is set. If the mark colour fails AA as text, set it.
|
|
81
175
|
|
|
82
176
|
```css
|
|
83
177
|
:root {
|
|
84
178
|
--ui-viz-clicks: var(--ui-primary);
|
|
85
179
|
--ui-viz-neutral: #94a3b8;
|
|
180
|
+
--ui-viz-comparison: #a3a292;
|
|
181
|
+
--ui-viz-comparison-text: #67713a;
|
|
86
182
|
}
|
|
87
183
|
```
|
|
88
184
|
|
|
@@ -92,11 +188,61 @@ A palette `hex` field is a CSS colour, such as `var(--ui-viz-clicks, #3b82f6)`.
|
|
|
92
188
|
|
|
93
189
|
```ts
|
|
94
190
|
// app.config.ts
|
|
95
|
-
export default
|
|
191
|
+
export default {
|
|
96
192
|
designSystem: {
|
|
97
193
|
iconColors: { lime: { bg: 'bg-lime-500/10', text: 'text-lime-500' } },
|
|
98
194
|
},
|
|
99
|
-
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
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>
|
|
100
211
|
```
|
|
101
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`.
|
|
247
|
+
|
|
102
248
|
**Motion.** The layer provides motion-v's `domAnimation` features to the whole app, `error.vue` included. A site needs no `<LazyMotion>` wrapper.
|
package/app/app.config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AppFaviconConfig } from './utils/app-favicon'
|
|
1
2
|
import type { IconColorClasses } from './utils/icon-color'
|
|
2
3
|
import { UI_ICON_SLOTS } from '../shared/icons/ui-slots'
|
|
3
4
|
|
|
@@ -13,16 +14,11 @@ export default {
|
|
|
13
14
|
designSystem: {
|
|
14
15
|
iconColors: {} as Record<string, IconColorClasses>,
|
|
15
16
|
},
|
|
16
|
-
// Theme-aware
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
favicon: {
|
|
21
|
-
light: '/favicon-light.png',
|
|
22
|
-
dark: '/favicon-dark.png',
|
|
23
|
-
appleTouchLight: '/touch-icon.png',
|
|
24
|
-
appleTouchDark: '/touch-icon-dark.png',
|
|
25
|
-
},
|
|
17
|
+
// Theme-aware favicon and touch icon paths, read by plugins/favicon.ts. The
|
|
18
|
+
// layer ships no brand images, so every path is unset here and the plugin
|
|
19
|
+
// emits nothing until an app names its own files. The MIME type follows
|
|
20
|
+
// each file's extension.
|
|
21
|
+
favicon: {} as AppFaviconConfig,
|
|
26
22
|
toaster: {
|
|
27
23
|
position: 'bottom-right' as const,
|
|
28
24
|
// Collapsed peek-deck that fans out on hover (Sonner/Linear). Read by UiToaster.
|
|
@@ -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">
|
|
@@ -116,6 +116,7 @@ function errorText(value: unknown): string {
|
|
|
116
116
|
type="search"
|
|
117
117
|
class="w-full sm:w-56"
|
|
118
118
|
:placeholder="searchPlaceholder"
|
|
119
|
+
icon="search"
|
|
119
120
|
autocomplete="off"
|
|
120
121
|
size="sm"
|
|
121
122
|
:ui="{ base: 'min-h-11 transition-[width] duration-200 focus-within:w-72 sm:min-h-0' }"
|
|
@@ -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.
|