@021.is/brand-studio 0.3.0 → 0.6.1

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/AGENTS.md CHANGED
@@ -46,16 +46,26 @@ src/
46
46
  - **Named exports only.** **No CSS framework lock-in** — inline `style` / `var(--md-*)`, accept `className`.
47
47
  - Sections theme from `var(--md-<role>)`, never from a hand-authored palette.
48
48
 
49
+ ## The downloads contract — MANDATORY (v0.4+)
50
+
51
+ Every consumer ships icon + logo download URLs under `/brand/download`. Wire `BrandConfig.downloads = buildStandardDownloads({ basePath })` (or supply explicit `BrandDownload[]`). The lib renders the link grid; the host app serves each URL — one Next.js route handler that renders the SVG sized + recoloured per path params is the canonical pattern.
52
+
53
+ - URL shape: `<basePath>/<kind>/<size>/<mode>.<format>` — `kind` ∈ {icon, logo} · `mode` ∈ {light, dark} · `format` defaults to `svg`.
54
+ - Canonical sizes: icon 64/128/256/512/1024 (square edge); logo 32/48/64/96/128 (height).
55
+ - Consumers that don't wire `downloads` fall back to the legacy on-click SVG-blob generator with a banner pointing at this contract. The fallback is for migration only — every published app must wire the matrix.
56
+ - Enforce: `grep -L "buildStandardDownloads\|downloads:" brand.config.ts` in every consumer repo's CI = regression.
57
+
49
58
  ## Build + publish
50
59
 
51
- - `bun run build` (tsup + `scripts/add-use-client.mjs`) → `dist/`. `bun run typecheck` + `bun run lint` green before commit.
52
- - `npm pack` → tgz (consumers currently vendor the tgz under `<app>/vendor/`). To publish: `npm publish` to GitHub Packages needs `NODE_AUTH_TOKEN` with `write:packages` (same as `@021is/spine-*`). **Only publish on Edvard's explicit OK.**
53
- - Bump `version` on any change; consumers re-vendor the new tgz + `bun install`.
60
+ - `bun run build` (tsup + `scripts/add-use-client.mjs`) → `dist/`. `bun run typecheck` + `bun run lint` + `bun run test` green before commit.
61
+ - `npm pack` → tgz (`021is-brand-studio-<version>.tgz`). Publish destination is **`registry.npmjs.org`** as `@021.is/brand-studio` (verified live for 0.3.0 earlier note about GitHub Packages is stale). `npm publish` needs Edvard's classic npm token + **his explicit OK** — never auto-publish.
62
+ - Bump `version` on every change; consumers update via `bun add @021.is/brand-studio@<version>`.
54
63
 
55
64
  ## Active threads
56
65
 
57
- - **v0.3.0 (current) — generic + MD3.** ✅ Ripped out all zeropost/postal content (icons, mailbox marks, FlagDroop/Stamp motions, forest/cream defaults). Added the MD3 color engine + seed-based `defineBrand`. Sections render consumer content (Icons/Motion) + the full MD3 role set (Colors). `private:false`, publishable.
58
- - **v0.4.0 — export CLI.** `npx brand-studio export ./brand.config.ts` icon.svg / apple-icon.png / og.png (satori/resvg).
66
+ - **v0.4.0 (current) — downloads URL contract.** ✅ Added `BrandConfig.downloads` + `buildStandardDownloads()` helper + rewrote `<DownloadSection>` as a URL link grid (open · copy · save). Legacy on-click SVG generator kept as fallback for v0.3.x consumers. zeropost is the reference wiring.
67
+ - **v0.3.0 — generic + MD3.** Ripped out all zeropost/postal content (icons, mailbox marks, FlagDroop/Stamp motions, forest/cream defaults). Added the MD3 color engine + seed-based `defineBrand`. `private:false`, publishable. Live on npm 2026-06-03.
68
+ - **v0.5.0 — export CLI.** `npx brand-studio export ./brand.config.ts` → icon.svg / apple-icon.png / og.png (satori/resvg). Complements the runtime URL contract for static-export use cases.
59
69
  - **v1.0.0 — visual editor + plugin API.**
60
70
 
61
71
  ## See
