@broberg/pwa 0.1.1 → 0.2.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/README.md CHANGED
@@ -118,6 +118,39 @@ With **Serwist**/**Workbox**, keep activation user-gated:
118
118
  new Serwist({ /* … */, skipWaiting: false, clientsClaim: true });
119
119
  ```
120
120
 
121
+ ## Install setup — manifest, icons, meta (`@broberg/pwa/manifest`, v0.2.0)
122
+
123
+ The *other* half of a PWA: the `manifest.webmanifest`, the icon set, and the
124
+ apple-touch `<meta>` tags — all as **pure, zero-dep** factories so you stop
125
+ hand-rolling `app/manifest.ts`, a `gen-pwa-icons.cjs` script, and a wall of
126
+ `<meta>` tags. Icons are emitted as self-contained **SVG** (modern manifests +
127
+ apple-touch accept SVG); no rasteriser is bundled.
128
+
129
+ ```ts
130
+ import { defineManifest, serializeManifest, buildIconSet, pwaMetaTags } from "@broberg/pwa/manifest";
131
+
132
+ // 1. Icons from a monogram (or pass `svg: "<svg…>"` for real artwork).
133
+ const { files, icons } = buildIconSet({ monogram: "AK", background: "#141969", color: "#fff" });
134
+ // files → [{ path:"/icons/icon-180.svg", content:"<svg…>", … }, …] (write to public/)
135
+ // icons → manifest icons[] incl. a padded maskable-512
136
+
137
+ // 2. Manifest — required members defaulted; `extra` merges last.
138
+ const manifest = defineManifest({ name: "Aalborg Klinik", shortName: "AK", themeColor: "#141969", icons });
139
+ writeFile("public/manifest.webmanifest", serializeManifest(manifest));
140
+
141
+ // 3. Head tags — typed descriptors you render in Next metadata / a Hono head / plain HTML.
142
+ pwaMetaTags({ themeColor: "#141969", title: "AK" });
143
+ // → [{tag:"link",attrs:{rel:"manifest",href:"/manifest.webmanifest"}}, {tag:"meta",attrs:{name:"theme-color",…}}, …]
144
+ ```
145
+
146
+ - **`buildIconSet`** emits apple-touch (180), 192, 512 + a maskable-512 (10 % safe-zone
147
+ inset) by default; override `sizes`, `basePath`, `maskable`. `180` goes in the
148
+ apple-touch `<link>`, not the manifest `icons[]`.
149
+ - **Everything is a pure return value** — you own the filesystem write and the head
150
+ render. Runs in a build script, a Route Handler, or the browser.
151
+ - Need PNG? Rasterise the returned SVGs with `sharp` on your side — the package
152
+ stays dependency-free.
153
+
121
154
  ## Gotchas (baked into this package so you don't rediscover them)
122
155
 
123
156
  - **First install is suppressed.** A worker reaching `installed` with no existing
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ // src/manifest.ts
4
+ function defineManifest(opts) {
5
+ if (!opts?.name) throw new Error("defineManifest: `name` is required");
6
+ const manifest = {
7
+ name: opts.name,
8
+ short_name: opts.shortName ?? opts.name,
9
+ start_url: opts.startUrl ?? "/",
10
+ scope: opts.scope ?? "/",
11
+ display: opts.display ?? "standalone",
12
+ theme_color: opts.themeColor ?? "#ffffff",
13
+ background_color: opts.backgroundColor ?? "#ffffff",
14
+ icons: opts.icons ?? []
15
+ };
16
+ if (opts.description) manifest.description = opts.description;
17
+ if (opts.lang) manifest.lang = opts.lang;
18
+ if (opts.orientation) manifest.orientation = opts.orientation;
19
+ if (opts.categories) manifest.categories = opts.categories;
20
+ if (opts.id) manifest.id = opts.id;
21
+ return { ...manifest, ...opts.extra ?? {} };
22
+ }
23
+ function serializeManifest(manifest, opts = {}) {
24
+ return JSON.stringify(manifest, null, opts.pretty === false ? void 0 : 2);
25
+ }
26
+ var DEFAULT_SIZES = [180, 192, 512];
27
+ function escapeXml(s) {
28
+ return s.replace(
29
+ /[<>&"']/g,
30
+ (c) => c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === "&" ? "&amp;" : c === '"' ? "&quot;" : "&apos;"
31
+ );
32
+ }
33
+ function toBase64(s) {
34
+ const g = globalThis;
35
+ if (typeof g.Buffer !== "undefined") return g.Buffer.from(s, "utf-8").toString("base64");
36
+ const bytes = new TextEncoder().encode(s);
37
+ let bin = "";
38
+ for (const b of bytes) bin += String.fromCharCode(b);
39
+ return g.btoa(bin);
40
+ }
41
+ function renderIcon(size, inset, opts) {
42
+ const bg = opts.background ?? "#111827";
43
+ const fg = opts.color ?? "#ffffff";
44
+ const pad = Math.round(size * inset);
45
+ const inner = size - pad * 2;
46
+ let art;
47
+ if (opts.svg) {
48
+ const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;
49
+ art = `<image x="${pad}" y="${pad}" width="${inner}" height="${inner}" href="${href}" preserveAspectRatio="xMidYMid meet"/>`;
50
+ } else {
51
+ const text = escapeXml((opts.monogram ?? "").slice(0, 3).toUpperCase());
52
+ const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));
53
+ art = `<text x="50%" y="50%" fill="${fg}" font-family="system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-weight="700" font-size="${fontSize}" text-anchor="middle" dominant-baseline="central">${text}</text>`;
54
+ }
55
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="${bg}"/>` + art + `</svg>`;
56
+ }
57
+ function buildIconSet(opts) {
58
+ if (!opts?.svg && !opts?.monogram) {
59
+ throw new Error("buildIconSet: provide either `svg` or `monogram`");
60
+ }
61
+ const base = (opts.basePath ?? "/icons").replace(/\/$/, "");
62
+ const sizes = opts.sizes ?? DEFAULT_SIZES;
63
+ const files = [];
64
+ const icons = [];
65
+ for (const size of sizes) {
66
+ const path = `${base}/icon-${size}.svg`;
67
+ files.push({ path, content: renderIcon(size, 0, opts), type: "image/svg+xml", size, purpose: "any" });
68
+ if (size !== 180) {
69
+ icons.push({ src: path, sizes: `${size}x${size}`, type: "image/svg+xml", purpose: "any" });
70
+ }
71
+ }
72
+ if (opts.maskable !== false) {
73
+ const path = `${base}/icon-maskable-512.svg`;
74
+ files.push({ path, content: renderIcon(512, 0.1, opts), type: "image/svg+xml", size: 512, purpose: "maskable" });
75
+ icons.push({ src: path, sizes: "512x512", type: "image/svg+xml", purpose: "maskable" });
76
+ }
77
+ return { files, icons };
78
+ }
79
+ function pwaMetaTags(opts = {}) {
80
+ const tags = [];
81
+ tags.push({ tag: "link", attrs: { rel: "manifest", href: opts.manifest ?? "/manifest.webmanifest" } });
82
+ tags.push({ tag: "meta", attrs: { name: "theme-color", content: opts.themeColor ?? "#ffffff" } });
83
+ tags.push({ tag: "link", attrs: { rel: "apple-touch-icon", href: opts.appleTouchIcon ?? "/icons/icon-180.svg" } });
84
+ if (opts.capable !== false) {
85
+ tags.push({ tag: "meta", attrs: { name: "mobile-web-app-capable", content: "yes" } });
86
+ tags.push({ tag: "meta", attrs: { name: "apple-mobile-web-app-capable", content: "yes" } });
87
+ tags.push({
88
+ tag: "meta",
89
+ attrs: { name: "apple-mobile-web-app-status-bar-style", content: opts.statusBarStyle ?? "default" }
90
+ });
91
+ }
92
+ if (opts.title) {
93
+ tags.push({ tag: "meta", attrs: { name: "apple-mobile-web-app-title", content: opts.title } });
94
+ }
95
+ return tags;
96
+ }
97
+
98
+ exports.buildIconSet = buildIconSet;
99
+ exports.defineManifest = defineManifest;
100
+ exports.pwaMetaTags = pwaMetaTags;
101
+ exports.serializeManifest = serializeManifest;
102
+ //# sourceMappingURL=manifest.cjs.map
103
+ //# sourceMappingURL=manifest.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest.ts"],"names":[],"mappings":"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","file":"manifest.cjs","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents (modern manifests + apple-touch accept SVG);\n * a consumer that specifically needs PNG can rasterise the SVGs with `sharp`\n * on their side — the package stays dep-free.\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n"]}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the
3
+ * root export). A dependency-free manifest/meta factory + an SVG-based icon
4
+ * generator, so a repo stops hand-rolling `app/manifest.ts`, a
5
+ * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.
6
+ *
7
+ * Everything here is PURE string/object generation — no filesystem, no
8
+ * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,
9
+ * or the browser, and every piece is offline-unit-testable. Icons are emitted
10
+ * as self-contained SVG documents (modern manifests + apple-touch accept SVG);
11
+ * a consumer that specifically needs PNG can rasterise the SVGs with `sharp`
12
+ * on their side — the package stays dep-free.
13
+ */
14
+ type Display = "standalone" | "fullscreen" | "minimal-ui" | "browser";
15
+ interface ManifestIcon {
16
+ src: string;
17
+ sizes: string;
18
+ type: string;
19
+ /** "any" | "maskable" | "monochrome" (space-separated allowed). */
20
+ purpose?: string;
21
+ }
22
+ interface WebAppManifest {
23
+ name: string;
24
+ short_name: string;
25
+ start_url: string;
26
+ scope: string;
27
+ display: Display;
28
+ theme_color: string;
29
+ background_color: string;
30
+ icons: ManifestIcon[];
31
+ description?: string;
32
+ lang?: string;
33
+ orientation?: string;
34
+ categories?: string[];
35
+ id?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ interface DefineManifestOptions {
39
+ /** App name (required). */
40
+ name: string;
41
+ /** Home-screen label. Default: `name`. */
42
+ shortName?: string;
43
+ /** Default `/`. */
44
+ startUrl?: string;
45
+ /** Default `/`. */
46
+ scope?: string;
47
+ /** Default `standalone`. */
48
+ display?: Display;
49
+ /** Address-bar / task-switcher colour. Default `#ffffff`. */
50
+ themeColor?: string;
51
+ /** Splash background. Default `#ffffff`. */
52
+ backgroundColor?: string;
53
+ description?: string;
54
+ lang?: string;
55
+ orientation?: string;
56
+ categories?: string[];
57
+ id?: string;
58
+ /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */
59
+ icons?: ManifestIcon[];
60
+ /** Escape hatch: extra top-level manifest members, merged last. */
61
+ extra?: Record<string, unknown>;
62
+ }
63
+ /**
64
+ * Build a spec-valid Web App Manifest object. Required members are defaulted so
65
+ * a one-liner produces an installable manifest; every default is overridable
66
+ * and `extra` merges last for anything not modelled (shortcuts, screenshots…).
67
+ */
68
+ declare function defineManifest(opts: DefineManifestOptions): WebAppManifest;
69
+ /** JSON-stringify a manifest for a `manifest.webmanifest` file. */
70
+ declare function serializeManifest(manifest: WebAppManifest, opts?: {
71
+ pretty?: boolean;
72
+ }): string;
73
+ interface GeneratedIcon {
74
+ /** URL path, e.g. `/icons/icon-192.svg`. */
75
+ path: string;
76
+ /** The SVG document. */
77
+ content: string;
78
+ type: "image/svg+xml";
79
+ size: number;
80
+ purpose: "any" | "maskable";
81
+ }
82
+ interface IconSet {
83
+ files: GeneratedIcon[];
84
+ /** Ready to drop into `defineManifest({ icons })`. */
85
+ icons: ManifestIcon[];
86
+ }
87
+ interface IconSetOptions {
88
+ /**
89
+ * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a
90
+ * data-URI `<image>` so it scales cleanly at any size regardless of its
91
+ * internal coordinate system. Mutually exclusive with `monogram`.
92
+ */
93
+ svg?: string;
94
+ /** Fallback artwork: 1–3 letters rendered centred (e.g. "AK"). */
95
+ monogram?: string;
96
+ /** Foreground (monogram text) colour. Default `#ffffff`. */
97
+ color?: string;
98
+ /** Background fill. Default `#111827`. */
99
+ background?: string;
100
+ /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */
101
+ sizes?: number[];
102
+ /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */
103
+ maskable?: boolean;
104
+ /** URL prefix for every icon `src`. Default `/icons`. */
105
+ basePath?: string;
106
+ }
107
+ /**
108
+ * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,
109
+ * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch
110
+ * (180), 192, 512 by default plus a padded maskable-512.
111
+ */
112
+ declare function buildIconSet(opts: IconSetOptions): IconSet;
113
+ interface MetaTag {
114
+ tag: "meta" | "link";
115
+ attrs: Record<string, string>;
116
+ }
117
+ interface MetaTagsOptions {
118
+ /** `<meta name="theme-color">`. Default `#ffffff`. */
119
+ themeColor?: string;
120
+ /** apple-touch-icon href. Default `/icons/icon-180.svg`. */
121
+ appleTouchIcon?: string;
122
+ /** Manifest href for the `<link rel="manifest">`. Default `/manifest.webmanifest`. */
123
+ manifest?: string;
124
+ /** apple-mobile-web-app-title. Default: omitted. */
125
+ title?: string;
126
+ /** apple-mobile-web-app-status-bar-style. Default `default`. */
127
+ statusBarStyle?: "default" | "black" | "black-translucent";
128
+ /** Emit the `*-web-app-capable` tags. Default `true`. */
129
+ capable?: boolean;
130
+ }
131
+ /**
132
+ * The head tags a PWA needs beyond the manifest link — returned as typed
133
+ * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX
134
+ * head, or plain HTML. No framework assumption.
135
+ */
136
+ declare function pwaMetaTags(opts?: MetaTagsOptions): MetaTag[];
137
+
138
+ export { type DefineManifestOptions, type Display, type GeneratedIcon, type IconSet, type IconSetOptions, type ManifestIcon, type MetaTag, type MetaTagsOptions, type WebAppManifest, buildIconSet, defineManifest, pwaMetaTags, serializeManifest };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the
3
+ * root export). A dependency-free manifest/meta factory + an SVG-based icon
4
+ * generator, so a repo stops hand-rolling `app/manifest.ts`, a
5
+ * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.
6
+ *
7
+ * Everything here is PURE string/object generation — no filesystem, no
8
+ * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,
9
+ * or the browser, and every piece is offline-unit-testable. Icons are emitted
10
+ * as self-contained SVG documents (modern manifests + apple-touch accept SVG);
11
+ * a consumer that specifically needs PNG can rasterise the SVGs with `sharp`
12
+ * on their side — the package stays dep-free.
13
+ */
14
+ type Display = "standalone" | "fullscreen" | "minimal-ui" | "browser";
15
+ interface ManifestIcon {
16
+ src: string;
17
+ sizes: string;
18
+ type: string;
19
+ /** "any" | "maskable" | "monochrome" (space-separated allowed). */
20
+ purpose?: string;
21
+ }
22
+ interface WebAppManifest {
23
+ name: string;
24
+ short_name: string;
25
+ start_url: string;
26
+ scope: string;
27
+ display: Display;
28
+ theme_color: string;
29
+ background_color: string;
30
+ icons: ManifestIcon[];
31
+ description?: string;
32
+ lang?: string;
33
+ orientation?: string;
34
+ categories?: string[];
35
+ id?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ interface DefineManifestOptions {
39
+ /** App name (required). */
40
+ name: string;
41
+ /** Home-screen label. Default: `name`. */
42
+ shortName?: string;
43
+ /** Default `/`. */
44
+ startUrl?: string;
45
+ /** Default `/`. */
46
+ scope?: string;
47
+ /** Default `standalone`. */
48
+ display?: Display;
49
+ /** Address-bar / task-switcher colour. Default `#ffffff`. */
50
+ themeColor?: string;
51
+ /** Splash background. Default `#ffffff`. */
52
+ backgroundColor?: string;
53
+ description?: string;
54
+ lang?: string;
55
+ orientation?: string;
56
+ categories?: string[];
57
+ id?: string;
58
+ /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */
59
+ icons?: ManifestIcon[];
60
+ /** Escape hatch: extra top-level manifest members, merged last. */
61
+ extra?: Record<string, unknown>;
62
+ }
63
+ /**
64
+ * Build a spec-valid Web App Manifest object. Required members are defaulted so
65
+ * a one-liner produces an installable manifest; every default is overridable
66
+ * and `extra` merges last for anything not modelled (shortcuts, screenshots…).
67
+ */
68
+ declare function defineManifest(opts: DefineManifestOptions): WebAppManifest;
69
+ /** JSON-stringify a manifest for a `manifest.webmanifest` file. */
70
+ declare function serializeManifest(manifest: WebAppManifest, opts?: {
71
+ pretty?: boolean;
72
+ }): string;
73
+ interface GeneratedIcon {
74
+ /** URL path, e.g. `/icons/icon-192.svg`. */
75
+ path: string;
76
+ /** The SVG document. */
77
+ content: string;
78
+ type: "image/svg+xml";
79
+ size: number;
80
+ purpose: "any" | "maskable";
81
+ }
82
+ interface IconSet {
83
+ files: GeneratedIcon[];
84
+ /** Ready to drop into `defineManifest({ icons })`. */
85
+ icons: ManifestIcon[];
86
+ }
87
+ interface IconSetOptions {
88
+ /**
89
+ * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a
90
+ * data-URI `<image>` so it scales cleanly at any size regardless of its
91
+ * internal coordinate system. Mutually exclusive with `monogram`.
92
+ */
93
+ svg?: string;
94
+ /** Fallback artwork: 1–3 letters rendered centred (e.g. "AK"). */
95
+ monogram?: string;
96
+ /** Foreground (monogram text) colour. Default `#ffffff`. */
97
+ color?: string;
98
+ /** Background fill. Default `#111827`. */
99
+ background?: string;
100
+ /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */
101
+ sizes?: number[];
102
+ /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */
103
+ maskable?: boolean;
104
+ /** URL prefix for every icon `src`. Default `/icons`. */
105
+ basePath?: string;
106
+ }
107
+ /**
108
+ * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,
109
+ * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch
110
+ * (180), 192, 512 by default plus a padded maskable-512.
111
+ */
112
+ declare function buildIconSet(opts: IconSetOptions): IconSet;
113
+ interface MetaTag {
114
+ tag: "meta" | "link";
115
+ attrs: Record<string, string>;
116
+ }
117
+ interface MetaTagsOptions {
118
+ /** `<meta name="theme-color">`. Default `#ffffff`. */
119
+ themeColor?: string;
120
+ /** apple-touch-icon href. Default `/icons/icon-180.svg`. */
121
+ appleTouchIcon?: string;
122
+ /** Manifest href for the `<link rel="manifest">`. Default `/manifest.webmanifest`. */
123
+ manifest?: string;
124
+ /** apple-mobile-web-app-title. Default: omitted. */
125
+ title?: string;
126
+ /** apple-mobile-web-app-status-bar-style. Default `default`. */
127
+ statusBarStyle?: "default" | "black" | "black-translucent";
128
+ /** Emit the `*-web-app-capable` tags. Default `true`. */
129
+ capable?: boolean;
130
+ }
131
+ /**
132
+ * The head tags a PWA needs beyond the manifest link — returned as typed
133
+ * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX
134
+ * head, or plain HTML. No framework assumption.
135
+ */
136
+ declare function pwaMetaTags(opts?: MetaTagsOptions): MetaTag[];
137
+
138
+ export { type DefineManifestOptions, type Display, type GeneratedIcon, type IconSet, type IconSetOptions, type ManifestIcon, type MetaTag, type MetaTagsOptions, type WebAppManifest, buildIconSet, defineManifest, pwaMetaTags, serializeManifest };
@@ -0,0 +1,98 @@
1
+ // src/manifest.ts
2
+ function defineManifest(opts) {
3
+ if (!opts?.name) throw new Error("defineManifest: `name` is required");
4
+ const manifest = {
5
+ name: opts.name,
6
+ short_name: opts.shortName ?? opts.name,
7
+ start_url: opts.startUrl ?? "/",
8
+ scope: opts.scope ?? "/",
9
+ display: opts.display ?? "standalone",
10
+ theme_color: opts.themeColor ?? "#ffffff",
11
+ background_color: opts.backgroundColor ?? "#ffffff",
12
+ icons: opts.icons ?? []
13
+ };
14
+ if (opts.description) manifest.description = opts.description;
15
+ if (opts.lang) manifest.lang = opts.lang;
16
+ if (opts.orientation) manifest.orientation = opts.orientation;
17
+ if (opts.categories) manifest.categories = opts.categories;
18
+ if (opts.id) manifest.id = opts.id;
19
+ return { ...manifest, ...opts.extra ?? {} };
20
+ }
21
+ function serializeManifest(manifest, opts = {}) {
22
+ return JSON.stringify(manifest, null, opts.pretty === false ? void 0 : 2);
23
+ }
24
+ var DEFAULT_SIZES = [180, 192, 512];
25
+ function escapeXml(s) {
26
+ return s.replace(
27
+ /[<>&"']/g,
28
+ (c) => c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === "&" ? "&amp;" : c === '"' ? "&quot;" : "&apos;"
29
+ );
30
+ }
31
+ function toBase64(s) {
32
+ const g = globalThis;
33
+ if (typeof g.Buffer !== "undefined") return g.Buffer.from(s, "utf-8").toString("base64");
34
+ const bytes = new TextEncoder().encode(s);
35
+ let bin = "";
36
+ for (const b of bytes) bin += String.fromCharCode(b);
37
+ return g.btoa(bin);
38
+ }
39
+ function renderIcon(size, inset, opts) {
40
+ const bg = opts.background ?? "#111827";
41
+ const fg = opts.color ?? "#ffffff";
42
+ const pad = Math.round(size * inset);
43
+ const inner = size - pad * 2;
44
+ let art;
45
+ if (opts.svg) {
46
+ const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;
47
+ art = `<image x="${pad}" y="${pad}" width="${inner}" height="${inner}" href="${href}" preserveAspectRatio="xMidYMid meet"/>`;
48
+ } else {
49
+ const text = escapeXml((opts.monogram ?? "").slice(0, 3).toUpperCase());
50
+ const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));
51
+ art = `<text x="50%" y="50%" fill="${fg}" font-family="system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-weight="700" font-size="${fontSize}" text-anchor="middle" dominant-baseline="central">${text}</text>`;
52
+ }
53
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="${bg}"/>` + art + `</svg>`;
54
+ }
55
+ function buildIconSet(opts) {
56
+ if (!opts?.svg && !opts?.monogram) {
57
+ throw new Error("buildIconSet: provide either `svg` or `monogram`");
58
+ }
59
+ const base = (opts.basePath ?? "/icons").replace(/\/$/, "");
60
+ const sizes = opts.sizes ?? DEFAULT_SIZES;
61
+ const files = [];
62
+ const icons = [];
63
+ for (const size of sizes) {
64
+ const path = `${base}/icon-${size}.svg`;
65
+ files.push({ path, content: renderIcon(size, 0, opts), type: "image/svg+xml", size, purpose: "any" });
66
+ if (size !== 180) {
67
+ icons.push({ src: path, sizes: `${size}x${size}`, type: "image/svg+xml", purpose: "any" });
68
+ }
69
+ }
70
+ if (opts.maskable !== false) {
71
+ const path = `${base}/icon-maskable-512.svg`;
72
+ files.push({ path, content: renderIcon(512, 0.1, opts), type: "image/svg+xml", size: 512, purpose: "maskable" });
73
+ icons.push({ src: path, sizes: "512x512", type: "image/svg+xml", purpose: "maskable" });
74
+ }
75
+ return { files, icons };
76
+ }
77
+ function pwaMetaTags(opts = {}) {
78
+ const tags = [];
79
+ tags.push({ tag: "link", attrs: { rel: "manifest", href: opts.manifest ?? "/manifest.webmanifest" } });
80
+ tags.push({ tag: "meta", attrs: { name: "theme-color", content: opts.themeColor ?? "#ffffff" } });
81
+ tags.push({ tag: "link", attrs: { rel: "apple-touch-icon", href: opts.appleTouchIcon ?? "/icons/icon-180.svg" } });
82
+ if (opts.capable !== false) {
83
+ tags.push({ tag: "meta", attrs: { name: "mobile-web-app-capable", content: "yes" } });
84
+ tags.push({ tag: "meta", attrs: { name: "apple-mobile-web-app-capable", content: "yes" } });
85
+ tags.push({
86
+ tag: "meta",
87
+ attrs: { name: "apple-mobile-web-app-status-bar-style", content: opts.statusBarStyle ?? "default" }
88
+ });
89
+ }
90
+ if (opts.title) {
91
+ tags.push({ tag: "meta", attrs: { name: "apple-mobile-web-app-title", content: opts.title } });
92
+ }
93
+ return tags;
94
+ }
95
+
96
+ export { buildIconSet, defineManifest, pwaMetaTags, serializeManifest };
97
+ //# sourceMappingURL=manifest.js.map
98
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest.ts"],"names":[],"mappings":"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","file":"manifest.js","sourcesContent":["/**\n * @broberg/pwa/manifest — the PWA *install* half (the update half lives in the\n * root export). A dependency-free manifest/meta factory + an SVG-based icon\n * generator, so a repo stops hand-rolling `app/manifest.ts`, a\n * `gen-pwa-icons.cjs` script, and a pile of apple-touch `<meta>` tags.\n *\n * Everything here is PURE string/object generation — no filesystem, no\n * rasteriser, no runtime deps — so it runs in Node, Bun, edge, a build script,\n * or the browser, and every piece is offline-unit-testable. Icons are emitted\n * as self-contained SVG documents (modern manifests + apple-touch accept SVG);\n * a consumer that specifically needs PNG can rasterise the SVGs with `sharp`\n * on their side — the package stays dep-free.\n */\n\n// ---------------------------------------------------------------------------\n// Manifest\n// ---------------------------------------------------------------------------\n\nexport type Display = \"standalone\" | \"fullscreen\" | \"minimal-ui\" | \"browser\";\n\nexport interface ManifestIcon {\n src: string;\n sizes: string;\n type: string;\n /** \"any\" | \"maskable\" | \"monochrome\" (space-separated allowed). */\n purpose?: string;\n}\n\nexport interface WebAppManifest {\n name: string;\n short_name: string;\n start_url: string;\n scope: string;\n display: Display;\n theme_color: string;\n background_color: string;\n icons: ManifestIcon[];\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n [key: string]: unknown;\n}\n\nexport interface DefineManifestOptions {\n /** App name (required). */\n name: string;\n /** Home-screen label. Default: `name`. */\n shortName?: string;\n /** Default `/`. */\n startUrl?: string;\n /** Default `/`. */\n scope?: string;\n /** Default `standalone`. */\n display?: Display;\n /** Address-bar / task-switcher colour. Default `#ffffff`. */\n themeColor?: string;\n /** Splash background. Default `#ffffff`. */\n backgroundColor?: string;\n description?: string;\n lang?: string;\n orientation?: string;\n categories?: string[];\n id?: string;\n /** Manifest `icons[]` — typically `buildIconSet(...).icons`. */\n icons?: ManifestIcon[];\n /** Escape hatch: extra top-level manifest members, merged last. */\n extra?: Record<string, unknown>;\n}\n\n/**\n * Build a spec-valid Web App Manifest object. Required members are defaulted so\n * a one-liner produces an installable manifest; every default is overridable\n * and `extra` merges last for anything not modelled (shortcuts, screenshots…).\n */\nexport function defineManifest(opts: DefineManifestOptions): WebAppManifest {\n if (!opts?.name) throw new Error(\"defineManifest: `name` is required\");\n const manifest: WebAppManifest = {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n start_url: opts.startUrl ?? \"/\",\n scope: opts.scope ?? \"/\",\n display: opts.display ?? \"standalone\",\n theme_color: opts.themeColor ?? \"#ffffff\",\n background_color: opts.backgroundColor ?? \"#ffffff\",\n icons: opts.icons ?? [],\n };\n if (opts.description) manifest.description = opts.description;\n if (opts.lang) manifest.lang = opts.lang;\n if (opts.orientation) manifest.orientation = opts.orientation;\n if (opts.categories) manifest.categories = opts.categories;\n if (opts.id) manifest.id = opts.id;\n return { ...manifest, ...(opts.extra ?? {}) };\n}\n\n/** JSON-stringify a manifest for a `manifest.webmanifest` file. */\nexport function serializeManifest(\n manifest: WebAppManifest,\n opts: { pretty?: boolean } = {},\n): string {\n return JSON.stringify(manifest, null, opts.pretty === false ? undefined : 2);\n}\n\n// ---------------------------------------------------------------------------\n// Icon generation (SVG, zero-dep)\n// ---------------------------------------------------------------------------\n\nexport interface GeneratedIcon {\n /** URL path, e.g. `/icons/icon-192.svg`. */\n path: string;\n /** The SVG document. */\n content: string;\n type: \"image/svg+xml\";\n size: number;\n purpose: \"any\" | \"maskable\";\n}\n\nexport interface IconSet {\n files: GeneratedIcon[];\n /** Ready to drop into `defineManifest({ icons })`. */\n icons: ManifestIcon[];\n}\n\nexport interface IconSetOptions {\n /**\n * Source artwork as a full SVG document (`<svg …>…</svg>`). Embedded via a\n * data-URI `<image>` so it scales cleanly at any size regardless of its\n * internal coordinate system. Mutually exclusive with `monogram`.\n */\n svg?: string;\n /** Fallback artwork: 1–3 letters rendered centred (e.g. \"AK\"). */\n monogram?: string;\n /** Foreground (monogram text) colour. Default `#ffffff`. */\n color?: string;\n /** Background fill. Default `#111827`. */\n background?: string;\n /** Icon sizes to emit. Default `[180, 192, 512]` (180 = apple-touch). */\n sizes?: number[];\n /** Also emit a maskable 512 with a padded safe-zone. Default `true`. */\n maskable?: boolean;\n /** URL prefix for every icon `src`. Default `/icons`. */\n basePath?: string;\n}\n\nconst DEFAULT_SIZES = [180, 192, 512];\n\nfunction escapeXml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) =>\n c === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === \"&\" ? \"&amp;\" : c === '\"' ? \"&quot;\" : \"&apos;\",\n );\n}\n\n/** UTF-8-safe base64 for the data-URI embed (works in Node/Bun/browser). */\nfunction toBase64(s: string): string {\n // Reached via globalThis so the package needs no @types/node (browser-safe).\n const g = globalThis as unknown as {\n Buffer?: { from(input: string, enc: string): { toString(enc: string): string } };\n btoa?: (data: string) => string;\n };\n if (typeof g.Buffer !== \"undefined\") return g.Buffer.from(s, \"utf-8\").toString(\"base64\");\n const bytes = new TextEncoder().encode(s);\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return g.btoa!(bin);\n}\n\n/** Render one square SVG icon document at `size`. `inset` = safe-zone fraction (maskable). */\nfunction renderIcon(size: number, inset: number, opts: IconSetOptions): string {\n const bg = opts.background ?? \"#111827\";\n const fg = opts.color ?? \"#ffffff\";\n const pad = Math.round(size * inset);\n const inner = size - pad * 2;\n let art: string;\n if (opts.svg) {\n const href = `data:image/svg+xml;base64,${toBase64(opts.svg)}`;\n art = `<image x=\"${pad}\" y=\"${pad}\" width=\"${inner}\" height=\"${inner}\" href=\"${href}\" preserveAspectRatio=\"xMidYMid meet\"/>`;\n } else {\n const text = escapeXml((opts.monogram ?? \"\").slice(0, 3).toUpperCase());\n // Font sized to the safe inner box; a touch smaller for breathing room.\n const fontSize = Math.round(inner * (text.length >= 3 ? 0.42 : 0.52));\n art =\n `<text x=\"50%\" y=\"50%\" fill=\"${fg}\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto, sans-serif\" ` +\n `font-weight=\"700\" font-size=\"${fontSize}\" text-anchor=\"middle\" dominant-baseline=\"central\">${text}</text>`;\n }\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\">` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${bg}\"/>` +\n art +\n `</svg>`\n );\n}\n\n/**\n * Generate a full icon set (SVG) + the matching manifest `icons[]`. Deterministic,\n * zero-dep. Give it either a source `svg` or a `monogram`; it emits apple-touch\n * (180), 192, 512 by default plus a padded maskable-512.\n */\nexport function buildIconSet(opts: IconSetOptions): IconSet {\n if (!opts?.svg && !opts?.monogram) {\n throw new Error(\"buildIconSet: provide either `svg` or `monogram`\");\n }\n const base = (opts.basePath ?? \"/icons\").replace(/\\/$/, \"\");\n const sizes = opts.sizes ?? DEFAULT_SIZES;\n const files: GeneratedIcon[] = [];\n const icons: ManifestIcon[] = [];\n\n for (const size of sizes) {\n const path = `${base}/icon-${size}.svg`;\n files.push({ path, content: renderIcon(size, 0, opts), type: \"image/svg+xml\", size, purpose: \"any\" });\n // apple-touch-icon is referenced via a <link> (see pwaMetaTags), not the\n // manifest, so only real manifest sizes go into icons[].\n if (size !== 180) {\n icons.push({ src: path, sizes: `${size}x${size}`, type: \"image/svg+xml\", purpose: \"any\" });\n }\n }\n\n if (opts.maskable !== false) {\n // Maskable safe-zone: keep content within the central 80% (10% inset).\n const path = `${base}/icon-maskable-512.svg`;\n files.push({ path, content: renderIcon(512, 0.1, opts), type: \"image/svg+xml\", size: 512, purpose: \"maskable\" });\n icons.push({ src: path, sizes: \"512x512\", type: \"image/svg+xml\", purpose: \"maskable\" });\n }\n\n return { files, icons };\n}\n\n// ---------------------------------------------------------------------------\n// Head meta tags\n// ---------------------------------------------------------------------------\n\nexport interface MetaTag {\n tag: \"meta\" | \"link\";\n attrs: Record<string, string>;\n}\n\nexport interface MetaTagsOptions {\n /** `<meta name=\"theme-color\">`. Default `#ffffff`. */\n themeColor?: string;\n /** apple-touch-icon href. Default `/icons/icon-180.svg`. */\n appleTouchIcon?: string;\n /** Manifest href for the `<link rel=\"manifest\">`. Default `/manifest.webmanifest`. */\n manifest?: string;\n /** apple-mobile-web-app-title. Default: omitted. */\n title?: string;\n /** apple-mobile-web-app-status-bar-style. Default `default`. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Emit the `*-web-app-capable` tags. Default `true`. */\n capable?: boolean;\n}\n\n/**\n * The head tags a PWA needs beyond the manifest link — returned as typed\n * descriptors so a consumer renders them in Next.js `metadata`, a Hono/JSX\n * head, or plain HTML. No framework assumption.\n */\nexport function pwaMetaTags(opts: MetaTagsOptions = {}): MetaTag[] {\n const tags: MetaTag[] = [];\n tags.push({ tag: \"link\", attrs: { rel: \"manifest\", href: opts.manifest ?? \"/manifest.webmanifest\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"theme-color\", content: opts.themeColor ?? \"#ffffff\" } });\n tags.push({ tag: \"link\", attrs: { rel: \"apple-touch-icon\", href: opts.appleTouchIcon ?? \"/icons/icon-180.svg\" } });\n if (opts.capable !== false) {\n tags.push({ tag: \"meta\", attrs: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\n tags.push({\n tag: \"meta\",\n attrs: { name: \"apple-mobile-web-app-status-bar-style\", content: opts.statusBarStyle ?? \"default\" },\n });\n }\n if (opts.title) {\n tags.push({ tag: \"meta\", attrs: { name: \"apple-mobile-web-app-title\", content: opts.title } });\n }\n return tags;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/pwa",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Fleet PWA primitive: a framework-agnostic service-worker update-detection controller (new-version-available lifecycle) + a React hook and unstyled banner skeleton + a service-worker skip-waiting helper. Zero runtime deps; ship-dark.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,6 +28,11 @@
28
28
  "import": "./dist/sw.js",
29
29
  "require": "./dist/sw.cjs"
30
30
  },
31
+ "./manifest": {
32
+ "types": "./dist/manifest.d.ts",
33
+ "import": "./dist/manifest.js",
34
+ "require": "./dist/manifest.cjs"
35
+ },
31
36
  "./preact": {
32
37
  "types": "./dist/preact.d.ts",
33
38
  "import": "./dist/preact.js",