@021.is/brand-studio 0.3.0 → 0.6.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/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-JGLPUAM6.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,6 +199,11 @@ function DownloadSection({
209
199
  const darkLabel = scheme.dark.onSurfaceVariant;
210
200
  const iconDarkNode = staticLogoDark ?? staticLogo;
211
201
  const hasDistinctDark = staticLogoDark !== void 0 && staticLogoDark !== null;
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);
212
207
  function downloadIcon(mode) {
213
208
  const ref = mode === "light" ? iconLightRef : iconDarkRef;
214
209
  const svg = ref.current?.querySelector("svg");
@@ -217,7 +212,7 @@ function DownloadSection({
217
212
  clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
218
213
  clone.setAttribute("width", String(iconSize));
219
214
  clone.setAttribute("height", String(iconSize));
220
- triggerDownload(
215
+ triggerBlobDownload(
221
216
  new XMLSerializer().serializeToString(clone),
222
217
  `${config.name}-icon-${iconSize}-${mode}.svg`
223
218
  );
@@ -237,7 +232,7 @@ function DownloadSection({
237
232
  `<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
233
  "</svg>"
239
234
  ].join("");
240
- triggerDownload(out, `${config.name}-wordmark-${wmHeight}-${mode}.svg`);
235
+ triggerBlobDownload(out, `${config.name}-wordmark-${wmHeight}-${mode}.svg`);
241
236
  }
242
237
  return /* @__PURE__ */ jsxs("section", { children: [
243
238
  /* @__PURE__ */ jsx(
@@ -257,11 +252,11 @@ function DownloadSection({
257
252
  {
258
253
  style: {
259
254
  color: "var(--md-on-surface-variant)",
260
- maxWidth: "40rem",
255
+ maxWidth: "44rem",
261
256
  margin: "0 0 2.5rem",
262
257
  lineHeight: 1.6
263
258
  },
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."
259
+ 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
260
  }
266
261
  ),
267
262
  /* @__PURE__ */ jsxs(
@@ -302,6 +297,7 @@ function DownloadSection({
302
297
  }
303
298
  )
304
299
  ] }),
300
+ iconLight || iconDark ? /* @__PURE__ */ jsx(UrlPair, { light: iconLight, dark: iconDark }) : null,
305
301
  staticLogo && !hasDistinctDark ? /* @__PURE__ */ jsxs(Note, { children: [
306
302
  "No dark icon supplied \u2014 dark export reuses the light mark. Pass",
307
303
  " ",
@@ -343,7 +339,8 @@ function DownloadSection({
343
339
  children: "Dark SVG"
344
340
  }
345
341
  )
346
- ] })
342
+ ] }),
343
+ logoLight || logoDark ? /* @__PURE__ */ jsx(UrlPair, { light: logoLight, dark: logoDark }) : null
347
344
  ] })
348
345
  ]
349
346
  }
@@ -390,6 +387,122 @@ function DownloadSection({
390
387
  )
391
388
  ] });
392
389
  }
390
+ function findEntry(items, kind, mode, size) {
391
+ const entry = items.find(
392
+ (it) => it.kind === kind && it.mode === mode && it.size === size && it.format === DownloadFormat.Svg
393
+ );
394
+ return entry ?? null;
395
+ }
396
+ function triggerBlobDownload(svgString, filename) {
397
+ const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
398
+ const url = URL.createObjectURL(blob);
399
+ const a = document.createElement("a");
400
+ a.href = url;
401
+ a.download = filename;
402
+ document.body.appendChild(a);
403
+ a.click();
404
+ a.remove();
405
+ window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
406
+ }
407
+ function UrlPair({ light, dark }) {
408
+ return /* @__PURE__ */ jsxs(
409
+ "div",
410
+ {
411
+ style: {
412
+ marginTop: "0.7rem",
413
+ display: "grid",
414
+ gap: "0.35rem"
415
+ },
416
+ children: [
417
+ /* @__PURE__ */ jsx(UrlRow, { label: "LIGHT", entry: light }),
418
+ /* @__PURE__ */ jsx(UrlRow, { label: "DARK", entry: dark })
419
+ ]
420
+ }
421
+ );
422
+ }
423
+ function UrlRow({ label, entry }) {
424
+ const [copied, setCopied] = useState(false);
425
+ const url = entry?.url ?? null;
426
+ function onCopy() {
427
+ if (!url || typeof navigator === "undefined" || !navigator.clipboard) return;
428
+ navigator.clipboard.writeText(toAbsolute(url)).then(() => {
429
+ setCopied(true);
430
+ window.setTimeout(() => setCopied(false), 1200);
431
+ });
432
+ }
433
+ const disabled = !url;
434
+ const downloadName = entry?.filename ?? lastSegment(url ?? "");
435
+ return /* @__PURE__ */ jsxs(
436
+ "div",
437
+ {
438
+ style: {
439
+ display: "flex",
440
+ alignItems: "center",
441
+ gap: "0.4rem",
442
+ padding: "0.4rem 0.6rem",
443
+ borderRadius: "0.45rem",
444
+ background: "var(--md-surface-container-low)",
445
+ border: "1px solid var(--md-outline-variant)",
446
+ opacity: disabled ? 0.55 : 1
447
+ },
448
+ children: [
449
+ /* @__PURE__ */ jsx("span", { style: urlLabelStyle, children: label }),
450
+ /* @__PURE__ */ jsx(
451
+ "a",
452
+ {
453
+ href: url ?? "#",
454
+ target: "_blank",
455
+ rel: "noreferrer",
456
+ download: downloadName || void 0,
457
+ style: {
458
+ flex: 1,
459
+ color: "var(--md-on-surface)",
460
+ textDecoration: "none",
461
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
462
+ fontSize: "0.74rem",
463
+ letterSpacing: "0.01em",
464
+ minWidth: 0,
465
+ overflow: "hidden",
466
+ textOverflow: "ellipsis",
467
+ whiteSpace: "nowrap",
468
+ pointerEvents: disabled ? "none" : "auto"
469
+ },
470
+ title: url ?? "no URL configured for this size",
471
+ children: url ?? "\u2014"
472
+ }
473
+ ),
474
+ /* @__PURE__ */ jsx(
475
+ "button",
476
+ {
477
+ type: "button",
478
+ onClick: onCopy,
479
+ disabled,
480
+ title: "Copy URL",
481
+ style: {
482
+ ...copyButtonStyle,
483
+ cursor: disabled ? "not-allowed" : "pointer"
484
+ },
485
+ children: copied ? "Copied" : "Copy"
486
+ }
487
+ )
488
+ ]
489
+ }
490
+ );
491
+ }
492
+ function lastSegment(url) {
493
+ const noQuery = url.split("?")[0] ?? "";
494
+ const parts = noQuery.split("/");
495
+ return parts[parts.length - 1] ?? "";
496
+ }
497
+ function toAbsolute(url) {
498
+ if (/^https?:\/\//.test(url)) return url;
499
+ if (typeof window === "undefined") return url;
500
+ try {
501
+ return new URL(url, window.location.origin).toString();
502
+ } catch {
503
+ return url;
504
+ }
505
+ }
393
506
  var mono = { fontFamily: "var(--bs-mono)" };
394
507
  function NoAsset() {
395
508
  return /* @__PURE__ */ jsx("span", { style: { color: "var(--md-on-surface-variant)", fontSize: "0.8rem" }, children: "no icon" });
@@ -518,6 +631,26 @@ var eyebrowStyle = {
518
631
  color: "var(--md-primary)",
519
632
  margin: 0
520
633
  };
634
+ var urlLabelStyle = {
635
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
636
+ fontSize: "0.62rem",
637
+ letterSpacing: "0.14em",
638
+ textTransform: "uppercase",
639
+ color: "var(--md-on-surface-variant)",
640
+ flexShrink: 0,
641
+ width: "3rem"
642
+ };
643
+ var copyButtonStyle = {
644
+ padding: "0.3rem 0.55rem",
645
+ borderRadius: "0.35rem",
646
+ border: "1px solid var(--md-outline-variant)",
647
+ background: "transparent",
648
+ color: "var(--md-primary)",
649
+ fontFamily: "var(--bs-mono, ui-monospace, monospace)",
650
+ fontSize: "0.68rem",
651
+ fontWeight: 600,
652
+ whiteSpace: "nowrap"
653
+ };
521
654
  function buttonStyle(disabled, kind) {
522
655
  const isPrimary = kind === "primary";
523
656
  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-JGLPUAM6.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.0",
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 };