@getvitops/astro 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +476 -0
- package/README.md +145 -3
- package/dist/index.d.mts +342 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +463 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -5
- package/src/analytics.ts +325 -0
- package/src/components/Analytics.astro +59 -0
- package/src/components/CookieConsent.astro +106 -0
- package/src/components/Details.astro +10 -4
- package/src/components/Drawer.astro +9 -3
- package/src/components/Head.astro +46 -1
- package/src/components/Icon.astro +94 -0
- package/src/components/Popover.astro +9 -3
- package/src/seo.ts +33 -3
- package/virtual.d.ts +49 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One icon, resolved through the site's semantic map.
|
|
4
|
+
*
|
|
5
|
+
* `name` is a semantic name ('menu', 'chevron-down') that resolves per the
|
|
6
|
+
* configured set, OR a fully-qualified `prefix:name` which passes straight
|
|
7
|
+
* through — that is the escape hatch for a glyph the map doesn't cover.
|
|
8
|
+
*
|
|
9
|
+
* **Why this doesn't use astro-icon.** It can't. astro-icon's `./components`
|
|
10
|
+
* entry is a TypeScript file that Vite has to transform, so it is only reachable
|
|
11
|
+
* through a statically-analysable import — and a *static* import is precisely
|
|
12
|
+
* what made the optional peer mandatory for anyone rendering Popover, Details or
|
|
13
|
+
* Drawer, which is the bug this component exists to fix. A dynamic
|
|
14
|
+
* `import(/* @vite-ignore *\/ …)` compiles nothing and hands Node raw TS, which
|
|
15
|
+
* fails silently and renders an empty box. So the glyph is read straight out of
|
|
16
|
+
* the `@iconify-json/*` collection instead — the same loader the sprite emitter
|
|
17
|
+
* uses, so both delivery paths draw the identical icon and neither needs a peer.
|
|
18
|
+
*
|
|
19
|
+
* Two other things worth knowing:
|
|
20
|
+
*
|
|
21
|
+
* - **An unresolvable name warns, it doesn't throw.** A missing glyph must never
|
|
22
|
+
* 500 an SSR page; the box renders empty and the build log says which name.
|
|
23
|
+
* `vitops icons` is the place that fails hard.
|
|
24
|
+
* - **`.icon` already does the layout.** It's an inline-flex box sized in `em`
|
|
25
|
+
* (`--icon-size`) that stretches its `<svg>` child and sets `fill:
|
|
26
|
+
* currentColor`. So a start/end adornment on `.cta`/`.btn` — both already
|
|
27
|
+
* `inline-flex` with a `gap` — is a matter of child order, not a new class.
|
|
28
|
+
* Don't confuse this with `.icon-mask`, the CSS-only adornment path (see
|
|
29
|
+
* `.link--icon::before` in anchor-link.css).
|
|
30
|
+
*/
|
|
31
|
+
import { loadIconSvg, spriteId } from '@getvitops/generator';
|
|
32
|
+
import { resolveIcon } from '@getvitops/utils';
|
|
33
|
+
import icons from 'virtual:getvitops/icons';
|
|
34
|
+
|
|
35
|
+
interface Props {
|
|
36
|
+
/** Semantic name ('menu') or fully-qualified ('ph:list'). */
|
|
37
|
+
name: string;
|
|
38
|
+
/** Resolve against the brand set rather than the UI set. */
|
|
39
|
+
brand?: boolean;
|
|
40
|
+
/** Any CSS length; sets `--icon-size`. Defaults to the framework's 1.25em. */
|
|
41
|
+
size?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Accessible name. Omit for decorative icons — the default — which are then
|
|
44
|
+
* `aria-hidden`. An icon that carries meaning on its own needs this.
|
|
45
|
+
*/
|
|
46
|
+
label?: string;
|
|
47
|
+
class?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { name, brand = false, size, label, class: className } = Astro.props;
|
|
51
|
+
|
|
52
|
+
const set = brand ? icons.brand : icons.ui;
|
|
53
|
+
const aliased = icons.overrides[name] ?? name;
|
|
54
|
+
|
|
55
|
+
let qualified: string | null = null;
|
|
56
|
+
try {
|
|
57
|
+
qualified = resolveIcon(aliased, set, { weight: icons.weight ?? undefined });
|
|
58
|
+
} catch {
|
|
59
|
+
// A bare name that isn't in the map is legitimate — a sprite id, or a local
|
|
60
|
+
// SVG — so fall through with what we were given rather than giving up.
|
|
61
|
+
qualified = aliased.includes(':') ? aliased : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let svg: { viewBox: string; body: string } | null = null;
|
|
65
|
+
if (icons.engine === 'inline' && qualified) {
|
|
66
|
+
// resolveFrom is the project root, not process.cwd() — see IconsData.root.
|
|
67
|
+
svg = await loadIconSvg(qualified, { resolveFrom: icons.root });
|
|
68
|
+
if (!svg)
|
|
69
|
+
console.warn(
|
|
70
|
+
`[getvitops] <Icon name="${name}"> — "${qualified}" not found. Is ` +
|
|
71
|
+
`@iconify-json/${qualified.slice(0, qualified.indexOf(':'))} installed?`,
|
|
72
|
+
);
|
|
73
|
+
} else if (icons.engine === 'none') {
|
|
74
|
+
console.warn(
|
|
75
|
+
`[getvitops] <Icon name="${name}"> rendered but icons are not configured. ` +
|
|
76
|
+
'Add `icons: true` to getvitops() in astro.config.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const a11y = label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': 'true' };
|
|
81
|
+
const style = size ? `--icon-size:${size}` : undefined;
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
<span class:list={['icon', className]} style={style} {...a11y}>
|
|
85
|
+
{
|
|
86
|
+
svg ? (
|
|
87
|
+
<svg viewBox={svg.viewBox} set:html={svg.body} />
|
|
88
|
+
) : icons.engine === 'sprite' && icons.sprite && qualified ? (
|
|
89
|
+
<svg>
|
|
90
|
+
<use href={`${icons.sprite}#${spriteId(qualified)}`} />
|
|
91
|
+
</svg>
|
|
92
|
+
) : null
|
|
93
|
+
}
|
|
94
|
+
</span>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import Icon from './Icon.astro';
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
id: string;
|
|
@@ -14,8 +14,14 @@ interface Props {
|
|
|
14
14
|
class?: string;
|
|
15
15
|
anchorName?: string;
|
|
16
16
|
positionArea?: string;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
17
|
+
/**
|
|
18
|
+
* Map a semantic name onto a fully-qualified one before rendering.
|
|
19
|
+
*
|
|
20
|
+
* @deprecated The `icons` option on `getvitops()` now does this centrally, so
|
|
21
|
+
* the site's set is configured once instead of threaded through every call
|
|
22
|
+
* site. Left in place because removing it would break consumers passing one;
|
|
23
|
+
* when omitted, `<Icon />` resolves through the site config.
|
|
24
|
+
*/
|
|
19
25
|
iconResolver?: (name: string) => string;
|
|
20
26
|
}
|
|
21
27
|
|
package/src/seo.ts
CHANGED
|
@@ -32,6 +32,11 @@ export interface GetvitopsSeoOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* Page-title pattern; `%s` is the page's own title. e.g. `'%s · Acme'`.
|
|
34
34
|
*
|
|
35
|
+
* Applies to `<title>` only. `og:title`/`twitter:title` get the untemplated
|
|
36
|
+
* title: a social card already shows `og:site_name` and the domain, so the
|
|
37
|
+
* suffix would sit directly above the brand it repeats. Override per page with
|
|
38
|
+
* `ogTitle`.
|
|
39
|
+
*
|
|
35
40
|
* Skipped when a page's title already equals `siteName` — a homepage titled
|
|
36
41
|
* "Acme" emits `Acme`, not `Acme · Acme` — and never applied to `defaultTitle`.
|
|
37
42
|
*/
|
|
@@ -70,6 +75,12 @@ export interface GetvitopsSeoOptions {
|
|
|
70
75
|
export interface SeoProps {
|
|
71
76
|
/** Page title, before `titleTemplate` is applied. */
|
|
72
77
|
title?: string;
|
|
78
|
+
/**
|
|
79
|
+
* `og:title`/`twitter:title`, when the social headline should differ from
|
|
80
|
+
* `title`. Defaults to `title` **without** `titleTemplate` — see the note on
|
|
81
|
+
* `titleTemplate` for why the suffix is dropped there.
|
|
82
|
+
*/
|
|
83
|
+
ogTitle?: string;
|
|
73
84
|
description?: string;
|
|
74
85
|
/** Overrides the computed canonical. Relative values resolve against `site`. */
|
|
75
86
|
canonical?: string;
|
|
@@ -132,6 +143,8 @@ export interface SeoNameTag {
|
|
|
132
143
|
/** Everything `<Seo />` renders, already decided. `null` means "emit nothing". */
|
|
133
144
|
export interface ResolvedSeo {
|
|
134
145
|
title: string | null;
|
|
146
|
+
/** `og:title`/`twitter:title` — the page title without `titleTemplate`. */
|
|
147
|
+
socialTitle: string | null;
|
|
135
148
|
description: string | null;
|
|
136
149
|
canonical: string | null;
|
|
137
150
|
robots: string | null;
|
|
@@ -214,11 +227,27 @@ export function resolveSeo(
|
|
|
214
227
|
// it over `defaultTitle`, and a homepage whose own title happens to equal the
|
|
215
228
|
// site name — which is the common case, not an edge case.
|
|
216
229
|
const templated = props.title && defaults.titleTemplate && props.title !== siteName;
|
|
230
|
+
const fallbackTitle = defaults.defaultTitle ?? siteName;
|
|
217
231
|
const title = props.title
|
|
218
232
|
? templated
|
|
219
233
|
? String(defaults.titleTemplate).replaceAll('%s', props.title)
|
|
220
234
|
: props.title
|
|
221
|
-
:
|
|
235
|
+
: fallbackTitle;
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* `og:title`/`twitter:title` take the page's title *without* the template.
|
|
239
|
+
*
|
|
240
|
+
* The two are read in different places. `<title>` is navigational — a browser
|
|
241
|
+
* tab or a search result, competing with a dozen others, so it needs the brand
|
|
242
|
+
* suffix to disambiguate. A social card is self-contained and already shows
|
|
243
|
+
* `og:site_name` and the domain, so repeating the brand there is furniture:
|
|
244
|
+
* "Installation · Acme" sitting directly above "Acme".
|
|
245
|
+
*
|
|
246
|
+
* They still derive from one value, which is the part that matters — the
|
|
247
|
+
* component this replaces computed them in separate places and they drifted.
|
|
248
|
+
* Pass `ogTitle` when a page wants a genuinely different social headline.
|
|
249
|
+
*/
|
|
250
|
+
const socialTitle = props.ogTitle ?? props.title ?? fallbackTitle;
|
|
222
251
|
|
|
223
252
|
const description = props.description ?? defaults.defaultDescription ?? null;
|
|
224
253
|
|
|
@@ -248,7 +277,7 @@ export function resolveSeo(
|
|
|
248
277
|
};
|
|
249
278
|
|
|
250
279
|
og('og:type', type);
|
|
251
|
-
og('og:title',
|
|
280
|
+
og('og:title', socialTitle);
|
|
252
281
|
og('og:description', description);
|
|
253
282
|
og('og:url', canonical);
|
|
254
283
|
og('og:site_name', siteName);
|
|
@@ -280,7 +309,7 @@ export function resolveSeo(
|
|
|
280
309
|
tw('twitter:card', image ? 'summary_large_image' : (defaults.twitter?.card ?? 'summary'));
|
|
281
310
|
tw('twitter:site', defaults.twitter?.site);
|
|
282
311
|
tw('twitter:creator', props.twitterCreator ?? defaults.twitter?.creator);
|
|
283
|
-
tw('twitter:title',
|
|
312
|
+
tw('twitter:title', socialTitle);
|
|
284
313
|
tw('twitter:description', description);
|
|
285
314
|
tw('twitter:image', image);
|
|
286
315
|
tw('twitter:image:alt', image ? imageAlt : null);
|
|
@@ -296,6 +325,7 @@ export function resolveSeo(
|
|
|
296
325
|
|
|
297
326
|
return {
|
|
298
327
|
title,
|
|
328
|
+
socialTitle,
|
|
299
329
|
description,
|
|
300
330
|
canonical,
|
|
301
331
|
robots: resolveRobots(props, defaults),
|
package/virtual.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Astro generates the real types for `astro:env/*` into a consumer's
|
|
3
|
+
* `.astro/` on `astro sync`, which never runs for this package — so the
|
|
4
|
+
* import typechecks nowhere but a consumer project. Declared here at the
|
|
5
|
+
* shape `plan-features.ts` actually uses; the runtime value comes from
|
|
6
|
+
* Astro's env schema, and this file cannot import it.
|
|
7
|
+
*/
|
|
8
|
+
declare module 'astro:env/server' {
|
|
9
|
+
export const SITE_PLAN: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
/** Types for the virtual module the getvitops() integration provides to <Head />. */
|
|
2
13
|
declare module 'virtual:getvitops/head' {
|
|
3
14
|
interface FaviconLink {
|
|
@@ -6,6 +17,11 @@ declare module 'virtual:getvitops/head' {
|
|
|
6
17
|
type?: string;
|
|
7
18
|
sizes?: string;
|
|
8
19
|
}
|
|
20
|
+
/** Mirrors `HeadFont` in src/fonts.ts. `preload` is Astro's `PreloadFilter`. */
|
|
21
|
+
interface HeadFont {
|
|
22
|
+
cssVariable: string;
|
|
23
|
+
preload: boolean | { weight?: string | number; style?: string }[];
|
|
24
|
+
}
|
|
9
25
|
// Keep these keys in step with `HeadData` in src/integration.ts — the two are
|
|
10
26
|
// hand-duplicated (an ambient `declare module` in a shipped .d.ts cannot import
|
|
11
27
|
// from src), and `head.test.ts` asserts the key sets match.
|
|
@@ -17,12 +33,45 @@ declare module 'virtual:getvitops/head' {
|
|
|
17
33
|
wcBase: string;
|
|
18
34
|
editor: boolean;
|
|
19
35
|
sitemap: string | null;
|
|
36
|
+
/** Families registered by `getvitops({ fonts })`, for `<Font />`. */
|
|
37
|
+
fonts: HeadFont[];
|
|
20
38
|
site: string | null;
|
|
21
39
|
// Structurally `GetvitopsSeoOptions` (src/seo.ts). Left loose here because
|
|
22
40
|
// this file can't import from src, and <Seo /> re-types it on the way into
|
|
23
41
|
// `resolveSeo`, which is where the real contract is enforced.
|
|
24
42
|
seo: Record<string, unknown>;
|
|
43
|
+
// Likewise `GetvitopsAnalyticsOptions` (src/analytics.ts) — re-typed by
|
|
44
|
+
// <Analytics /> on the way into `resolveAnalytics`.
|
|
45
|
+
analytics: Record<string, unknown>;
|
|
46
|
+
consent: boolean;
|
|
47
|
+
consentCategories: string[];
|
|
48
|
+
consentPolicyUrl: string | null;
|
|
49
|
+
consentRuntime: boolean;
|
|
25
50
|
}
|
|
26
51
|
const data: HeadData;
|
|
27
52
|
export default data;
|
|
28
53
|
}
|
|
54
|
+
|
|
55
|
+
/** Types for the virtual module the getvitops() integration provides to <Icon />. */
|
|
56
|
+
declare module 'virtual:getvitops/icons' {
|
|
57
|
+
// Keep these keys in step with `IconsData` in src/integration.ts, for the same
|
|
58
|
+
// reason as HeadData above; `icons.test.ts` asserts the key sets match.
|
|
59
|
+
//
|
|
60
|
+
// Note what is NOT here: a resolver function. The module is JSON.stringify'd,
|
|
61
|
+
// so only data crosses. <Icon /> imports the pure `resolveIcon` from
|
|
62
|
+
// @getvitops/utils and feeds it these values.
|
|
63
|
+
interface IconsData {
|
|
64
|
+
engine: 'inline' | 'sprite' | 'none';
|
|
65
|
+
/** Absolute project root — where `@iconify-json/*` is resolved from. */
|
|
66
|
+
root: string;
|
|
67
|
+
ui: string;
|
|
68
|
+
brand: string;
|
|
69
|
+
weight: string | null;
|
|
70
|
+
/** Consumer aliases applied before the semantic map (legacy names, content values). */
|
|
71
|
+
overrides: Record<string, string>;
|
|
72
|
+
/** Public href of the sprite, or null when none is emitted. */
|
|
73
|
+
sprite: string | null;
|
|
74
|
+
}
|
|
75
|
+
const data: IconsData;
|
|
76
|
+
export default data;
|
|
77
|
+
}
|