package/README.md CHANGED
@@ -54,6 +54,32 @@ export default defineBrand({
54
54
 
55
55
  `defineBrand` eagerly resolves `config.scheme` (the full `{ light, dark }` role set) so downstream consumers never wait on a derivation.
56
56
 
57
+ ## Expose the download matrix (mandatory, v0.4+)
58
+
59
+ Every consumer ships a `/brand/download` link grid for the **app icon** and **logo** at **multiple sizes** in **both light and dark** variants. Wire `BrandConfig.downloads` with `buildStandardDownloads(basePath)` and serve the URLs from your host app:
60
+
61
+ ```ts
62
+ // brand.config.ts
63
+ import { defineBrand, buildStandardDownloads } from "@021.is/brand-studio/define";
64
+
65
+ export default defineBrand({
66
+ name: "Acme",
67
+ // ... color, type, voice ...
68
+ downloads: buildStandardDownloads({ basePath: "/brand/download" }),
69
+ });
70
+ ```
71
+
72
+ Default URL shape: `<basePath>/<kind>/<size>/<mode>.<format>`
73
+ Example: `/brand/download/icon/256/light.svg` · `/brand/download/logo/64/dark.svg`
74
+
75
+ Canonical matrix:
76
+ - **Icon** sizes: 64 / 128 / 256 / 512 / 1024 (square edge px)
77
+ - **Logo** sizes: 32 / 48 / 64 / 96 / 128 (height px)
78
+ - Each size in both `light` and `dark`
79
+ - Format: `svg` by default (PNG opt-in via the `formats` option)
80
+
81
+ The host app serves each URL — typically one Next.js route handler that renders the SVG mark sized + recoloured per the path params. Consumers that don't wire `downloads` keep working: the Download section falls back to the v0.3.x on-click SVG-blob generator, with a banner pointing at this section.
82
+
57
83
  ## Mount the brand page
58
84
 
59
85
  ```tsx
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { B as BrandConfig } from './types-PWNYyaBF.js';
3
+ import { B as BrandConfig } from './types-BycZ2igw.js';
4
4
 
5
5
  type BrandIcon = {
6
6
  node: ReactNode;
@@ -1,7 +1,7 @@
1
- export { B as BrandIcon, a as BrandStudio, b as BrandStudioProps } from '../BrandStudio-D2DcT8Fu.js';
1
+ export { B as BrandIcon, a as BrandStudio, b as BrandStudioProps } from '../BrandStudio-D1QR4hIC.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ReactNode } from 'react';
4
- import { a as BrandStudioSection } from '../types-PWNYyaBF.js';
4
+ import { c as BrandStudioSection } from '../types-BycZ2igw.js';
5
5
  export { c as contrastBadge, a as contrastRatio, r as relativeLuminance } from '../contrast-TVW3pzdd.js';
6
6
  import '../generateScheme-BDDcIzA3.js';
7
7
 
package/dist/app/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use client";
2
- export { BrandStudio, Navigation, buildNavigation } from '../chunk-Z2DJJJDC.js';
3
- import '../chunk-55AYEWNQ.js';
2
+ export { BrandStudio, Navigation, buildNavigation } from '../chunk-L4FQAO3K.js';
3
+ import '../chunk-YDZA26YU.js';
4
+ import '../chunk-MDTU2JR5.js';
4
5
  export { contrastBadge, contrastRatio, relativeLuminance } from '../chunk-JQV3ASME.js';
@@ -0,0 +1,50 @@
1
+ import { DownloadKind, DownloadFormat, DownloadMode } from './chunk-MDTU2JR5.js';
2
+ import { generateScheme } from './chunk-JQV3ASME.js';
3
+
4
+ // src/define/defineBrand.ts
5
+ function defineBrand(config) {
6
+ return { ...config, scheme: config.scheme ?? generateScheme(config.color) };
7
+ }
8
+
9
+ // src/define/downloads.ts
10
+ var STANDARD_ICON_SIZES = [64, 128, 256, 512, 1024];
11
+ var STANDARD_LOGO_SIZES = [32, 48, 64, 96, 128];
12
+ var KIND_PATH = {
13
+ [DownloadKind.Icon]: "app-icon",
14
+ [DownloadKind.Logo]: "logo"
15
+ };
16
+ function buildStandardDownloads(input) {
17
+ const brand = slugify(input.brand);
18
+ if (!brand) throw new Error("buildStandardDownloads: `brand` is required (non-empty after slug)");
19
+ const basePath = (input.basePath ?? "/brand").replace(/\/+$/, "");
20
+ const iconSizes = input.iconSizes ?? STANDARD_ICON_SIZES;
21
+ const logoSizes = input.logoSizes ?? STANDARD_LOGO_SIZES;
22
+ const formats = input.formats ?? [DownloadFormat.Svg];
23
+ const modes = [DownloadMode.Light, DownloadMode.Dark];
24
+ const items = [];
25
+ for (const kind of [DownloadKind.Icon, DownloadKind.Logo]) {
26
+ const sizes = kind === DownloadKind.Icon ? iconSizes : logoSizes;
27
+ const kindPath = KIND_PATH[kind];
28
+ for (const size of sizes) {
29
+ for (const mode of modes) {
30
+ for (const format of formats) {
31
+ const filename = `${brand}-${size}-${mode}.${format}`;
32
+ items.push({
33
+ kind,
34
+ mode,
35
+ size,
36
+ format,
37
+ url: `${basePath}/${kindPath}/${filename}`,
38
+ filename
39
+ });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return { items };
45
+ }
46
+ function slugify(input) {
47
+ return input.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
48
+ }
49
+
50
+ export { STANDARD_ICON_SIZES, STANDARD_LOGO_SIZES, buildStandardDownloads, defineBrand, slugify };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- import { LoadingVariant, SpinnerLoadingMark } from './chunk-55AYEWNQ.js';
2
+ import { SpinnerLoadingMark } from './chunk-YDZA26YU.js';
3
+ import { LoadingVariant } from './chunk-MDTU2JR5.js';
3
4
  import { motion } from 'framer-motion';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
5
6
 
@@ -1,4 +1,5 @@
1
- import { BrandStudioSection, SpinnerLoadingMark } from './chunk-55AYEWNQ.js';
1
+ import { SpinnerLoadingMark } from './chunk-YDZA26YU.js';
2
+ import { BrandStudioSection, DownloadMode, DownloadKind, DownloadFormat } from './chunk-MDTU2JR5.js';
2
3
  import { generateScheme, schemeToCssText, ROLE_GROUPS, contrastBadge, contrastRatio } from './chunk-JQV3ASME.js';
3
4
  import { jsx, jsxs } from 'react/jsx-runtime';
4
5
  import { useRef, useState } from 'react';
@@ -178,17 +179,6 @@ function ColorsSection({ scheme }) {
178
179
  }
179
180
  var ICON_SIZES = [64, 128, 256, 512, 1024];
180
181
  var WORDMARK_HEIGHTS = [32, 48, 64, 96, 128];
181
- function triggerDownload(svgString, filename) {
182
- const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
183
- const url = URL.createObjectURL(blob);
184
- const a = document.createElement("a");
185
- a.href = url;
186
- a.download = filename;
187
- document.body.appendChild(a);
188
- a.click();
189
- a.remove();
190
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
191
- }
192
182
  function DownloadSection({
193
183
  config,
194
184
  staticLogo,
@@ -209,7 +199,17 @@ function DownloadSection({
209
199
  const darkLabel = scheme.dark.onSurfaceVariant;
210
200
  const iconDarkNode = staticLogoDark ?? staticLogo;
211
201
  const hasDistinctDark = staticLogoDark !== void 0 && staticLogoDark !== null;
212
- function downloadIcon(mode) {
202
+ const downloads = config.downloads?.items ?? [];
203
+ const iconLight = findEntry(downloads, DownloadKind.Icon, DownloadMode.Light, iconSize);
204
+ const iconDark = findEntry(downloads, DownloadKind.Icon, DownloadMode.Dark, iconSize);
205
+ const logoLight = findEntry(downloads, DownloadKind.Logo, DownloadMode.Light, wmHeight);
206
+ const logoDark = findEntry(downloads, DownloadKind.Logo, DownloadMode.Dark, wmHeight);
207
+ async function downloadIcon(mode) {
208
+ const entry = mode === "light" ? iconLight : iconDark;
209
+ if (entry) {
210
+ await fetchAndSave(entry.url, entry.filename ?? lastSegment(entry.url));
211
+ return;
212
+ }
213
213
  const ref = mode === "light" ? iconLightRef : iconDarkRef;
214
214
  const svg = ref.current?.querySelector("svg");
215
215
  if (!svg) return;
@@ -217,12 +217,17 @@ function DownloadSection({
217
217
  clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
218
218
  clone.setAttribute("width", String(iconSize));
219
219
  clone.setAttribute("height", String(iconSize));
220
- triggerDownload(
220
+ triggerBlobDownload(
221
221
  new XMLSerializer().serializeToString(clone),
222
222
  `${config.name}-icon-${iconSize}-${mode}.svg`
223
223
  );
224
224
  }
225
225
  async function downloadWordmark(mode) {
226
+ const entry = mode === "light" ? logoLight : logoDark;
227
+ if (entry) {
228
+ await fetchAndSave(entry.url, entry.filename ?? lastSegment(entry.url));
229
+ return;
230
+ }
226
231
  await document.fonts?.ready;
227
232
  const text = measureRef.current?.querySelector("text");
228
233
  if (!text) return;
@@ -237,7 +242,7 @@ function DownloadSection({
237
242
  `<text x="${(pad - bbox.x).toFixed(1)}" y="${pad.toFixed(1)}" dominant-baseline="text-before-edge" font-family="${sans}, system-ui, sans-serif" font-weight="600" font-size="100" letter-spacing="-3" fill="${fill}">${config.name}</text>`,
238
243
  "</svg>"
239
244
  ].join("");
240
- triggerDownload(out, `${config.name}-wordmark-${wmHeight}-${mode}.svg`);
245
+ triggerBlobDownload(out, `${config.name}-wordmark-${wmHeight}-${mode}.svg`);
241
246
  }
242
247
  return /* @__PURE__ */ jsxs("section", { children: [
243
248
  /* @__PURE__ */ jsx(
@@ -257,11 +262,11 @@ function DownloadSection({
257
262
  {
258
263
  style: {
259
264
  color: "var(--md-on-surface-variant)",
260
- maxWidth: "40rem",
265
+ maxWidth: "44rem",
261
266
  margin: "0 0 2.5rem",
262
267
  lineHeight: 1.6
263
268
  },
264
- children: "Export the app icon and wordmark as standalone SVGs in light and dark variants. Pick a size, download, drop into your console branding page."
269
+ children: "Export the app icon and wordmark as standalone SVGs in light and dark variants. Pick a size, download, or copy the stable URL beneath the buttons to embed or share."
265
270
  }
266
271
  ),
267
272
  /* @__PURE__ */ jsxs(
@@ -302,6 +307,7 @@ function DownloadSection({
302
307
  }
303
308
  )
304
309
  ] }),
310
+ iconLight || iconDark ? /* @__PURE__ */ jsx(UrlPair, { light: iconLight, dark: iconDark }) : null,
305
311
  staticLogo && !hasDistinctDark ? /* @__PURE__ */ jsxs(Note, { children: [
306
312
  "No dark icon supplied \u2014 dark export reuses the light mark. Pass",
307
313
  " ",
@@ -343,7 +349,8 @@ function DownloadSection({
343
349
  children: "Dark SVG"
344
350
  }
345
351
  )
346
- ] })
352
+ ] }),
353
+ logoLight || logoDark ? /* @__PURE__ */ jsx(UrlPair, { light: logoLight, dark: logoDark }) : null
347
354
  ] })
348
355
  ]
349
356
  }
@@ -390,6 +397,139 @@ function DownloadSection({
390
397
  )
391
398
  ] });
392
399
  }
400
+ function findEntry(items, kind, mode, size) {
401
+ const entry = items.find(
402
+ (it) => it.kind === kind && it.mode === mode && it.size === size && it.format === DownloadFormat.Svg
403
+ );
404
+ return entry ?? null;
405
+ }
406
+ function triggerBlobDownload(svgString, filename) {
407
+ const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
408
+ const url = URL.createObjectURL(blob);
409
+ const a = document.createElement("a");
410
+ a.href = url;
411
+ a.download = filename;
412
+ document.body.appendChild(a);
413
+ a.click();
414
+ a.remove();
415
+ window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
416
+ }
417
+ async function fetchAndSave(url, filename) {
418
+ try {
419
+ const res = await fetch(url, { cache: "no-store" });
420
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
421
+ const blob = await res.blob();
422
+ const objectUrl = URL.createObjectURL(blob);
423
+ const a = document.createElement("a");
424
+ a.href = objectUrl;
425
+ a.download = filename;
426
+ document.body.appendChild(a);
427
+ a.click();
428
+ a.remove();
429
+ window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1e3);
430
+ } catch {
431
+ window.open(url, "_blank", "noopener,noreferrer");
432
+ }
433
+ }
434
+ function UrlPair({ light, dark }) {
435
+ return /* @__PURE__ */ jsxs(
436
+ "div",
437
+ {
438
+ style: {
439
+ marginTop: "0.7rem",
440
+ display: "grid",
441
+ gap: "0.35rem"
442
+ },
443
+ children: [
444
+ /* @__PURE__ */ jsx(UrlRow, { label: "LIGHT", entry: light }),
445
+ /* @__PURE__ */ jsx(UrlRow, { label: "DARK", entry: dark })
446
+ ]
447
+ }
448
+ );
449
+ }
450
+ function UrlRow({ label, entry }) {
451
+ const [copied, setCopied] = useState(false);
452
+ const url = entry?.url ?? null;
453
+ function onCopy() {
454
+ if (!url || typeof navigator === "undefined" || !navigator.clipboard) return;
455
+ navigator.clipboard.writeText(toAbsolute(url)).then(() => {
456
+ setCopied(true);
457
+ window.setTimeout(() => setCopied(false), 1200);
458
+ });
459
+ }
460
+ const disabled = !url;
461
+ const downloadName = entry?.filename ?? lastSegment(url ?? "");
462
+ return /* @__PURE__ */ jsxs(
463
+ "div",
464
+ {
465
+ style: {
466
+ display: "flex",
467
+ alignItems: "center",
468
+ gap: "0.4rem",
469
+ padding: "0.4rem 0.6rem",
470
+ borderRadius: "0.45rem",
471
+ background: "var(--md-surface-container-low)",
472
+ border: "1px solid var(--md-outline-variant)",
473
+ opacity: disabled ? 0.55 : 1
474
+ },
475
+ children: [
476
+ /* @__PURE__ */ jsx("span", { style: urlLabelStyle, children: label }),
477
+ /* @__PURE__ */ jsx(
478
+ "a",
479
+ {
480
+ href: url ?? "#",
481
+ target: "_blank",
482
+ rel: "noreferrer",
483
+ download: downloadName || void 0,
484
+ style: {
485
+ flex: 1,
486
+ color: "var(--md-on-surface)",
487
+ textDecoration: "none",
488
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
489
+ fontSize: "0.74rem",
490
+ letterSpacing: "0.01em",
491
+ minWidth: 0,
492
+ overflow: "hidden",
493
+ textOverflow: "ellipsis",
494
+ whiteSpace: "nowrap",
495
+ pointerEvents: disabled ? "none" : "auto"
496
+ },
497
+ title: url ?? "no URL configured for this size",
498
+ children: url ?? "\u2014"
499
+ }
500
+ ),
501
+ /* @__PURE__ */ jsx(
502
+ "button",
503
+ {
504
+ type: "button",
505
+ onClick: onCopy,
506
+ disabled,
507
+ title: "Copy URL",
508
+ style: {
509
+ ...copyButtonStyle,
510
+ cursor: disabled ? "not-allowed" : "pointer"
511
+ },
512
+ children: copied ? "Copied" : "Copy"
513
+ }
514
+ )
515
+ ]
516
+ }
517
+ );
518
+ }
519
+ function lastSegment(url) {
520
+ const noQuery = url.split("?")[0] ?? "";
521
+ const parts = noQuery.split("/");
522
+ return parts[parts.length - 1] ?? "";
523
+ }
524
+ function toAbsolute(url) {
525
+ if (/^https?:\/\//.test(url)) return url;
526
+ if (typeof window === "undefined") return url;
527
+ try {
528
+ return new URL(url, window.location.origin).toString();
529
+ } catch {
530
+ return url;
531
+ }
532
+ }
393
533
  var mono = { fontFamily: "var(--bs-mono)" };
394
534
  function NoAsset() {
395
535
  return /* @__PURE__ */ jsx("span", { style: { color: "var(--md-on-surface-variant)", fontSize: "0.8rem" }, children: "no icon" });
@@ -518,6 +658,26 @@ var eyebrowStyle = {
518
658
  color: "var(--md-primary)",
519
659
  margin: 0
520
660
  };
661
+ var urlLabelStyle = {
662
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
663
+ fontSize: "0.62rem",
664
+ letterSpacing: "0.14em",
665
+ textTransform: "uppercase",
666
+ color: "var(--md-on-surface-variant)",
667
+ flexShrink: 0,
668
+ width: "3rem"
669
+ };
670
+ var copyButtonStyle = {
671
+ padding: "0.3rem 0.55rem",
672
+ borderRadius: "0.35rem",
673
+ border: "1px solid var(--md-outline-variant)",
674
+ background: "transparent",
675
+ color: "var(--md-primary)",
676
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
677
+ fontSize: "0.68rem",
678
+ fontWeight: 600,
679
+ whiteSpace: "nowrap"
680
+ };
521
681
  function buttonStyle(disabled, kind) {
522
682
  const isPrimary = kind === "primary";
523
683
  return {
@@ -0,0 +1,35 @@
1
+ // src/types.ts
2
+ var LoadingVariant = {
3
+ Spinner: "spinner",
4
+ Ring: "ring",
5
+ Pulse: "pulse"
6
+ };
7
+ var BrandStudioSection = {
8
+ Overview: "overview",
9
+ Logo: "logo",
10
+ Colors: "colors",
11
+ Typography: "typography",
12
+ Icons: "icons",
13
+ Motion: "motion",
14
+ Download: "download"
15
+ };
16
+ var MotionState = {
17
+ Idle: "idle",
18
+ Loading: "loading",
19
+ Success: "success",
20
+ Empty: "empty"
21
+ };
22
+ var DownloadKind = {
23
+ Icon: "icon",
24
+ Logo: "logo"
25
+ };
26
+ var DownloadMode = {
27
+ Light: "light",
28
+ Dark: "dark"
29
+ };
30
+ var DownloadFormat = {
31
+ Svg: "svg",
32
+ Png: "png"
33
+ };
34
+
35
+ export { BrandStudioSection, DownloadFormat, DownloadKind, DownloadMode, LoadingVariant, MotionState };
@@ -2,27 +2,6 @@
2
2
  import { motion } from 'framer-motion';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
 
5
- // src/types.ts
6
- var LoadingVariant = {
7
- Spinner: "spinner",
8
- Ring: "ring",
9
- Pulse: "pulse"
10
- };
11
- var BrandStudioSection = {
12
- Overview: "overview",
13
- Logo: "logo",
14
- Colors: "colors",
15
- Typography: "typography",
16
- Icons: "icons",
17
- Motion: "motion",
18
- Download: "download"
19
- };
20
- var MotionState = {
21
- Idle: "idle",
22
- Loading: "loading",
23
- Success: "success",
24
- Empty: "empty"
25
- };
26
5
  function SpinnerLoadingMark({
27
6
  size = 200,
28
7
  background = "transparent",
@@ -81,4 +60,4 @@ function SpinnerLoadingMark({
81
60
  );
82
61
  }
83
62
 
84
- export { BrandStudioSection, LoadingVariant, MotionState, SpinnerLoadingMark };
63
+ export { SpinnerLoadingMark };
@@ -1,5 +1,5 @@
1
- import { B as BrandConfig, R as ResolvedBrandConfig } from '../types-PWNYyaBF.js';
2
- export { b as BrandType } from '../types-PWNYyaBF.js';
1
+ import { B as BrandConfig, R as ResolvedBrandConfig, D as DownloadFormat, b as BrandDownloads } from '../types-BycZ2igw.js';
2
+ export { a as BrandDownload, d as BrandType, e as DownloadKind, f as DownloadMode } from '../types-BycZ2igw.js';
3
3
  export { B as BrandScheme, C as ColorScheme, a as ColorSpec, H as Hex, d as Role, S as SchemeVariant, g as generateScheme } from '../generateScheme-BDDcIzA3.js';
4
4
 
5
5
  /**
@@ -22,4 +22,46 @@ export { B as BrandScheme, C as ColorScheme, a as ColorSpec, H as Hex, d as Role
22
22
  */
23
23
  declare function defineBrand(config: BrandConfig): ResolvedBrandConfig;
24
24
 
25
- export { BrandConfig, ResolvedBrandConfig, defineBrand };
25
+ /** Canonical icon export sizes square pixel edges. */
26
+ declare const STANDARD_ICON_SIZES: readonly [64, 128, 256, 512, 1024];
27
+ /** Canonical logo export sizes — pixel heights. */
28
+ declare const STANDARD_LOGO_SIZES: readonly [32, 48, 64, 96, 128];
29
+ type StandardDownloadsInput = {
30
+ /**
31
+ * The brand slug embedded in every URL. Required — this is the SEO win;
32
+ * `/brand/app-icon/zeropost-256-light.svg` indexes far better than
33
+ * `/brand/light.svg`. Pass the lowercased brand name (e.g. `"zeropost"`)
34
+ * or any slug. Auto-slugified to lowercase ASCII + hyphens.
35
+ */
36
+ brand: string;
37
+ /**
38
+ * Mount path the host app serves the assets under, no trailing slash.
39
+ * Default `/brand`. Full URL shape:
40
+ * `<basePath>/<kind-path>/<brand>-<size>-<mode>.<format>`
41
+ * Example: `/brand/app-icon/zeropost-256-light.svg`
42
+ * Kind paths: `app-icon` (square mark) · `logo` (wordmark).
43
+ *
44
+ * The kind segment never collides with brand-studio's own section routes
45
+ * (`/brand/logo`, `/brand/colors`, …) because assets are two-segment URLs
46
+ * (`/brand/logo/<slug>.svg`) while sections are one-segment (`/brand/logo`).
47
+ * The host route handler at `app/brand/[kind]/[slug]/route.ts` beats the
48
+ * brand-page catch-all for two-segment URLs.
49
+ */
50
+ basePath?: string;
51
+ /** Icon edges in px. Default `STANDARD_ICON_SIZES`. */
52
+ iconSizes?: readonly number[];
53
+ /** Logo heights in px. Default `STANDARD_LOGO_SIZES`. */
54
+ logoSizes?: readonly number[];
55
+ /** Formats to emit per (kind, size, mode). Default `["svg"]`. */
56
+ formats?: readonly (typeof DownloadFormat)[keyof typeof DownloadFormat][];
57
+ };
58
+ /**
59
+ * Build the canonical icon + logo × {light, dark} × sizes × formats matrix.
60
+ * Every brand-studio consumer ships this (or a superset) so users always have
61
+ * shareable, SEO-friendly URLs to the brand marks.
62
+ */
63
+ declare function buildStandardDownloads(input: StandardDownloadsInput): BrandDownloads;
64
+ /** Lowercase ASCII, replace non-alphanumeric with hyphens, collapse + trim. */
65
+ declare function slugify(input: string): string;
66
+
67
+ export { BrandConfig, BrandDownloads, DownloadFormat, ResolvedBrandConfig, STANDARD_ICON_SIZES, STANDARD_LOGO_SIZES, type StandardDownloadsInput, buildStandardDownloads, defineBrand, slugify };
@@ -1,2 +1,3 @@
1
- export { defineBrand } from '../chunk-QT5N4K7D.js';
1
+ export { STANDARD_ICON_SIZES, STANDARD_LOGO_SIZES, buildStandardDownloads, defineBrand, slugify } from '../chunk-6J2NFZLN.js';
2
+ export { DownloadFormat, DownloadKind, DownloadMode } from '../chunk-MDTU2JR5.js';
2
3
  export { SchemeVariant, generateScheme } from '../chunk-JQV3ASME.js';
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export { Disc, DiscProps, Ring, RingProps, Squircle, SquircleProps } from './shapes/index.js';
2
2
  export { AppearGrow, AppearGrowProps, Drift, DriftProps, PulseLoop, PulseLoopProps, RingDraw, RingDrawProps, Sequence, SequenceProps, SequenceStep, SequenceStepProps, Strikethrough, StrikethroughProps, Sweep, SweepDirection, SweepProps, Wobble, WobbleProps } from './motion/index.js';
3
3
  export { LoadingMark, LoadingMarkProps, PulseLoadingMark, PulseLoadingMarkProps, RingLoadingMark, RingLoadingMarkProps, SpinnerLoadingMark, SpinnerLoadingMarkProps } from './loading/index.js';
4
- export { B as BrandConfig, a as BrandStudioSection, b as BrandType, L as LoadingVariant, M as MotionState, R as ResolvedBrandConfig } from './types-PWNYyaBF.js';
4
+ export { B as BrandConfig, a as BrandDownload, b as BrandDownloads, c as BrandStudioSection, d as BrandType, D as DownloadFormat, e as DownloadKind, f as DownloadMode, L as LoadingVariant, M as MotionState, R as ResolvedBrandConfig } from './types-BycZ2igw.js';
5
5
  export { B as BrandScheme, C as ColorScheme, a as ColorSpec, H as Hex, R as ROLE, b as ROLE_GROUPS, c as ROLE_ORDER, d as Role, S as SchemeVariant, g as generateScheme } from './generateScheme-BDDcIzA3.js';
6
6
  export { cssVarName, schemeToCssText, schemeVars } from './color/index.js';
7
7
  export { C as ContrastTier, c as contrastBadge, a as contrastRatio, r as relativeLuminance } from './contrast-TVW3pzdd.js';
8
- export { a as BrandStudio, b as BrandStudioProps } from './BrandStudio-D2DcT8Fu.js';
9
- export { defineBrand } from './define/index.js';
8
+ export { a as BrandStudio, b as BrandStudioProps } from './BrandStudio-D1QR4hIC.js';
9
+ export { STANDARD_ICON_SIZES, STANDARD_LOGO_SIZES, StandardDownloadsInput, buildStandardDownloads, defineBrand, slugify } from './define/index.js';
10
10
  import 'react/jsx-runtime';
11
11
  import 'react';
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  export { Disc, Ring, Squircle } from './chunk-LJ4HZCAP.js';
3
3
  export { AppearGrow, Drift, PulseLoop, RingDraw, Sequence, SequenceStep, Strikethrough, Sweep, Wobble } from './chunk-7325RQRP.js';
4
- export { LoadingMark, PulseLoadingMark, RingLoadingMark } from './chunk-ZE5UZAY6.js';
5
- export { BrandStudio } from './chunk-Z2DJJJDC.js';
6
- export { BrandStudioSection, LoadingVariant, MotionState, SpinnerLoadingMark } from './chunk-55AYEWNQ.js';
7
- export { defineBrand } from './chunk-QT5N4K7D.js';
4
+ export { LoadingMark, PulseLoadingMark, RingLoadingMark } from './chunk-F4CFQDS7.js';
5
+ export { BrandStudio } from './chunk-L4FQAO3K.js';
6
+ export { SpinnerLoadingMark } from './chunk-YDZA26YU.js';
7
+ export { STANDARD_ICON_SIZES, STANDARD_LOGO_SIZES, buildStandardDownloads, defineBrand, slugify } from './chunk-6J2NFZLN.js';
8
+ export { BrandStudioSection, DownloadFormat, DownloadKind, DownloadMode, LoadingVariant, MotionState } from './chunk-MDTU2JR5.js';
8
9
  export { ContrastTier, ROLE, ROLE_GROUPS, ROLE_ORDER, SchemeVariant, contrastBadge, contrastRatio, cssVarName, generateScheme, relativeLuminance, schemeToCssText, schemeVars } from './chunk-JQV3ASME.js';
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
- import { L as LoadingVariant } from '../types-PWNYyaBF.js';
3
+ import { L as LoadingVariant } from '../types-BycZ2igw.js';
4
4
  import '../generateScheme-BDDcIzA3.js';
5
5
 
6
6
  type LoadingMarkProps = {
@@ -1,3 +1,4 @@
1
1
  "use client";
2
- export { LoadingMark, PulseLoadingMark, RingLoadingMark } from '../chunk-ZE5UZAY6.js';
3
- export { LoadingVariant, SpinnerLoadingMark } from '../chunk-55AYEWNQ.js';
2
+ export { LoadingMark, PulseLoadingMark, RingLoadingMark } from '../chunk-F4CFQDS7.js';
3
+ export { SpinnerLoadingMark } from '../chunk-YDZA26YU.js';
4
+ export { LoadingVariant } from '../chunk-MDTU2JR5.js';
@@ -0,0 +1,119 @@
1
+ import { a as ColorSpec, B as BrandScheme } from './generateScheme-BDDcIzA3.js';
2
+
3
+ /**
4
+ * Shared types for @021.is/brand-studio.
5
+ *
6
+ * Const-as-object enums (no inline string-literal unions in public signatures).
7
+ */
8
+
9
+ /** Generic loading-mark variants. No brand-coded marks live in the lib. */
10
+ declare const LoadingVariant: {
11
+ readonly Spinner: "spinner";
12
+ readonly Ring: "ring";
13
+ readonly Pulse: "pulse";
14
+ };
15
+ type LoadingVariant = (typeof LoadingVariant)[keyof typeof LoadingVariant];
16
+ declare const BrandStudioSection: {
17
+ readonly Overview: "overview";
18
+ readonly Logo: "logo";
19
+ readonly Colors: "colors";
20
+ readonly Typography: "typography";
21
+ readonly Icons: "icons";
22
+ readonly Motion: "motion";
23
+ readonly Download: "download";
24
+ };
25
+ type BrandStudioSection = (typeof BrandStudioSection)[keyof typeof BrandStudioSection];
26
+ declare const MotionState: {
27
+ readonly Idle: "idle";
28
+ readonly Loading: "loading";
29
+ readonly Success: "success";
30
+ readonly Empty: "empty";
31
+ };
32
+ type MotionState = (typeof MotionState)[keyof typeof MotionState];
33
+ /**
34
+ * Mandatory download contract — every brand-studio consumer SHOULD expose its
35
+ * app icon + logo at multiple sizes, in both light- and dark-surface variants,
36
+ * as shareable URLs. The lib renders them as a link grid on `/brand/download`.
37
+ *
38
+ * Host apps wire `BrandConfig.downloads` to a route handler (or static files)
39
+ * that returns the asset. Use `buildStandardDownloads(basePath)` for the
40
+ * canonical 5×2×2 matrix.
41
+ */
42
+ declare const DownloadKind: {
43
+ readonly Icon: "icon";
44
+ readonly Logo: "logo";
45
+ };
46
+ type DownloadKind = (typeof DownloadKind)[keyof typeof DownloadKind];
47
+ declare const DownloadMode: {
48
+ readonly Light: "light";
49
+ readonly Dark: "dark";
50
+ };
51
+ type DownloadMode = (typeof DownloadMode)[keyof typeof DownloadMode];
52
+ declare const DownloadFormat: {
53
+ readonly Svg: "svg";
54
+ readonly Png: "png";
55
+ };
56
+ type DownloadFormat = (typeof DownloadFormat)[keyof typeof DownloadFormat];
57
+ type BrandDownload = {
58
+ kind: DownloadKind;
59
+ mode: DownloadMode;
60
+ /** Pixel size — square edge for icons, height for logos. */
61
+ size: number;
62
+ /** Asset format. SVG is the canonical export; PNG is optional raster. */
63
+ format: DownloadFormat;
64
+ /** Absolute or root-relative URL the host app serves. */
65
+ url: string;
66
+ /**
67
+ * Filename the user should see when downloading. Used as the
68
+ * `<a download="…">` attribute. Defaults to the URL's last segment.
69
+ */
70
+ filename?: string;
71
+ /** Optional override for the link label. */
72
+ label?: string;
73
+ };
74
+ type BrandDownloads = {
75
+ /** Linear list of download entries. Order is preserved for the link grid. */
76
+ items: readonly BrandDownload[];
77
+ };
78
+ type BrandType = {
79
+ sans: string;
80
+ mono?: string;
81
+ serif?: string;
82
+ };
83
+ type BrandConfig = {
84
+ /** Brand display name, shown on the overview + exports. */
85
+ name: string;
86
+ /** Short tagline shown on the brand-kit overview. */
87
+ tagline?: string;
88
+ /** Operator legal entity, shown in the brand-kit footer. */
89
+ operator?: string;
90
+ /** Domain (used for "view live" links). */
91
+ domain?: string;
92
+ /**
93
+ * Color spec — a seed (+ optional key colors / overrides) the MD3 engine
94
+ * derives a complete light + dark role set from. See `defineBrand`.
95
+ */
96
+ color: ColorSpec;
97
+ type: BrandType;
98
+ /** Voice rules, rendered as a bullet list. */
99
+ voice?: readonly string[];
100
+ /**
101
+ * Download URL contract — icon + logo at multiple sizes × {light, dark}.
102
+ * Rendered as a link grid under `/brand/download`. Use
103
+ * `buildStandardDownloads(basePath)` for the canonical matrix or supply
104
+ * explicit entries. When absent, the Download section falls back to
105
+ * on-click SVG-blob generation (legacy behaviour).
106
+ */
107
+ downloads?: BrandDownloads;
108
+ /**
109
+ * Resolved MD3 light + dark role sets. Populated by `defineBrand`; do not set
110
+ * by hand. Sections read it (and the injected `--md-*` CSS vars) for theming.
111
+ */
112
+ scheme?: BrandScheme;
113
+ };
114
+ /** A `BrandConfig` after `defineBrand` has resolved its `scheme`. */
115
+ type ResolvedBrandConfig = BrandConfig & {
116
+ scheme: BrandScheme;
117
+ };
118
+
119
+ export { type BrandConfig as B, DownloadFormat as D, LoadingVariant as L, MotionState as M, type ResolvedBrandConfig as R, type BrandDownload as a, type BrandDownloads as b, BrandStudioSection as c, type BrandType as d, DownloadKind as e, DownloadMode as f };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@021.is/brand-studio",
3
- "version": "0.3.0",
4
- "description": "Generic MD3 brand-kit toolkit: seed → full light + dark role set, composable SVG shapes + motion + loading marks, and a mountable <BrandStudio> /brand page. Ships no brand content — every app supplies its own logo, icons, and seed colour.",
3
+ "version": "0.6.1",
4
+ "description": "Generic MD3 brand-kit toolkit: seed → full light + dark role set, composable SVG shapes + motion + loading marks, mountable <BrandStudio> /brand page, mandatory icon + logo download-URL contract. Ships no brand content — every app supplies its own logo, icons, and seed colour.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "edvone",
@@ -1,8 +0,0 @@
1
- import { generateScheme } from './chunk-JQV3ASME.js';
2
-
3
- // src/define/defineBrand.ts
4
- function defineBrand(config) {
5
- return { ...config, scheme: config.scheme ?? generateScheme(config.color) };
6
- }
7
-
8
- export { defineBrand };
@@ -1,66 +0,0 @@
1
- import { a as ColorSpec, B as BrandScheme } from './generateScheme-BDDcIzA3.js';
2
-
3
- /**
4
- * Shared types for @021.is/brand-studio.
5
- *
6
- * Const-as-object enums (no inline string-literal unions in public signatures).
7
- */
8
-
9
- /** Generic loading-mark variants. No brand-coded marks live in the lib. */
10
- declare const LoadingVariant: {
11
- readonly Spinner: "spinner";
12
- readonly Ring: "ring";
13
- readonly Pulse: "pulse";
14
- };
15
- type LoadingVariant = (typeof LoadingVariant)[keyof typeof LoadingVariant];
16
- declare const BrandStudioSection: {
17
- readonly Overview: "overview";
18
- readonly Logo: "logo";
19
- readonly Colors: "colors";
20
- readonly Typography: "typography";
21
- readonly Icons: "icons";
22
- readonly Motion: "motion";
23
- readonly Download: "download";
24
- };
25
- type BrandStudioSection = (typeof BrandStudioSection)[keyof typeof BrandStudioSection];
26
- declare const MotionState: {
27
- readonly Idle: "idle";
28
- readonly Loading: "loading";
29
- readonly Success: "success";
30
- readonly Empty: "empty";
31
- };
32
- type MotionState = (typeof MotionState)[keyof typeof MotionState];
33
- type BrandType = {
34
- sans: string;
35
- mono?: string;
36
- serif?: string;
37
- };
38
- type BrandConfig = {
39
- /** Brand display name, shown on the overview + exports. */
40
- name: string;
41
- /** Short tagline shown on the brand-kit overview. */
42
- tagline?: string;
43
- /** Operator legal entity, shown in the brand-kit footer. */
44
- operator?: string;
45
- /** Domain (used for "view live" links). */
46
- domain?: string;
47
- /**
48
- * Color spec — a seed (+ optional key colors / overrides) the MD3 engine
49
- * derives a complete light + dark role set from. See `defineBrand`.
50
- */
51
- color: ColorSpec;
52
- type: BrandType;
53
- /** Voice rules, rendered as a bullet list. */
54
- voice?: readonly string[];
55
- /**
56
- * Resolved MD3 light + dark role sets. Populated by `defineBrand`; do not set
57
- * by hand. Sections read it (and the injected `--md-*` CSS vars) for theming.
58
- */
59
- scheme?: BrandScheme;
60
- };
61
- /** A `BrandConfig` after `defineBrand` has resolved its `scheme`. */
62
- type ResolvedBrandConfig = BrandConfig & {
63
- scheme: BrandScheme;
64
- };
65
-
66
- export { type BrandConfig as B, LoadingVariant as L, MotionState as M, type ResolvedBrandConfig as R, BrandStudioSection as a, type BrandType as b };