@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's own theme-aware favicon and touch icon, set under `favicon` in
|
|
3
|
+
* app.config. Every path is optional: the layer ships no brand images, so an
|
|
4
|
+
* unset path emits no link. A mode with no path of its own uses the other
|
|
5
|
+
* mode's path.
|
|
6
|
+
*/
|
|
7
|
+
export interface AppFaviconConfig {
|
|
8
|
+
light?: string
|
|
9
|
+
dark?: string
|
|
10
|
+
appleTouchLight?: string
|
|
11
|
+
appleTouchDark?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type AppFaviconLink
|
|
15
|
+
= { id: 'favicon', rel: 'icon', href: string, type?: string, sizes?: string }
|
|
16
|
+
| { id: 'apple-touch-icon', rel: 'apple-touch-icon', href: string, sizes: string }
|
|
17
|
+
|
|
18
|
+
const MIME_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
19
|
+
avif: 'image/avif',
|
|
20
|
+
gif: 'image/gif',
|
|
21
|
+
ico: 'image/x-icon',
|
|
22
|
+
jpeg: 'image/jpeg',
|
|
23
|
+
jpg: 'image/jpeg',
|
|
24
|
+
png: 'image/png',
|
|
25
|
+
svg: 'image/svg+xml',
|
|
26
|
+
webp: 'image/webp',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The MIME type a favicon path declares, read from its extension. Undefined for an unknown extension. */
|
|
30
|
+
export function faviconMimeType(href: string): string | undefined {
|
|
31
|
+
const path = href.split(/[?#]/, 1)[0] ?? ''
|
|
32
|
+
const extension = /\.([a-z0-9]+)$/i.exec(path)?.[1]?.toLowerCase()
|
|
33
|
+
return extension === undefined ? undefined : MIME_BY_EXTENSION[extension]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pick(dark: boolean, light: string | undefined, darkPath: string | undefined): string | undefined {
|
|
37
|
+
return dark ? (darkPath ?? light) : (light ?? darkPath)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function iconSizes(type: string | undefined): string | undefined {
|
|
41
|
+
if (type === 'image/svg+xml')
|
|
42
|
+
return 'any'
|
|
43
|
+
if (type === 'image/x-icon')
|
|
44
|
+
return undefined
|
|
45
|
+
return '32x32'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The head links for the app's favicon in one colour mode. The ids let an app
|
|
50
|
+
* replace either link with its own `useHead` entry.
|
|
51
|
+
*/
|
|
52
|
+
export function appFaviconLinks(config: AppFaviconConfig | undefined, dark: boolean): AppFaviconLink[] {
|
|
53
|
+
const links: AppFaviconLink[] = []
|
|
54
|
+
const icon = pick(dark, config?.light, config?.dark)
|
|
55
|
+
if (icon !== undefined) {
|
|
56
|
+
const type = faviconMimeType(icon)
|
|
57
|
+
const sizes = iconSizes(type)
|
|
58
|
+
links.push({ id: 'favicon', rel: 'icon', ...(type ? { type } : {}), ...(sizes ? { sizes } : {}), href: icon })
|
|
59
|
+
}
|
|
60
|
+
const touch = pick(dark, config?.appleTouchLight, config?.appleTouchDark)
|
|
61
|
+
if (touch !== undefined)
|
|
62
|
+
links.push({ id: 'apple-touch-icon', rel: 'apple-touch-icon', sizes: '180x180', href: touch })
|
|
63
|
+
return links
|
|
64
|
+
}
|
package/build/design-system.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ModuleOptions as IconModuleOptions } from '@nuxt/icon'
|
|
2
2
|
import type { NuxtOptions } from 'nuxt/schema'
|
|
3
3
|
import type { IconSet } from '../shared/icons'
|
|
4
|
+
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs'
|
|
4
5
|
import { defineNuxtModule } from 'nuxt/kit'
|
|
6
|
+
import { dirname, resolve } from 'pathe'
|
|
5
7
|
import { iconAliasMap, iconBundleList, parseIconSet } from '../shared/icons'
|
|
8
|
+
import { externalLinkMarkerCss } from './external-links'
|
|
6
9
|
|
|
7
10
|
export interface DesignSystemOptions {
|
|
8
11
|
/**
|
|
@@ -10,6 +13,19 @@ export interface DesignSystemOptions {
|
|
|
10
13
|
* Install the matching `@iconify-json/<set>` collection in the app.
|
|
11
14
|
*/
|
|
12
15
|
iconSet?: IconSet
|
|
16
|
+
/**
|
|
17
|
+
* Keep the layer's composables and utils in the app's auto-import scan, and
|
|
18
|
+
* so in `#imports`. Defaults to false: their names, such as a one-argument
|
|
19
|
+
* `formatNumber`, would clash with a site's own helpers. A site imports
|
|
20
|
+
* them by path instead. Nuxt SEO's private `layers/brand` turns it on.
|
|
21
|
+
*/
|
|
22
|
+
autoImportUtils?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Hosts that count as this site. An absolute prose link to one of them gets
|
|
25
|
+
* no external-link arrow. Defaults to none, so every absolute http(s) link
|
|
26
|
+
* is marked. Matches a substring of the href after `//`, so list `www.` too.
|
|
27
|
+
*/
|
|
28
|
+
internalLinkHosts?: string[]
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
/**
|
|
@@ -43,5 +59,47 @@ export default defineNuxtModule<DesignSystemOptions>({
|
|
|
43
59
|
icon.aliases = { ...iconAliasMap(iconSet), ...icon.aliases }
|
|
44
60
|
icon.clientBundle ??= {}
|
|
45
61
|
icon.clientBundle.icons = [...new Set([...iconBundleList(iconSet), ...(icon.clientBundle.icons ?? [])])]
|
|
62
|
+
|
|
63
|
+
// A plain file, not a Nuxt template. In dev, Nuxt links every
|
|
64
|
+
// `nuxt.options.css` entry the module graph has not loaded, matching by
|
|
65
|
+
// file path. The graph holds a template as a `virtual:nuxt:` id with no
|
|
66
|
+
// such path, so a template stylesheet was linked twice, the second copy
|
|
67
|
+
// after the site's own CSS. The CLI clears the build directory after
|
|
68
|
+
// modules load, so the file is written when the build starts.
|
|
69
|
+
const externalLinks = resolve(nuxt.options.buildDir, 'design-system/external-links.css')
|
|
70
|
+
nuxt.options.css.push(externalLinks)
|
|
71
|
+
nuxt.hook('build:before', () => {
|
|
72
|
+
mkdirSync(dirname(externalLinks), { recursive: true })
|
|
73
|
+
writeFileSync(externalLinks, externalLinkMarkerCss(options.internalLinkHosts ?? []))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// The layer's templates pass `aria-*` attributes and a few listeners to
|
|
77
|
+
// components through $attrs, and `app/component-attributes.d.ts` types
|
|
78
|
+
// them. A consumer's tsconfig excludes node_modules, so the declaration
|
|
79
|
+
// never reached its program and a TypeScript 7 native typecheck failed
|
|
80
|
+
// inside the layer's templates. A reference ignores that exclude.
|
|
81
|
+
nuxt.hook('prepare:types', ({ references }) => {
|
|
82
|
+
references.push({ path: resolve(LAYER_ROOTS[0]!, 'app/component-attributes.d.ts') })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Nuxt scans every layer's composables/ and utils/ for auto-imports, and a
|
|
86
|
+
// layer's `imports.scan: false` would merge into the app and stop its scan
|
|
87
|
+
// too. So the layer removes only its own directories here.
|
|
88
|
+
if (!options.autoImportUtils) {
|
|
89
|
+
nuxt.hook('imports:dirs', (dirs) => {
|
|
90
|
+
const kept = dirs.filter(dir => !isLayerPath(dir))
|
|
91
|
+
dirs.splice(0, dirs.length, ...kept)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
46
94
|
},
|
|
47
95
|
})
|
|
96
|
+
|
|
97
|
+
const LAYER_ROOTS = [...new Set([resolve(import.meta.dirname, '..'), resolve(realpathSync(resolve(import.meta.dirname, '..')))])]
|
|
98
|
+
|
|
99
|
+
/** Whether a directory sits inside this layer, through a symlink or not. */
|
|
100
|
+
export function isLayerPath(dir: string): boolean {
|
|
101
|
+
const paths = [resolve(dir)]
|
|
102
|
+
if (existsSync(dir))
|
|
103
|
+
paths.push(resolve(realpathSync(dir)))
|
|
104
|
+
return paths.some(path => LAYER_ROOTS.some(root => path === root || path.startsWith(`${root}/`)))
|
|
105
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The external-link arrow on outbound prose links. Only an absolute http(s)
|
|
3
|
+
* link that leaves the site gets it; a relative link never matches, and a
|
|
4
|
+
* link around an image is excluded because the arrow would hang off the
|
|
5
|
+
* corner of the figure.
|
|
6
|
+
*
|
|
7
|
+
* Which hosts count as this site is a brand choice, so the rules are built
|
|
8
|
+
* from `designSystem.internalLinkHosts`. A CSS custom property cannot feed an
|
|
9
|
+
* attribute selector, so the hosts have to reach the selector at build time.
|
|
10
|
+
*
|
|
11
|
+
* The marker is a background on a pseudo-element whose only content is a
|
|
12
|
+
* no-break space: the NBSP glues the arrow to the last word, so it can never
|
|
13
|
+
* orphan onto a line of its own, and there is no text for a screen reader to
|
|
14
|
+
* announce. The mask resolves `currentColor` against the image's own initial
|
|
15
|
+
* colour, which is opaque black, so the arrow always paints in the link's
|
|
16
|
+
* colour and the CSS holds no colour of its own.
|
|
17
|
+
*/
|
|
18
|
+
export function externalLinkMarkerCss(internalHosts: readonly string[]): string {
|
|
19
|
+
const own = internalHosts
|
|
20
|
+
.map(host => `:not([href*="//${host.replace(/["\\]/g, '\\$&')}"])`)
|
|
21
|
+
.join('')
|
|
22
|
+
const link = `a.prose-link[href^="http"]${own}:not(:has(img))`
|
|
23
|
+
return `${link}::after {
|
|
24
|
+
content: '\\00a0';
|
|
25
|
+
padding-inline-end: 0.72em;
|
|
26
|
+
background-color: currentcolor;
|
|
27
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 7h10v10'/%3E%3Cpath d='M7 17 17 7'/%3E%3C/svg%3E");
|
|
28
|
+
mask-repeat: no-repeat;
|
|
29
|
+
mask-position: right center;
|
|
30
|
+
mask-size: 0.72em 0.72em;
|
|
31
|
+
opacity: 0.65;
|
|
32
|
+
vertical-align: baseline;
|
|
33
|
+
}
|
|
34
|
+
${link}:hover::after {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
}
|
package/css/global.css
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
2
|
@import '@nuxt/ui';
|
|
3
3
|
|
|
4
|
+
/* Type families. A site sets its own in an unlayered `:root` rule; OG images
|
|
5
|
+
read them from the Tailwind entry instead (README, Brand seams). `--font-title`
|
|
6
|
+
carries page titles (`.text-title`, `font-title`) and defaults to the display
|
|
7
|
+
family. The layer registers no faces; a site loads its own (README). */
|
|
4
8
|
@theme {
|
|
5
9
|
--font-sans: 'Hubot Sans', sans-serif;
|
|
6
10
|
--font-display: 'Hubot Sans', sans-serif;
|
|
11
|
+
--font-title: 'Hubot Sans', sans-serif;
|
|
7
12
|
--font-mono: 'Fira Code', monospace;
|
|
8
13
|
}
|
|
9
14
|
|
|
@@ -271,11 +276,40 @@
|
|
|
271
276
|
--color-viz-resource-image: var(--ui-viz-resource-image, var(--_viz-resource-image, var(--color-cyan-500)));
|
|
272
277
|
--color-viz-resource-font: var(--ui-viz-resource-font, var(--color-pink-500));
|
|
273
278
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
--text-color-viz-
|
|
277
|
-
--text-color-viz-
|
|
278
|
-
--text-color-viz-
|
|
279
|
+
/* Text side. `--ui-viz-<slot>-text` recolours only the `text-viz-<slot>`
|
|
280
|
+
class, for a mark hue that fails AA as text; unset, text follows the mark. */
|
|
281
|
+
--text-color-viz-clicks: var(--ui-viz-clicks-text, var(--ui-viz-clicks, var(--color-blue-500)));
|
|
282
|
+
--text-color-viz-impressions: var(--ui-viz-impressions-text, var(--ui-viz-impressions, var(--color-purple-500)));
|
|
283
|
+
--text-color-viz-ctr: var(--ui-viz-ctr-text, var(--ui-viz-ctr, var(--_viz-ctr, var(--color-emerald-500))));
|
|
284
|
+
--text-color-viz-position: var(--ui-viz-position-text, var(--ui-viz-position, var(--_viz-position, var(--color-orange-500))));
|
|
285
|
+
--text-color-viz-sessions: var(--ui-viz-sessions-text, var(--ui-viz-sessions, var(--color-blue-500)));
|
|
286
|
+
--text-color-viz-users: var(--ui-viz-users-text, var(--ui-viz-users, var(--_viz-users, var(--color-cyan-500))));
|
|
287
|
+
--text-color-viz-pageviews: var(--ui-viz-pageviews-text, var(--ui-viz-pageviews, var(--color-purple-500)));
|
|
288
|
+
--text-color-viz-lcp: var(--ui-viz-lcp-text, var(--ui-viz-lcp, var(--color-blue-500)));
|
|
289
|
+
--text-color-viz-inp: var(--ui-viz-inp-text, var(--ui-viz-inp, var(--ui-text-warning)));
|
|
290
|
+
--text-color-viz-cls: var(--ui-viz-cls-text, var(--ui-viz-cls, var(--color-purple-500)));
|
|
291
|
+
--text-color-viz-current: var(--ui-viz-current-text, var(--ui-viz-current, var(--_viz-current-text)));
|
|
292
|
+
--text-color-viz-comparison: var(--ui-viz-comparison-text, var(--ui-viz-comparison, var(--_viz-comparison-text)));
|
|
293
|
+
--text-color-viz-device-desktop: var(--ui-viz-device-desktop-text, var(--ui-viz-device-desktop, var(--_viz-device-desktop, var(--color-blue-400))));
|
|
294
|
+
--text-color-viz-device-mobile: var(--ui-viz-device-mobile-text, var(--ui-viz-device-mobile, var(--_viz-device-mobile, var(--color-green-400))));
|
|
295
|
+
--text-color-viz-device-tablet: var(--ui-viz-device-tablet-text, var(--ui-viz-device-tablet, var(--ui-text-warning)));
|
|
296
|
+
--text-color-viz-brand: var(--ui-viz-brand-text, var(--ui-viz-brand, var(--color-violet-500)));
|
|
297
|
+
--text-color-viz-non-brand: var(--ui-viz-non-brand-text, var(--ui-viz-non-brand, var(--_viz-non-brand, var(--color-emerald-500))));
|
|
298
|
+
--text-color-viz-rank-1: var(--ui-viz-rank-1-text, var(--ui-viz-rank-1, var(--color-blue-500)));
|
|
299
|
+
--text-color-viz-rank-2: var(--ui-viz-rank-2-text, var(--ui-viz-rank-2, var(--color-purple-500)));
|
|
300
|
+
--text-color-viz-rank-3: var(--ui-viz-rank-3-text, var(--ui-viz-rank-3, var(--_viz-rank-3, var(--color-emerald-400))));
|
|
301
|
+
--text-color-viz-rank-4: var(--ui-viz-rank-4-text, var(--ui-viz-rank-4, var(--_viz-rank-4, var(--color-orange-500))));
|
|
302
|
+
--text-color-viz-rank-5: var(--ui-viz-rank-5-text, var(--ui-viz-rank-5, var(--_viz-rank-5, var(--color-cyan-500))));
|
|
303
|
+
--text-color-viz-rank-6: var(--ui-viz-rank-6-text, var(--ui-viz-rank-6, var(--color-pink-500)));
|
|
304
|
+
--text-color-viz-rank-7: var(--ui-viz-rank-7-text, var(--ui-viz-rank-7, var(--ui-text-warning)));
|
|
305
|
+
--text-color-viz-rank-8: var(--ui-viz-rank-8-text, var(--ui-viz-rank-8, var(--color-indigo-400)));
|
|
306
|
+
--text-color-viz-rank-9: var(--ui-viz-rank-9-text, var(--ui-viz-rank-9, var(--_viz-rank-9, var(--color-teal-400))));
|
|
307
|
+
--text-color-viz-rank-10: var(--ui-viz-rank-10-text, var(--ui-viz-rank-10, var(--_viz-rank-10, var(--color-rose-400))));
|
|
308
|
+
--text-color-viz-resource-document: var(--ui-viz-resource-document-text, var(--ui-viz-resource-document, var(--color-blue-500)));
|
|
309
|
+
--text-color-viz-resource-script: var(--ui-viz-resource-script-text, var(--ui-viz-resource-script, var(--_viz-resource-script, var(--color-orange-500))));
|
|
310
|
+
--text-color-viz-resource-stylesheet: var(--ui-viz-resource-stylesheet-text, var(--ui-viz-resource-stylesheet, var(--color-purple-500)));
|
|
311
|
+
--text-color-viz-resource-image: var(--ui-viz-resource-image-text, var(--ui-viz-resource-image, var(--_viz-resource-image, var(--color-cyan-500))));
|
|
312
|
+
--text-color-viz-resource-font: var(--ui-viz-resource-font-text, var(--ui-viz-resource-font, var(--color-pink-500)));
|
|
279
313
|
}
|
|
280
314
|
|
|
281
315
|
:root {
|
|
@@ -541,7 +575,13 @@
|
|
|
541
575
|
touch-action: manipulation;
|
|
542
576
|
}
|
|
543
577
|
|
|
544
|
-
.
|
|
578
|
+
/* Surface seam. The mode and dashboard blocks below sit in `:where()`, so
|
|
579
|
+
they carry no specificity: a site's plain `:root` rule sets any surface
|
|
580
|
+
token in both modes, and its `.dark` rule sets dark, whichever stylesheet
|
|
581
|
+
loads last. Nuxt UI's own defaults sit in `@layer theme` and still lose to
|
|
582
|
+
these. Source order decides between these blocks, so the dashboard blocks
|
|
583
|
+
stay after the mode blocks. */
|
|
584
|
+
:where(.light) {
|
|
545
585
|
color-scheme: light;
|
|
546
586
|
/* Bind surfaces to `--ui-color-neutral-*` (Nuxt UI's neutral alias → the
|
|
547
587
|
hue-292 slate override) rather than stock `--color-neutral-*` (cold hue
|
|
@@ -552,11 +592,13 @@
|
|
|
552
592
|
--ui-bg-elevated: var(--ui-color-neutral-100);
|
|
553
593
|
--ui-bg-accented: var(--ui-color-neutral-200);
|
|
554
594
|
--ui-bg-inverted: var(--ui-color-neutral-900);
|
|
555
|
-
|
|
595
|
+
/* Text seam. These three sit on Nuxt SEO's violet hue, off the neutral
|
|
596
|
+
ramp; a site re-hues them by setting `--ui-light-text*` (README). */
|
|
597
|
+
--ui-text-dimmed: var(--ui-light-text-dimmed, oklch(47.5% 0.052 292));
|
|
556
598
|
--ui-text-muted: var(--ui-color-neutral-600);
|
|
557
599
|
--ui-text-toned: var(--ui-color-neutral-700);
|
|
558
|
-
--ui-text: oklch(37% 0.044 285);
|
|
559
|
-
--ui-text-highlighted: oklch(21% 0.042 285);
|
|
600
|
+
--ui-text: var(--ui-light-text, oklch(37% 0.044 285));
|
|
601
|
+
--ui-text-highlighted: var(--ui-light-text-highlighted, oklch(21% 0.042 285));
|
|
560
602
|
--ui-text-inverted: #fff;
|
|
561
603
|
--ui-border: var(--ui-color-neutral-200);
|
|
562
604
|
--ui-border-muted: var(--ui-color-neutral-100);
|
|
@@ -573,7 +615,7 @@
|
|
|
573
615
|
--nuxtseo-brand-tile-ink: oklch(98.4% 0.005 292);
|
|
574
616
|
}
|
|
575
617
|
|
|
576
|
-
.dark {
|
|
618
|
+
:where(.dark) {
|
|
577
619
|
color-scheme: dark;
|
|
578
620
|
/* Tier ladder in dark: 8.5 → 15 → 21 → 30. The old 7.5 → 13 gap read flat
|
|
579
621
|
on large surfaces; the new deltas give nav / body / elevated cards a
|
|
@@ -1155,29 +1197,30 @@ textarea {
|
|
|
1155
1197
|
.section-accent-sky { @apply bg-sky-50 dark:bg-sky-500/5 border-sky-200/50 dark:border-sky-500/10; }
|
|
1156
1198
|
|
|
1157
1199
|
/* Dashboard mauve palette: editorial monochrome tone reserved for /pro/dashboard.
|
|
1158
|
-
Marketing/docs pages stay on the warm-slate neutral (hue 285).
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
--ui-color-neutral-
|
|
1162
|
-
--ui-color-neutral-
|
|
1163
|
-
--ui-color-neutral-
|
|
1164
|
-
--ui-color-neutral-
|
|
1165
|
-
--ui-color-neutral-
|
|
1166
|
-
--ui-color-neutral-
|
|
1167
|
-
--ui-color-neutral-
|
|
1168
|
-
--ui-color-neutral-
|
|
1169
|
-
--ui-color-neutral-
|
|
1170
|
-
--ui-color-neutral-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1200
|
+
Marketing/docs pages stay on the warm-slate neutral (hue 285). A site keeps
|
|
1201
|
+
its own ramp here by setting `--ui-dashboard-neutral-<step>` (README). */
|
|
1202
|
+
:where(.dashboard-theme) {
|
|
1203
|
+
--ui-color-neutral-50: var(--ui-dashboard-neutral-50, var(--color-mauve-50, oklch(98.5% 0 0)));
|
|
1204
|
+
--ui-color-neutral-100: var(--ui-dashboard-neutral-100, var(--color-mauve-100, oklch(96.7% 0.001 286.375)));
|
|
1205
|
+
--ui-color-neutral-200: var(--ui-dashboard-neutral-200, var(--color-mauve-200, oklch(92% 0.004 286.32)));
|
|
1206
|
+
--ui-color-neutral-300: var(--ui-dashboard-neutral-300, var(--color-mauve-300, oklch(87.1% 0.006 286.286)));
|
|
1207
|
+
--ui-color-neutral-400: var(--ui-dashboard-neutral-400, var(--color-mauve-400, oklch(70.5% 0.015 286.067)));
|
|
1208
|
+
--ui-color-neutral-500: var(--ui-dashboard-neutral-500, var(--color-mauve-500, oklch(55.2% 0.016 285.938)));
|
|
1209
|
+
--ui-color-neutral-600: var(--ui-dashboard-neutral-600, var(--color-mauve-600, oklch(44.2% 0.017 285.786)));
|
|
1210
|
+
--ui-color-neutral-700: var(--ui-dashboard-neutral-700, var(--color-mauve-700, oklch(37.4% 0.016 285.822)));
|
|
1211
|
+
--ui-color-neutral-800: var(--ui-dashboard-neutral-800, var(--color-mauve-800, oklch(27.4% 0.006 286.033)));
|
|
1212
|
+
--ui-color-neutral-900: var(--ui-dashboard-neutral-900, var(--color-mauve-900, oklch(21% 0.006 285.885)));
|
|
1213
|
+
--ui-color-neutral-950: var(--ui-dashboard-neutral-950, var(--color-mauve-950, oklch(14.1% 0.005 285.823)));
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
:where(.light.dashboard-theme) {
|
|
1174
1217
|
background-color: var(--ui-bg);
|
|
1175
1218
|
--ui-bg: #fff;
|
|
1176
1219
|
--ui-bg-muted: var(--ui-color-neutral-50);
|
|
1177
1220
|
--ui-bg-elevated: var(--ui-color-neutral-100);
|
|
1178
1221
|
--ui-bg-accented: var(--ui-color-neutral-200);
|
|
1179
1222
|
--ui-bg-inverted: var(--ui-color-neutral-900);
|
|
1180
|
-
--ui-text-dimmed: oklch(47.5% 0.018 286);
|
|
1223
|
+
--ui-text-dimmed: var(--ui-dashboard-light-text-dimmed, oklch(47.5% 0.018 286));
|
|
1181
1224
|
--ui-text-muted: var(--ui-color-neutral-600);
|
|
1182
1225
|
--ui-text-toned: var(--ui-color-neutral-700);
|
|
1183
1226
|
--ui-text: var(--ui-color-neutral-800);
|
|
@@ -1189,7 +1232,7 @@ textarea {
|
|
|
1189
1232
|
--ui-border-inverted: var(--ui-color-neutral-900);
|
|
1190
1233
|
}
|
|
1191
1234
|
|
|
1192
|
-
.dark.dashboard-theme {
|
|
1235
|
+
:where(.dark.dashboard-theme) {
|
|
1193
1236
|
background-color: var(--ui-bg);
|
|
1194
1237
|
--color-mauve-50: oklch(98.5% 0.002 326);
|
|
1195
1238
|
--color-mauve-100: oklch(96.4% 0.005 325.6);
|
|
@@ -1202,22 +1245,13 @@ textarea {
|
|
|
1202
1245
|
--color-mauve-800: oklch(24.4% 0.032 320.12);
|
|
1203
1246
|
--color-mauve-900: oklch(18.9% 0.026 322.12);
|
|
1204
1247
|
--color-mauve-950: oklch(14.8% 0.018 326);
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
--ui-color-neutral-200: var(--color-mauve-200, oklch(92% 0.004 286.32));
|
|
1208
|
-
--ui-color-neutral-300: var(--color-mauve-300, oklch(87.1% 0.006 286.286));
|
|
1209
|
-
--ui-color-neutral-400: var(--color-mauve-400, oklch(70.5% 0.015 286.067));
|
|
1210
|
-
--ui-color-neutral-500: var(--color-mauve-500, oklch(55.2% 0.016 285.938));
|
|
1211
|
-
--ui-color-neutral-600: var(--color-mauve-600, oklch(44.2% 0.017 285.786));
|
|
1212
|
-
--ui-color-neutral-700: var(--color-mauve-700, oklch(37.4% 0.016 285.822));
|
|
1213
|
-
--ui-color-neutral-800: var(--color-mauve-800, oklch(27.4% 0.006 286.033));
|
|
1214
|
-
--ui-color-neutral-900: var(--color-mauve-900, oklch(21% 0.006 285.885));
|
|
1215
|
-
--ui-color-neutral-950: var(--color-mauve-950, oklch(14.1% 0.005 285.823));
|
|
1248
|
+
/* `.dashboard-theme` binds `--ui-color-neutral-*` to these steps on this same
|
|
1249
|
+
element, so the dark values above flow through without a second binding. */
|
|
1216
1250
|
|
|
1217
1251
|
/* Mauve 400 measures 4.48:1 on mauve 700 (bg-accented), so muted and dimmed
|
|
1218
1252
|
share one step just above it: 4.6:1 there, 7.7:1 on bg-default. */
|
|
1219
|
-
--ui-text-dimmed: oklch(71.5% 0.032 323);
|
|
1220
|
-
--ui-text-muted: oklch(71.5% 0.032 323);
|
|
1253
|
+
--ui-text-dimmed: var(--ui-dashboard-dark-text-dimmed, oklch(71.5% 0.032 323));
|
|
1254
|
+
--ui-text-muted: var(--ui-dashboard-dark-text-muted, oklch(71.5% 0.032 323));
|
|
1221
1255
|
--ui-text-toned: var(--ui-color-neutral-300);
|
|
1222
1256
|
--ui-text: var(--ui-color-neutral-200);
|
|
1223
1257
|
--ui-text-highlighted: #fff;
|
|
@@ -1328,7 +1362,7 @@ textarea {
|
|
|
1328
1362
|
mask-image: linear-gradient(to bottom, black 0%, transparent 86%);
|
|
1329
1363
|
-webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 86%);
|
|
1330
1364
|
}
|
|
1331
|
-
.ui-atmosphere[data-geometry="bloom"] {
|
|
1365
|
+
.ui-atmosphere[data-geometry="bloom"][data-bloom-mask] {
|
|
1332
1366
|
mask-image: radial-gradient(58% 50% at 50% 38%, black, transparent 76%);
|
|
1333
1367
|
-webkit-mask-image: radial-gradient(58% 50% at 50% 38%, black, transparent 76%);
|
|
1334
1368
|
}
|
|
@@ -1338,7 +1372,8 @@ textarea {
|
|
|
1338
1372
|
}
|
|
1339
1373
|
/* veil = full scrim tint over a faded preview; no mask, softer by default */
|
|
1340
1374
|
|
|
1341
|
-
/* grain —
|
|
1375
|
+
/* grain — opt in through `grain` or `designSystem.atmosphere.grain`; static,
|
|
1376
|
+
soft-light so it reads as tooth, not dirt */
|
|
1342
1377
|
.ui-atmosphere[data-grain]::after {
|
|
1343
1378
|
content: "";
|
|
1344
1379
|
position: absolute;
|
|
@@ -1360,7 +1395,7 @@ textarea {
|
|
|
1360
1395
|
/* Page title — the largest in-page heading (page headers, hero metrics). Size is
|
|
1361
1396
|
a role var so it stays editorial on marketing, compact inside .dashboard-theme. */
|
|
1362
1397
|
.text-title {
|
|
1363
|
-
font-family: var(--font-
|
|
1398
|
+
font-family: var(--font-title);
|
|
1364
1399
|
font-size: var(--size-title);
|
|
1365
1400
|
font-weight: var(--wght-title);
|
|
1366
1401
|
font-stretch: var(--wdth-heading);
|
|
@@ -1449,10 +1484,40 @@ textarea {
|
|
|
1449
1484
|
need primary/success/current keep overriding it (an unlayered rule would beat
|
|
1450
1485
|
the utility and silently force muted). */
|
|
1451
1486
|
@layer components {
|
|
1487
|
+
/* Type seam: a site sets `--ui-label-size`, `--ui-label-weight`,
|
|
1488
|
+
`--ui-label-case` and `--ui-label-tracking` at `:root` to retune every
|
|
1489
|
+
label at once (README, Brand seams). Unset, each keeps this spec. */
|
|
1452
1490
|
.text-label {
|
|
1453
|
-
|
|
1491
|
+
font-size: var(--ui-label-size, 10px);
|
|
1492
|
+
font-weight: var(--ui-label-weight, 600);
|
|
1493
|
+
text-transform: var(--ui-label-case, uppercase);
|
|
1494
|
+
letter-spacing: var(--ui-label-tracking, 0.05em);
|
|
1454
1495
|
color: var(--ui-text-muted);
|
|
1455
1496
|
}
|
|
1497
|
+
|
|
1498
|
+
/* Table type seam. Cells read `--ui-table-cell-size` and
|
|
1499
|
+
`--ui-table-cell-leading`; header labels read `--ui-table-head-size`, then
|
|
1500
|
+
the label size. Declared after `.text-label`, so a header carrying both
|
|
1501
|
+
takes the table size. Unset leading inherits, as the old rules did. */
|
|
1502
|
+
.ui-table-cell {
|
|
1503
|
+
font-size: var(--ui-table-cell-size, var(--text-xs));
|
|
1504
|
+
line-height: var(--ui-table-cell-leading, var(--text-xs--line-height));
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.ui-table-head {
|
|
1508
|
+
font-size: var(--ui-table-head-size, var(--ui-label-size, 10px));
|
|
1509
|
+
line-height: var(--ui-table-head-leading);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/* `UiTrend` sizes `xs`; `2xs` sits below, unlayered like `.text-mini`. */
|
|
1513
|
+
.ui-trend-xs {
|
|
1514
|
+
font-size: var(--ui-trend-xs-size, var(--text-xs));
|
|
1515
|
+
line-height: var(--ui-trend-xs-leading, var(--text-xs--line-height));
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
.ui-trend-2xs {
|
|
1520
|
+
font-size: var(--ui-trend-2xs-size, 10px);
|
|
1456
1521
|
}
|
|
1457
1522
|
|
|
1458
1523
|
/* Micro label — the 9px tier below .text-label, for the tiniest tags (table
|
|
@@ -1739,32 +1804,8 @@ pre.prose-pre::-webkit-scrollbar-thumb {
|
|
|
1739
1804
|
border-radius: 9999px;
|
|
1740
1805
|
}
|
|
1741
1806
|
|
|
1742
|
-
/*
|
|
1743
|
-
|
|
1744
|
-
places. Only absolute http(s) links that are not ours: every `/docs/*` and
|
|
1745
|
-
`/learn-seo/*` link is relative and never matches. A link around an image is
|
|
1746
|
-
excluded — the arrow would hang off the corner of the figure.
|
|
1747
|
-
|
|
1748
|
-
The marker is a background on a pseudo-element whose only content is a
|
|
1749
|
-
no-break space: the NBSP glues the arrow to the last word, so it can never
|
|
1750
|
-
orphan onto a line of its own, and there is no text for a screen reader to
|
|
1751
|
-
announce. The mask resolves `currentColor` against the image's own initial
|
|
1752
|
-
colour, which is opaque black, so the arrow always paints in the link's
|
|
1753
|
-
colour and the CSS holds no colour of its own. */
|
|
1754
|
-
a.prose-link[href^="http"]:not([href*="//nuxtseo.com"]):not([href*="//www.nuxtseo.com"]):not(:has(img))::after {
|
|
1755
|
-
content: '\00a0';
|
|
1756
|
-
padding-inline-end: 0.72em;
|
|
1757
|
-
background-color: currentcolor;
|
|
1758
|
-
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 7h10v10'/%3E%3Cpath d='M7 17 17 7'/%3E%3C/svg%3E");
|
|
1759
|
-
mask-repeat: no-repeat;
|
|
1760
|
-
mask-position: right center;
|
|
1761
|
-
mask-size: 0.72em 0.72em;
|
|
1762
|
-
opacity: 0.65;
|
|
1763
|
-
vertical-align: baseline;
|
|
1764
|
-
}
|
|
1765
|
-
a.prose-link[href^="http"]:not([href*="//nuxtseo.com"]):not([href*="//www.nuxtseo.com"]):not(:has(img)):hover::after {
|
|
1766
|
-
opacity: 1;
|
|
1767
|
-
}
|
|
1807
|
+
/* The external-link arrow on outbound prose links lives in
|
|
1808
|
+
build/external-links.ts: its own-site hosts are a build option. */
|
|
1768
1809
|
|
|
1769
1810
|
/* Inline code inside a link. `prose.code` is an atomic inline (`inline-block`
|
|
1770
1811
|
with its own fill), so the link underline stopped at the pill and the pill's
|
package/nuxt.config.ts
CHANGED
|
@@ -80,33 +80,23 @@ export default defineNuxtConfig({
|
|
|
80
80
|
],
|
|
81
81
|
},
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// don't redeclare in root or app configs.
|
|
83
|
+
// The type stack is a brand seam. The layer ships and registers no faces, so
|
|
84
|
+
// a site sets its own through `fonts.families` and the `--font-*` tokens.
|
|
86
85
|
fonts: {
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// CSS variable discovery cannot add another face, so avoid parsing every
|
|
87
|
+
// component style during each Vite build.
|
|
89
88
|
processCSSVariables: false,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'Fira Code',
|
|
104
|
-
src: '/fonts/fira-code-var-latin.woff2',
|
|
105
|
-
weight: '400 500',
|
|
106
|
-
style: 'normal',
|
|
107
|
-
global: true,
|
|
108
|
-
},
|
|
109
|
-
],
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
vite: {
|
|
92
|
+
optimizeDeps: {
|
|
93
|
+
// The layer's tables run @tanstack/vue-table 9, and @nuxt/ui's UTable
|
|
94
|
+
// runs 8. Vite pre-bundles one copy per package name, so whichever it
|
|
95
|
+
// meets first serves both, and a layer table fails with "does not
|
|
96
|
+
// provide an export named 'useTable'". Unbundled, each importer resolves
|
|
97
|
+
// its own copy.
|
|
98
|
+
exclude: ['@tanstack/vue-table', '@tanstack/table-core'],
|
|
99
|
+
},
|
|
110
100
|
},
|
|
111
101
|
|
|
112
102
|
icon: {
|
|
@@ -125,7 +115,10 @@ export default defineNuxtConfig({
|
|
|
125
115
|
experimental: { componentDetection: true },
|
|
126
116
|
content: true,
|
|
127
117
|
theme: {
|
|
128
|
-
|
|
118
|
+
// Each colour adds its variants to every Nuxt UI component theme in the
|
|
119
|
+
// CSS every page inlines. No app.config maps a `tertiary` colour and no
|
|
120
|
+
// component asks for one, so it is not listed.
|
|
121
|
+
colors: ['primary', 'secondary', 'info', 'success', 'warning', 'error', 'pro'],
|
|
129
122
|
},
|
|
130
123
|
},
|
|
131
124
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/design-system",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "Nuxt SEO design system layer: UI primitives, tokens,
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"description": "Nuxt SEO design system layer: UI primitives, tokens, motion, and chart helpers. Extend it from a Nuxt app.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://nuxtseo.com",
|
|
8
8
|
"repository": {
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"./composables/*": "./app/composables/*.ts",
|
|
27
27
|
"./package.json": "./package.json",
|
|
28
28
|
"./server/*": "./server/*.ts",
|
|
29
|
-
"./shared
|
|
29
|
+
"./shared/contracts": "./shared/contracts/index.ts",
|
|
30
|
+
"./shared/icons": "./shared/icons/index.ts",
|
|
31
|
+
"./shared/*": "./shared/*.ts",
|
|
32
|
+
"./types": "./types/index.ts"
|
|
30
33
|
},
|
|
31
34
|
"main": "./nuxt.config.ts",
|
|
32
35
|
"files": [
|
|
@@ -34,18 +37,23 @@
|
|
|
34
37
|
"build",
|
|
35
38
|
"css",
|
|
36
39
|
"nuxt.config.ts",
|
|
37
|
-
"public",
|
|
38
40
|
"server",
|
|
39
41
|
"shared",
|
|
40
42
|
"types"
|
|
41
43
|
],
|
|
42
44
|
"peerDependencies": {
|
|
45
|
+
"@nuxt/scripts": "^1.3.10 || ^2.0.0-beta.14",
|
|
46
|
+
"@nuxt/ui": "^4.11.1",
|
|
47
|
+
"@vueuse/core": "^14.0.0 || ^15.0.0",
|
|
48
|
+
"motion-v": "^2.4.2",
|
|
43
49
|
"nuxt": "^4.6.0",
|
|
50
|
+
"tailwindcss": "^4.3.0",
|
|
44
51
|
"typescript": ">=5.8.0 <7",
|
|
45
|
-
"vue": "^3.5.0"
|
|
52
|
+
"vue": "^3.5.0",
|
|
53
|
+
"zod": "^4.0.0"
|
|
46
54
|
},
|
|
47
55
|
"dependencies": {
|
|
48
|
-
"@comark/nuxt": "0.7.0",
|
|
56
|
+
"@comark/nuxt": "^0.7.0",
|
|
49
57
|
"@iconify-json/hugeicons": "^1.2.34",
|
|
50
58
|
"@iconify-json/logos": "^1.2.14",
|
|
51
59
|
"@iconify-json/lucide": "^1.2.130",
|
|
@@ -55,27 +63,27 @@
|
|
|
55
63
|
"@nuxt/fonts": "^1.0.1",
|
|
56
64
|
"@nuxt/icon": "^2.5.1",
|
|
57
65
|
"@nuxt/image": "^2.1.0",
|
|
58
|
-
"@nuxt/scripts": "2.0.0-beta.14",
|
|
59
|
-
"@nuxt/ui": "^4.11.1",
|
|
60
66
|
"@tanstack/table-core": "^9.2.4",
|
|
61
67
|
"@tanstack/vue-table": "^9.2.4",
|
|
62
68
|
"@unovis/ts": "^1.6.7",
|
|
63
69
|
"@unovis/vue": "^1.6.7",
|
|
64
|
-
"@vueuse/core": "^14.4.0",
|
|
65
|
-
"motion-v": "2.4.2",
|
|
66
70
|
"nuxtseo-shared": "^6.0.2",
|
|
67
71
|
"pathe": "^2.0.3",
|
|
68
72
|
"rangi": "^2.2.0",
|
|
69
|
-
"reka-ui": "^2.10.4"
|
|
70
|
-
"tailwindcss": "^4.3.3",
|
|
71
|
-
"zod": "^4.6.7"
|
|
73
|
+
"reka-ui": "^2.10.4"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|
|
76
|
+
"@nuxt/scripts": "^2.0.0-beta.14",
|
|
77
|
+
"@nuxt/ui": "^4.11.1",
|
|
78
|
+
"@vueuse/core": "^14.4.0",
|
|
74
79
|
"ai": "^7.0.122",
|
|
80
|
+
"motion-v": "2.4.2",
|
|
75
81
|
"nuxt": "4.6.1",
|
|
76
82
|
"sharp": "^0.35.4",
|
|
77
83
|
"site-typecheck": "0.0.0",
|
|
78
|
-
"
|
|
84
|
+
"tailwindcss": "^4.3.3",
|
|
85
|
+
"vue": "^3.5.43",
|
|
86
|
+
"zod": "^4.6.7"
|
|
79
87
|
},
|
|
80
88
|
"publishConfig": {
|
|
81
89
|
"access": "public"
|
|
Binary file
|
|
Binary file
|
package/public/favicon-dark.png
DELETED
|
Binary file
|
package/public/favicon-light.png
DELETED
|
Binary file
|
package/public/favicon.ico
DELETED
|
Binary file
|