@tangle-network/brand 1.24.2 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.25.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3ed6fb1: Add shared interaction timing roles beside the existing motion vocabulary. Consolidate shared design policy in the design bible; consumer adoption remains separate.
8
+ - 3d3ba7a: Publish opt-in expressive typography and shared introductory presentation. Export canonical knot data and safe static SVG without React, and reuse that geometry in the React mark.
9
+
3
10
  ## 1.24.2
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tangle-network/brand
2
2
 
3
- The authoring owner of shared Tangle color, typography, radius, motion, shadow and status decisions. Generic components live in `@tangle-network/ui`; product composition stays in its product repository. See the [brand guidelines](../../docs/brand-guidelines.md) for visual, copy and component standards.
3
+ The authoring owner of shared Tangle color, typography, radius, motion, shadow and status decisions. Generic components live in `@tangle-network/ui`; product composition stays in its product repository. Read the [design bible](../../docs/design-bible.md) for shared policy and the [brand guidelines](../../docs/brand-guidelines.md) for implementation contracts.
4
4
 
5
5
  ## Install
6
6
 
@@ -107,7 +107,7 @@ The base surface ladder is the one GTM proved out: dark is indigo-lifted `#0a0a1
107
107
 
108
108
  Chrome radii are 6/8/10/12px in both base modes; the composer has its own 26px role. Inter (variable, then static) leads body and display stacks, with Geist fallbacks; Geist Mono leads code with JetBrains Mono and system fallbacks. Runtime font/radius registrations retain their public override names; tests keep Tailwind registration defaults aligned with the canonical declarations.
109
109
 
110
- Existing `--transition-*` values are preserved. The shared `--duration-*`, `--ease-*` and `--motion-*` vocabulary comes from the existing Agent App contract. It does not apply animations automatically. Reduced motion zeroes those durations and re-resolves composites at theme boundaries.
110
+ Existing `--transition-*` values are preserved. The shared `--duration-*`, `--ease-*` and `--motion-*` vocabulary comes from the existing Agent App contract. It does not apply animations automatically. Reduced motion zeroes those durations and re-resolves composites at theme boundaries. The adjacent `--interaction-*` roles are elapsed acknowledgment/loading thresholds for existing product instrumentation, not animation durations; reduced motion does not change them.
111
111
 
112
112
  Focus uses `--focus-border`, `--focus-halo`, their danger variants, and `--border-strong`, derived from the local ring/border palette. UI's `focusField` / `focusRing` consume them. `globals.css` supplies the existing fallback for controls without their own focus treatment.
113
113
 
@@ -142,3 +142,32 @@ pnpm test
142
142
  For actual browser cascade checks, serve the repository with any local static server and open `packages/brand/scripts/theme-browser.html`. Repeat with light/dark color preferences and reduced motion. The page reports assertions for both default contracts, all named modes, nested scopes, aliases and immediate code-color changes. This CSS fixture does not substitute for the React SSR/hydration tests or a consuming app build.
143
143
 
144
144
  For local consumer iteration, use workspace linking or a `file:` dependency, then run that consumer's real build and browser path before release.
145
+
146
+ ## Expressive introductions and framework-neutral knot
147
+
148
+ Opt in with `data-typography="expressive"` on the application root. Load Outfit,
149
+ Geist and Geist Mono from your own origin using `@font-face` with
150
+ `font-display: swap`; the default Inter contract stays available. Theme boundaries
151
+ rebind the opt-in stacks, so changing between Tangle dark and light keeps identity.
152
+
153
+ `@tangle-network/brand/styles` includes the presentation recipes. Consumers that
154
+ import individual files also import `@tangle-network/brand/styles/presentation.css`
155
+ after tokens, theme and named themes. Use `tangle-intro`, `tangle-display`,
156
+ `tangle-intro-description`, `tangle-intro-actions` and `tangle-control-display`.
157
+ `data-grid`, `data-gradient` and `data-motion` opt into atmosphere; reading and
158
+ operational surfaces stay solid. Reduced motion removes expressive movement.
159
+ The display control reads shared role tokens; operational sizes are unchanged.
160
+
161
+ Hono and static consumers import `{ knot, knotSvg }` from
162
+ `@tangle-network/brand/knot`. That entry requires neither React nor a DOM.
163
+ `raw(knotSvg({ id: "request-unique-mark" }))` is safe static SVG with escaped
164
+ labels; IDs must be unique per inline instance. Omit `label` for decoration,
165
+ supply it for informative marks. React uses the same geometry in `TangleKnot`;
166
+ `label=""` makes it decorative while the existing default accessible name remains.
167
+ Consumers must remove their copied paths and palette/type definitions on adoption.
168
+
169
+ `scripts/design-contract-proof.mjs` measures real React and Hono rendering with
170
+ one set of recipes. It captures 390/1440px light/dark, contrast, font loading,
171
+ contained width, reduced motion, control roles and path parity. `DESIGN_FONT_DIR`
172
+ points to a consumer's self-hosted font assets; `DESIGN_EVIDENCE` selects a retained
173
+ output directory. The baseline Hono capture explicitly records the prior export gap.
@@ -114,7 +114,7 @@
114
114
  "local_primitives": {}
115
115
  },
116
116
  "sandbox": {
117
- "repo": "tangle-network/agent-dev-container",
117
+ "repo": "tangle-network/agent-container",
118
118
  "roots": [
119
119
  "products/sandbox/web"
120
120
  ],
@@ -236,7 +236,7 @@
236
236
  "transcript_type_override": 0
237
237
  },
238
238
  "platform": {
239
- "repo": "tangle-network/agent-dev-container",
239
+ "repo": "tangle-network/agent-container",
240
240
  "roots": [
241
241
  "products/platform/web"
242
242
  ],
@@ -497,7 +497,7 @@
497
497
  "transcript_type_override": 0
498
498
  },
499
499
  "intelligence": {
500
- "repo": "tangle-network/agent-dev-container",
500
+ "repo": "tangle-network/agent-container",
501
501
  "roots": [
502
502
  "products/intelligence/web"
503
503
  ],
@@ -28,10 +28,11 @@ export const DEFAULT_BASELINE = fileURLToPath(new URL("./drift-baseline.json", i
28
28
 
29
29
  /** surface → GitHub repository and web roots. The default branch is resolved from the remote. */
30
30
  export const SURFACES = {
31
+ router: { repo: "tangle-network/tangle-router", roots: ["src/views", "app/globals.css"] },
31
32
  website: { repo: "tangle-network/tangle-website", roots: ["src"] },
32
- sandbox: { repo: "tangle-network/agent-dev-container", roots: ["products/sandbox/web"] },
33
- platform: { repo: "tangle-network/agent-dev-container", roots: ["products/platform/web"] },
34
- intelligence: { repo: "tangle-network/agent-dev-container", roots: ["products/intelligence/web"] },
33
+ sandbox: { repo: "tangle-network/agent-container", roots: ["products/sandbox/web"] },
34
+ platform: { repo: "tangle-network/agent-container", roots: ["products/platform/web"] },
35
+ intelligence: { repo: "tangle-network/agent-container", roots: ["products/intelligence/web"] },
35
36
  gtm: { repo: "tangle-network/gtm-agent", roots: [""] },
36
37
  tax: { repo: "tangle-network/tax-agent", roots: ["apps/web"] },
37
38
  legal: { repo: "tangle-network/legal-agent", roots: [""] },
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { knot, knotGradientIds, knotSvg } from "./knot.js";
1
2
  import * as React from "react";
2
3
  //#region src/logo.d.ts
3
4
  interface LogoProps {
@@ -6,10 +7,13 @@ interface LogoProps {
6
7
  suffix?: string | null;
7
8
  className?: string;
8
9
  }
9
- export declare function TangleKnot({ size, className }: {
10
+ interface TangleKnotProps {
10
11
  size?: number;
11
12
  className?: string;
12
- }): React.JSX.Element;
13
+ /** Omit for a decorative mark. Supply a name when the mark conveys identity. */
14
+ label?: string;
15
+ }
16
+ export declare function TangleKnot({ size, className, label }: TangleKnotProps): React.JSX.Element;
13
17
  export declare function Logo({ size, variant, suffix, className }: LogoProps): React.JSX.Element;
14
18
  //#endregion
15
19
  //#region src/palette.generated.d.ts
@@ -183,4 +187,4 @@ export declare const palettes: {
183
187
  };
184
188
  };
185
189
  //#endregion
186
- export type { BrandPalette, LogoProps };
190
+ export { type BrandPalette, type LogoProps, type TangleKnotProps, knot, knotGradientIds, knotSvg };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { knot, knotGradientIds, knotSvg } from "./knot.js";
1
2
  import * as React from "react";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/logo.tsx
@@ -27,52 +28,35 @@ const sizeConfig = {
27
28
  gap: "gap-3"
28
29
  }
29
30
  };
30
- function TangleKnot({ size = 32, className }) {
31
+ function TangleKnot({ size = 32, className, label = "Tangle" }) {
31
32
  const uid = React.useId();
32
- const g1 = `tk-a-${uid}`;
33
- const g2 = `tk-b-${uid}`;
33
+ const ids = knotGradientIds(`tk-${uid}`);
34
34
  return /* @__PURE__ */ jsxs("svg", {
35
35
  width: size,
36
36
  height: size,
37
- viewBox: "20 45 156 140",
37
+ viewBox: knot.viewBox,
38
38
  fill: "none",
39
39
  xmlns: "http://www.w3.org/2000/svg",
40
40
  className,
41
- "aria-label": "Tangle",
41
+ focusable: "false",
42
+ role: label ? "img" : void 0,
43
+ "aria-label": label || void 0,
44
+ "aria-hidden": !label || void 0,
42
45
  children: [
43
- /* @__PURE__ */ jsx("title", { children: "Tangle" }),
44
- /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
45
- id: g1,
46
- x1: "36.79",
47
- y1: "58.78",
48
- x2: "145.99",
49
- y2: "183.59",
46
+ label && /* @__PURE__ */ jsx("title", { children: label }),
47
+ /* @__PURE__ */ jsx("defs", { children: knot.gradients.map((g, i) => /* @__PURE__ */ jsx("linearGradient", {
48
+ id: ids[i],
49
+ ...g,
50
50
  gradientUnits: "userSpaceOnUse",
51
- children: [/* @__PURE__ */ jsx("stop", { stopColor: "#8E59FF" }), /* @__PURE__ */ jsx("stop", {
52
- offset: "1",
53
- stopColor: "#6888F9"
54
- })]
55
- }), /* @__PURE__ */ jsxs("linearGradient", {
56
- id: g2,
57
- x1: "36.78",
58
- y1: "169.38",
59
- x2: "145.99",
60
- y2: "44.57",
61
- gradientUnits: "userSpaceOnUse",
62
- children: [/* @__PURE__ */ jsx("stop", { stopColor: "#8E59FF" }), /* @__PURE__ */ jsx("stop", {
63
- offset: "1",
64
- stopColor: "#6888F9"
65
- })]
66
- })] }),
67
- /* @__PURE__ */ jsx("path", {
68
- d: "M170.74 150.082C170.747 142.848 167.464 135.908 161.612 130.784C155.76 125.66 147.818 122.772 139.528 122.754L104.921 122.754C104.704 119.939 104.567 117.062 104.567 114.078C104.567 111.095 104.429 108.231 104.236 105.403L139.528 105.403C147.746 105.288 155.582 102.359 161.347 97.2467C167.112 92.135 170.343 85.2506 170.343 78.0787C170.343 70.9069 167.112 64.0225 161.347 58.9107C155.582 53.799 147.746 50.8696 139.528 50.7545L56.817 50.7545C48.5993 50.8696 40.7628 53.799 34.9981 58.9107C29.2334 64.0225 26.0025 70.9069 26.0025 78.0787C26.0025 85.2506 29.2334 92.135 34.9981 97.2467C40.7628 102.359 48.5993 105.288 56.817 105.403L89.3238 106.163C91.5854 106.163 91.7785 111.095 91.7785 114.078C91.7785 117.062 91.9156 119.925 92.1091 122.754L56.817 122.754C48.5993 122.869 40.7628 125.798 34.9981 130.91C29.2334 136.022 26.0025 142.906 26.0025 150.078C26.0025 157.25 29.2334 164.134 34.9981 169.246C40.7628 174.358 48.5993 177.287 56.817 177.402L139.528 177.402C147.816 177.384 155.757 174.497 161.609 169.374C167.461 164.252 170.745 157.314 170.74 150.082ZM157.952 78.0752C157.954 82.3443 156.016 86.4397 152.562 89.463C149.108 92.4863 144.42 94.1905 139.528 94.2017L102.881 94.2017C101.554 86.2073 99.0244 78.4005 95.3585 70.9829C93.7421 67.7691 91.6728 64.7438 89.1983 61.9769L139.528 61.9769C144.415 61.988 149.097 63.6884 152.55 66.7056C156.003 69.7228 157.946 73.811 157.952 78.0752ZM56.817 94.2017C51.9157 94.2017 47.2151 92.5027 43.7493 89.4784C40.2835 86.4541 38.3364 82.3522 38.3364 78.0752C38.3364 73.7982 40.2835 69.6964 43.7493 66.6721C47.2151 63.6478 51.9157 61.9487 56.817 61.9487L65.1139 61.9487C72.4433 61.9487 79.3856 67.0146 83.7074 75.5071C86.6658 81.491 88.77 87.7697 89.9724 94.2017L56.817 94.2017ZM38.3929 150.082C38.3907 145.812 40.3293 141.717 43.7834 138.694C47.2374 135.67 51.9248 133.966 56.817 133.955L93.4637 133.955C94.7912 141.949 97.3206 149.756 100.987 157.174C102.603 160.388 104.672 163.413 107.147 166.18L56.817 166.18C51.9304 166.169 47.2479 164.468 43.7948 161.451C40.3416 158.434 38.3993 154.346 38.3929 150.082ZM112.686 152.65C109.728 146.666 107.623 140.387 106.421 133.955L139.528 133.955C144.429 133.955 149.13 135.654 152.596 138.678C156.062 141.703 158.009 145.804 158.009 150.082C158.009 154.359 156.062 158.46 152.596 161.485C149.13 164.509 144.429 166.208 139.528 166.208L131.231 166.208C123.934 166.208 116.992 161.142 112.686 152.65Z",
69
- fill: `url(#${g1})`
70
- }),
71
- /* @__PURE__ */ jsx("path", {
72
- opacity: "0.8",
73
- d: "M170.74 78.0752C170.747 85.3089 167.463 92.2492 161.612 97.3728C155.76 102.496 147.817 105.384 139.527 105.403L104.92 105.403C104.702 108.217 104.565 111.095 104.565 114.078C104.565 117.062 104.428 119.925 104.235 122.754L139.527 122.754C147.745 122.869 155.582 125.798 161.347 130.91C167.112 136.022 170.342 142.906 170.342 150.078C170.342 157.25 167.112 164.134 161.347 169.246C155.582 174.358 147.745 177.287 139.527 177.402L56.8149 177.402C48.597 177.287 40.7604 174.358 34.9956 169.246C29.2307 164.134 25.9998 157.25 25.9998 150.078C25.9998 142.906 29.2307 136.022 34.9956 130.91C40.7604 125.798 48.597 122.869 56.8149 122.754L89.3223 121.994C91.5839 121.994 91.777 117.062 91.777 114.078C91.777 111.095 91.9141 108.231 92.1076 105.403L56.8149 105.403C48.597 105.288 40.7604 102.359 34.9956 97.2468C29.2307 92.135 25.9998 85.2506 25.9998 78.0788C25.9998 70.9069 29.2307 64.0225 34.9956 58.9108C40.7604 53.799 48.597 50.8696 56.8149 50.7545L139.527 50.7545C147.816 50.7731 155.757 53.6602 161.609 58.7823C167.46 63.9043 170.744 70.8428 170.74 78.0752ZM157.952 150.082C157.954 145.812 156.015 141.717 152.561 138.694C149.107 135.67 144.42 133.966 139.527 133.955L102.88 133.955C101.552 141.949 99.023 149.756 95.3571 157.174C93.7406 160.388 91.6712 163.413 89.1968 166.18L139.527 166.18C144.414 166.169 149.097 164.468 152.55 161.451C156.003 158.434 157.945 154.346 157.952 150.082ZM56.8149 133.955C51.9134 133.955 47.2127 135.654 43.7469 138.678C40.2811 141.703 38.334 145.805 38.334 150.082C38.334 154.359 40.2811 158.46 43.7469 161.485C47.2127 164.509 51.9134 166.208 56.8149 166.208L65.1119 166.208C72.4414 166.208 79.3838 161.142 83.7057 152.65C86.6642 146.666 88.7684 140.387 89.9708 133.955L56.8149 133.955ZM38.3904 78.0752C38.3883 82.3443 40.3269 86.4398 43.781 89.4631C47.2351 92.4863 51.9226 94.1905 56.8149 94.2017L93.4622 94.2017C94.7898 86.2073 97.3192 78.4005 100.985 70.983C102.602 67.7692 104.671 64.7438 107.145 61.9769L56.8149 61.9769C51.9282 61.9881 47.2456 63.6884 43.7924 66.7056C40.3392 69.7228 38.3968 73.8111 38.3904 78.0752ZM112.685 75.5071C109.726 81.491 107.622 87.7697 106.42 94.2017L139.527 94.2017C144.429 94.2017 149.13 92.5027 152.595 89.4784C156.061 86.4541 158.008 82.3522 158.008 78.0752C158.008 73.7982 156.061 69.6964 152.595 66.6721C149.13 63.6478 144.429 61.9488 139.527 61.9488L131.23 61.9488C123.933 61.9488 116.991 67.0147 112.685 75.5071Z",
74
- fill: `url(#${g2})`
75
- })
51
+ children: knot.stops.map((color, index) => /* @__PURE__ */ jsx("stop", {
52
+ offset: index,
53
+ stopColor: color
54
+ }, color))
55
+ }, ids[i])) }),
56
+ knot.paths.map((path, i) => /* @__PURE__ */ jsx("path", {
57
+ ...path,
58
+ fill: `url(#${ids[i]})`
59
+ }, i))
76
60
  ]
77
61
  });
78
62
  }
@@ -242,4 +226,4 @@ const palettes = {
242
226
  }
243
227
  };
244
228
  //#endregion
245
- export { Logo, TangleKnot, palettes };
229
+ export { Logo, TangleKnot, knot, knotGradientIds, knotSvg, palettes };
package/dist/knot.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ //#region src/knot.d.ts
2
+ /** Canonical geometry. This entry has no React or browser dependencies. */
3
+ export declare const knot: {
4
+ readonly viewBox: "20 45 156 140";
5
+ readonly gradients: readonly [{
6
+ readonly x1: "36.79";
7
+ readonly y1: "58.78";
8
+ readonly x2: "145.99";
9
+ readonly y2: "183.59";
10
+ }, {
11
+ readonly x1: "36.78";
12
+ readonly y1: "169.38";
13
+ readonly x2: "145.99";
14
+ readonly y2: "44.57";
15
+ }];
16
+ readonly stops: readonly ["#8E59FF", "#6888F9"];
17
+ readonly paths: readonly [{
18
+ readonly d: "M170.74 150.082C170.747 142.848 167.464 135.908 161.612 130.784C155.76 125.66 147.818 122.772 139.528 122.754L104.921 122.754C104.704 119.939 104.567 117.062 104.567 114.078C104.567 111.095 104.429 108.231 104.236 105.403L139.528 105.403C147.746 105.288 155.582 102.359 161.347 97.2467C167.112 92.135 170.343 85.2506 170.343 78.0787C170.343 70.9069 167.112 64.0225 161.347 58.9107C155.582 53.799 147.746 50.8696 139.528 50.7545L56.817 50.7545C48.5993 50.8696 40.7628 53.799 34.9981 58.9107C29.2334 64.0225 26.0025 70.9069 26.0025 78.0787C26.0025 85.2506 29.2334 92.135 34.9981 97.2467C40.7628 102.359 48.5993 105.288 56.817 105.403L89.3238 106.163C91.5854 106.163 91.7785 111.095 91.7785 114.078C91.7785 117.062 91.9156 119.925 92.1091 122.754L56.817 122.754C48.5993 122.869 40.7628 125.798 34.9981 130.91C29.2334 136.022 26.0025 142.906 26.0025 150.078C26.0025 157.25 29.2334 164.134 34.9981 169.246C40.7628 174.358 48.5993 177.287 56.817 177.402L139.528 177.402C147.816 177.384 155.757 174.497 161.609 169.374C167.461 164.252 170.745 157.314 170.74 150.082ZM157.952 78.0752C157.954 82.3443 156.016 86.4397 152.562 89.463C149.108 92.4863 144.42 94.1905 139.528 94.2017L102.881 94.2017C101.554 86.2073 99.0244 78.4005 95.3585 70.9829C93.7421 67.7691 91.6728 64.7438 89.1983 61.9769L139.528 61.9769C144.415 61.988 149.097 63.6884 152.55 66.7056C156.003 69.7228 157.946 73.811 157.952 78.0752ZM56.817 94.2017C51.9157 94.2017 47.2151 92.5027 43.7493 89.4784C40.2835 86.4541 38.3364 82.3522 38.3364 78.0752C38.3364 73.7982 40.2835 69.6964 43.7493 66.6721C47.2151 63.6478 51.9157 61.9487 56.817 61.9487L65.1139 61.9487C72.4433 61.9487 79.3856 67.0146 83.7074 75.5071C86.6658 81.491 88.77 87.7697 89.9724 94.2017L56.817 94.2017ZM38.3929 150.082C38.3907 145.812 40.3293 141.717 43.7834 138.694C47.2374 135.67 51.9248 133.966 56.817 133.955L93.4637 133.955C94.7912 141.949 97.3206 149.756 100.987 157.174C102.603 160.388 104.672 163.413 107.147 166.18L56.817 166.18C51.9304 166.169 47.2479 164.468 43.7948 161.451C40.3416 158.434 38.3993 154.346 38.3929 150.082ZM112.686 152.65C109.728 146.666 107.623 140.387 106.421 133.955L139.528 133.955C144.429 133.955 149.13 135.654 152.596 138.678C156.062 141.703 158.009 145.804 158.009 150.082C158.009 154.359 156.062 158.46 152.596 161.485C149.13 164.509 144.429 166.208 139.528 166.208L131.231 166.208C123.934 166.208 116.992 161.142 112.686 152.65Z";
19
+ readonly opacity: 1;
20
+ }, {
21
+ readonly d: "M170.74 78.0752C170.747 85.3089 167.463 92.2492 161.612 97.3728C155.76 102.496 147.817 105.384 139.527 105.403L104.92 105.403C104.702 108.217 104.565 111.095 104.565 114.078C104.565 117.062 104.428 119.925 104.235 122.754L139.527 122.754C147.745 122.869 155.582 125.798 161.347 130.91C167.112 136.022 170.342 142.906 170.342 150.078C170.342 157.25 167.112 164.134 161.347 169.246C155.582 174.358 147.745 177.287 139.527 177.402L56.8149 177.402C48.597 177.287 40.7604 174.358 34.9956 169.246C29.2307 164.134 25.9998 157.25 25.9998 150.078C25.9998 142.906 29.2307 136.022 34.9956 130.91C40.7604 125.798 48.597 122.869 56.8149 122.754L89.3223 121.994C91.5839 121.994 91.777 117.062 91.777 114.078C91.777 111.095 91.9141 108.231 92.1076 105.403L56.8149 105.403C48.597 105.288 40.7604 102.359 34.9956 97.2468C29.2307 92.135 25.9998 85.2506 25.9998 78.0788C25.9998 70.9069 29.2307 64.0225 34.9956 58.9108C40.7604 53.799 48.597 50.8696 56.8149 50.7545L139.527 50.7545C147.816 50.7731 155.757 53.6602 161.609 58.7823C167.46 63.9043 170.744 70.8428 170.74 78.0752ZM157.952 150.082C157.954 145.812 156.015 141.717 152.561 138.694C149.107 135.67 144.42 133.966 139.527 133.955L102.88 133.955C101.552 141.949 99.023 149.756 95.3571 157.174C93.7406 160.388 91.6712 163.413 89.1968 166.18L139.527 166.18C144.414 166.169 149.097 164.468 152.55 161.451C156.003 158.434 157.945 154.346 157.952 150.082ZM56.8149 133.955C51.9134 133.955 47.2127 135.654 43.7469 138.678C40.2811 141.703 38.334 145.805 38.334 150.082C38.334 154.359 40.2811 158.46 43.7469 161.485C47.2127 164.509 51.9134 166.208 56.8149 166.208L65.1119 166.208C72.4414 166.208 79.3838 161.142 83.7057 152.65C86.6642 146.666 88.7684 140.387 89.9708 133.955L56.8149 133.955ZM38.3904 78.0752C38.3883 82.3443 40.3269 86.4398 43.781 89.4631C47.2351 92.4863 51.9226 94.1905 56.8149 94.2017L93.4622 94.2017C94.7898 86.2073 97.3192 78.4005 100.985 70.983C102.602 67.7692 104.671 64.7438 107.145 61.9769L56.8149 61.9769C51.9282 61.9881 47.2456 63.6884 43.7924 66.7056C40.3392 69.7228 38.3968 73.8111 38.3904 78.0752ZM112.685 75.5071C109.726 81.491 107.622 87.7697 106.42 94.2017L139.527 94.2017C144.429 94.2017 149.13 92.5027 152.595 89.4784C156.061 86.4541 158.008 82.3522 158.008 78.0752C158.008 73.7982 156.061 69.6964 152.595 66.6721C149.13 63.6478 144.429 61.9488 139.527 61.9488L131.23 61.9488C123.933 61.9488 116.991 67.0147 112.685 75.5071Z";
22
+ readonly opacity: 0.8;
23
+ }];
24
+ };
25
+ /** Prefix with an instance/request identifier when more than one mark is inline. */
26
+ export declare function knotGradientIds(id: string): [string, string];
27
+ /** Safe static markup for Hono's raw(), static sites and SVG responses. */
28
+ export declare function knotSvg({ id, size, label }: {
29
+ id: string;
30
+ size?: number;
31
+ label?: string;
32
+ }): string;
33
+ //#endregion
package/dist/knot.js ADDED
@@ -0,0 +1,45 @@
1
+ //#region src/knot.ts
2
+ /** Canonical geometry. This entry has no React or browser dependencies. */
3
+ const knot = {
4
+ viewBox: "20 45 156 140",
5
+ gradients: [{
6
+ x1: "36.79",
7
+ y1: "58.78",
8
+ x2: "145.99",
9
+ y2: "183.59"
10
+ }, {
11
+ x1: "36.78",
12
+ y1: "169.38",
13
+ x2: "145.99",
14
+ y2: "44.57"
15
+ }],
16
+ stops: ["#8E59FF", "#6888F9"],
17
+ paths: [{
18
+ "d": "M170.74 150.082C170.747 142.848 167.464 135.908 161.612 130.784C155.76 125.66 147.818 122.772 139.528 122.754L104.921 122.754C104.704 119.939 104.567 117.062 104.567 114.078C104.567 111.095 104.429 108.231 104.236 105.403L139.528 105.403C147.746 105.288 155.582 102.359 161.347 97.2467C167.112 92.135 170.343 85.2506 170.343 78.0787C170.343 70.9069 167.112 64.0225 161.347 58.9107C155.582 53.799 147.746 50.8696 139.528 50.7545L56.817 50.7545C48.5993 50.8696 40.7628 53.799 34.9981 58.9107C29.2334 64.0225 26.0025 70.9069 26.0025 78.0787C26.0025 85.2506 29.2334 92.135 34.9981 97.2467C40.7628 102.359 48.5993 105.288 56.817 105.403L89.3238 106.163C91.5854 106.163 91.7785 111.095 91.7785 114.078C91.7785 117.062 91.9156 119.925 92.1091 122.754L56.817 122.754C48.5993 122.869 40.7628 125.798 34.9981 130.91C29.2334 136.022 26.0025 142.906 26.0025 150.078C26.0025 157.25 29.2334 164.134 34.9981 169.246C40.7628 174.358 48.5993 177.287 56.817 177.402L139.528 177.402C147.816 177.384 155.757 174.497 161.609 169.374C167.461 164.252 170.745 157.314 170.74 150.082ZM157.952 78.0752C157.954 82.3443 156.016 86.4397 152.562 89.463C149.108 92.4863 144.42 94.1905 139.528 94.2017L102.881 94.2017C101.554 86.2073 99.0244 78.4005 95.3585 70.9829C93.7421 67.7691 91.6728 64.7438 89.1983 61.9769L139.528 61.9769C144.415 61.988 149.097 63.6884 152.55 66.7056C156.003 69.7228 157.946 73.811 157.952 78.0752ZM56.817 94.2017C51.9157 94.2017 47.2151 92.5027 43.7493 89.4784C40.2835 86.4541 38.3364 82.3522 38.3364 78.0752C38.3364 73.7982 40.2835 69.6964 43.7493 66.6721C47.2151 63.6478 51.9157 61.9487 56.817 61.9487L65.1139 61.9487C72.4433 61.9487 79.3856 67.0146 83.7074 75.5071C86.6658 81.491 88.77 87.7697 89.9724 94.2017L56.817 94.2017ZM38.3929 150.082C38.3907 145.812 40.3293 141.717 43.7834 138.694C47.2374 135.67 51.9248 133.966 56.817 133.955L93.4637 133.955C94.7912 141.949 97.3206 149.756 100.987 157.174C102.603 160.388 104.672 163.413 107.147 166.18L56.817 166.18C51.9304 166.169 47.2479 164.468 43.7948 161.451C40.3416 158.434 38.3993 154.346 38.3929 150.082ZM112.686 152.65C109.728 146.666 107.623 140.387 106.421 133.955L139.528 133.955C144.429 133.955 149.13 135.654 152.596 138.678C156.062 141.703 158.009 145.804 158.009 150.082C158.009 154.359 156.062 158.46 152.596 161.485C149.13 164.509 144.429 166.208 139.528 166.208L131.231 166.208C123.934 166.208 116.992 161.142 112.686 152.65Z",
19
+ "opacity": 1
20
+ }, {
21
+ "d": "M170.74 78.0752C170.747 85.3089 167.463 92.2492 161.612 97.3728C155.76 102.496 147.817 105.384 139.527 105.403L104.92 105.403C104.702 108.217 104.565 111.095 104.565 114.078C104.565 117.062 104.428 119.925 104.235 122.754L139.527 122.754C147.745 122.869 155.582 125.798 161.347 130.91C167.112 136.022 170.342 142.906 170.342 150.078C170.342 157.25 167.112 164.134 161.347 169.246C155.582 174.358 147.745 177.287 139.527 177.402L56.8149 177.402C48.597 177.287 40.7604 174.358 34.9956 169.246C29.2307 164.134 25.9998 157.25 25.9998 150.078C25.9998 142.906 29.2307 136.022 34.9956 130.91C40.7604 125.798 48.597 122.869 56.8149 122.754L89.3223 121.994C91.5839 121.994 91.777 117.062 91.777 114.078C91.777 111.095 91.9141 108.231 92.1076 105.403L56.8149 105.403C48.597 105.288 40.7604 102.359 34.9956 97.2468C29.2307 92.135 25.9998 85.2506 25.9998 78.0788C25.9998 70.9069 29.2307 64.0225 34.9956 58.9108C40.7604 53.799 48.597 50.8696 56.8149 50.7545L139.527 50.7545C147.816 50.7731 155.757 53.6602 161.609 58.7823C167.46 63.9043 170.744 70.8428 170.74 78.0752ZM157.952 150.082C157.954 145.812 156.015 141.717 152.561 138.694C149.107 135.67 144.42 133.966 139.527 133.955L102.88 133.955C101.552 141.949 99.023 149.756 95.3571 157.174C93.7406 160.388 91.6712 163.413 89.1968 166.18L139.527 166.18C144.414 166.169 149.097 164.468 152.55 161.451C156.003 158.434 157.945 154.346 157.952 150.082ZM56.8149 133.955C51.9134 133.955 47.2127 135.654 43.7469 138.678C40.2811 141.703 38.334 145.805 38.334 150.082C38.334 154.359 40.2811 158.46 43.7469 161.485C47.2127 164.509 51.9134 166.208 56.8149 166.208L65.1119 166.208C72.4414 166.208 79.3838 161.142 83.7057 152.65C86.6642 146.666 88.7684 140.387 89.9708 133.955L56.8149 133.955ZM38.3904 78.0752C38.3883 82.3443 40.3269 86.4398 43.781 89.4631C47.2351 92.4863 51.9226 94.1905 56.8149 94.2017L93.4622 94.2017C94.7898 86.2073 97.3192 78.4005 100.985 70.983C102.602 67.7692 104.671 64.7438 107.145 61.9769L56.8149 61.9769C51.9282 61.9881 47.2456 63.6884 43.7924 66.7056C40.3392 69.7228 38.3968 73.8111 38.3904 78.0752ZM112.685 75.5071C109.726 81.491 107.622 87.7697 106.42 94.2017L139.527 94.2017C144.429 94.2017 149.13 92.5027 152.595 89.4784C156.061 86.4541 158.008 82.3522 158.008 78.0752C158.008 73.7982 156.061 69.6964 152.595 66.6721C149.13 63.6478 144.429 61.9488 139.527 61.9488L131.23 61.9488C123.933 61.9488 116.991 67.0147 112.685 75.5071Z",
22
+ "opacity": .8
23
+ }]
24
+ };
25
+ /** Prefix with an instance/request identifier when more than one mark is inline. */
26
+ function knotGradientIds(id) {
27
+ if (!/^[a-zA-Z_][a-zA-Z0-9_:.-]*$/.test(id)) throw new Error("A safe, unique knot id is required");
28
+ return [`${id}-a`, `${id}-b`];
29
+ }
30
+ const escape = (text) => text.replace(/[&<>"']/g, (c) => ({
31
+ "&": "&amp;",
32
+ "<": "&lt;",
33
+ ">": "&gt;",
34
+ "\"": "&quot;",
35
+ "'": "&#39;"
36
+ })[c]);
37
+ /** Safe static markup for Hono's raw(), static sites and SVG responses. */
38
+ function knotSvg({ id, size = 32, label }) {
39
+ if (!Number.isFinite(size) || size <= 0) throw new Error("Knot size must be positive");
40
+ const ids = knotGradientIds(id);
41
+ const accessibility = label ? `role="img" aria-label="${escape(label)}"` : "aria-hidden=\"true\"";
42
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${knot.viewBox}" fill="none" focusable="false" ${accessibility}><defs>${knot.gradients.map((g, i) => `<linearGradient id="${ids[i]}" x1="${g.x1}" y1="${g.y1}" x2="${g.x2}" y2="${g.y2}" gradientUnits="userSpaceOnUse">${knot.stops.map((color, index) => `<stop offset="${index}" stop-color="${color}"/>`).join("")}</linearGradient>`).join("")}</defs>${knot.paths.map((path, i) => `<path d="${path.d}" opacity="${path.opacity}" fill="url(#${ids[i]})"/>`).join("")}</svg>`;
43
+ }
44
+ //#endregion
45
+ export { knot, knotGradientIds, knotSvg };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/brand",
3
- "version": "1.24.2",
3
+ "version": "1.25.0",
4
4
  "description": "Tangle brand system — design tokens, logo, Tailwind v4 theme. Single source of truth for every Tangle app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,7 +44,13 @@
44
44
  "./styles/named-themes.css": "./src/styles/named-themes.css",
45
45
  "./styles/ladders.css": "./src/styles/ladders.css",
46
46
  "./styles/system.css": "./src/styles/system.css",
47
- "./styles/legacy-light.css": "./src/styles/legacy-light.css"
47
+ "./styles/legacy-light.css": "./src/styles/legacy-light.css",
48
+ "./knot": {
49
+ "types": "./dist/knot.d.ts",
50
+ "import": "./dist/knot.js",
51
+ "default": "./dist/knot.js"
52
+ },
53
+ "./styles/presentation.css": "./src/styles/presentation.css"
48
54
  },
49
55
  "peerDependencies": {
50
56
  "react": ">=18"
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { Logo, TangleKnot, type LogoProps } from "./logo.js";
1
+ export { Logo, TangleKnot, type LogoProps, type TangleKnotProps } from "./logo.js";
2
2
  export { palettes, type BrandPalette } from "./palette.generated.js";
3
+ export { knot, knotGradientIds, knotSvg } from "./knot.js";
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as React from "react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+ import { knot, knotSvg } from "./knot";
5
+ import { TangleKnot } from "./logo";
6
+ describe("canonical knot across renderers",()=>{
7
+ it("uses identical geometry and distinct gradient ids",()=>{
8
+ const react=renderToStaticMarkup(<><TangleKnot/><TangleKnot label=""/></>);
9
+ for(const path of knot.paths)expect(react).toContain(path.d);
10
+ const ids=[...react.matchAll(/linearGradient id="([^"]+)"/g)].map(m=>m[1]);
11
+ expect(new Set(ids).size).toBe(4);
12
+ const svg=knotSvg({id:"hono-1"});for(const path of knot.paths)expect(svg).toContain(path.d);
13
+ expect(svg).toContain('aria-hidden="true"');expect(react).toContain('aria-hidden="true"');
14
+ });
15
+ it("escapes names and rejects unsafe instance ids",()=>{
16
+ expect(knotSvg({id:"a",label:'<img onerror="bad">'})).toContain('&lt;img onerror=&quot;bad&quot;&gt;');
17
+ expect(()=>knotSvg({id:'a" onload="bad'})).toThrow();
18
+ expect(()=>knotSvg({id:"a",size:NaN})).toThrow();
19
+ });
20
+ });
package/src/knot.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** Canonical geometry. This entry has no React or browser dependencies. */
2
+ export const knot = {
3
+ viewBox: "20 45 156 140",
4
+ gradients: [
5
+ { x1: "36.79", y1: "58.78", x2: "145.99", y2: "183.59" },
6
+ { x1: "36.78", y1: "169.38", x2: "145.99", y2: "44.57" },
7
+ ],
8
+ stops: ["#8E59FF", "#6888F9"],
9
+ paths: [
10
+ {
11
+ "d": "M170.74 150.082C170.747 142.848 167.464 135.908 161.612 130.784C155.76 125.66 147.818 122.772 139.528 122.754L104.921 122.754C104.704 119.939 104.567 117.062 104.567 114.078C104.567 111.095 104.429 108.231 104.236 105.403L139.528 105.403C147.746 105.288 155.582 102.359 161.347 97.2467C167.112 92.135 170.343 85.2506 170.343 78.0787C170.343 70.9069 167.112 64.0225 161.347 58.9107C155.582 53.799 147.746 50.8696 139.528 50.7545L56.817 50.7545C48.5993 50.8696 40.7628 53.799 34.9981 58.9107C29.2334 64.0225 26.0025 70.9069 26.0025 78.0787C26.0025 85.2506 29.2334 92.135 34.9981 97.2467C40.7628 102.359 48.5993 105.288 56.817 105.403L89.3238 106.163C91.5854 106.163 91.7785 111.095 91.7785 114.078C91.7785 117.062 91.9156 119.925 92.1091 122.754L56.817 122.754C48.5993 122.869 40.7628 125.798 34.9981 130.91C29.2334 136.022 26.0025 142.906 26.0025 150.078C26.0025 157.25 29.2334 164.134 34.9981 169.246C40.7628 174.358 48.5993 177.287 56.817 177.402L139.528 177.402C147.816 177.384 155.757 174.497 161.609 169.374C167.461 164.252 170.745 157.314 170.74 150.082ZM157.952 78.0752C157.954 82.3443 156.016 86.4397 152.562 89.463C149.108 92.4863 144.42 94.1905 139.528 94.2017L102.881 94.2017C101.554 86.2073 99.0244 78.4005 95.3585 70.9829C93.7421 67.7691 91.6728 64.7438 89.1983 61.9769L139.528 61.9769C144.415 61.988 149.097 63.6884 152.55 66.7056C156.003 69.7228 157.946 73.811 157.952 78.0752ZM56.817 94.2017C51.9157 94.2017 47.2151 92.5027 43.7493 89.4784C40.2835 86.4541 38.3364 82.3522 38.3364 78.0752C38.3364 73.7982 40.2835 69.6964 43.7493 66.6721C47.2151 63.6478 51.9157 61.9487 56.817 61.9487L65.1139 61.9487C72.4433 61.9487 79.3856 67.0146 83.7074 75.5071C86.6658 81.491 88.77 87.7697 89.9724 94.2017L56.817 94.2017ZM38.3929 150.082C38.3907 145.812 40.3293 141.717 43.7834 138.694C47.2374 135.67 51.9248 133.966 56.817 133.955L93.4637 133.955C94.7912 141.949 97.3206 149.756 100.987 157.174C102.603 160.388 104.672 163.413 107.147 166.18L56.817 166.18C51.9304 166.169 47.2479 164.468 43.7948 161.451C40.3416 158.434 38.3993 154.346 38.3929 150.082ZM112.686 152.65C109.728 146.666 107.623 140.387 106.421 133.955L139.528 133.955C144.429 133.955 149.13 135.654 152.596 138.678C156.062 141.703 158.009 145.804 158.009 150.082C158.009 154.359 156.062 158.46 152.596 161.485C149.13 164.509 144.429 166.208 139.528 166.208L131.231 166.208C123.934 166.208 116.992 161.142 112.686 152.65Z",
12
+ "opacity": 1
13
+ },
14
+ {
15
+ "d": "M170.74 78.0752C170.747 85.3089 167.463 92.2492 161.612 97.3728C155.76 102.496 147.817 105.384 139.527 105.403L104.92 105.403C104.702 108.217 104.565 111.095 104.565 114.078C104.565 117.062 104.428 119.925 104.235 122.754L139.527 122.754C147.745 122.869 155.582 125.798 161.347 130.91C167.112 136.022 170.342 142.906 170.342 150.078C170.342 157.25 167.112 164.134 161.347 169.246C155.582 174.358 147.745 177.287 139.527 177.402L56.8149 177.402C48.597 177.287 40.7604 174.358 34.9956 169.246C29.2307 164.134 25.9998 157.25 25.9998 150.078C25.9998 142.906 29.2307 136.022 34.9956 130.91C40.7604 125.798 48.597 122.869 56.8149 122.754L89.3223 121.994C91.5839 121.994 91.777 117.062 91.777 114.078C91.777 111.095 91.9141 108.231 92.1076 105.403L56.8149 105.403C48.597 105.288 40.7604 102.359 34.9956 97.2468C29.2307 92.135 25.9998 85.2506 25.9998 78.0788C25.9998 70.9069 29.2307 64.0225 34.9956 58.9108C40.7604 53.799 48.597 50.8696 56.8149 50.7545L139.527 50.7545C147.816 50.7731 155.757 53.6602 161.609 58.7823C167.46 63.9043 170.744 70.8428 170.74 78.0752ZM157.952 150.082C157.954 145.812 156.015 141.717 152.561 138.694C149.107 135.67 144.42 133.966 139.527 133.955L102.88 133.955C101.552 141.949 99.023 149.756 95.3571 157.174C93.7406 160.388 91.6712 163.413 89.1968 166.18L139.527 166.18C144.414 166.169 149.097 164.468 152.55 161.451C156.003 158.434 157.945 154.346 157.952 150.082ZM56.8149 133.955C51.9134 133.955 47.2127 135.654 43.7469 138.678C40.2811 141.703 38.334 145.805 38.334 150.082C38.334 154.359 40.2811 158.46 43.7469 161.485C47.2127 164.509 51.9134 166.208 56.8149 166.208L65.1119 166.208C72.4414 166.208 79.3838 161.142 83.7057 152.65C86.6642 146.666 88.7684 140.387 89.9708 133.955L56.8149 133.955ZM38.3904 78.0752C38.3883 82.3443 40.3269 86.4398 43.781 89.4631C47.2351 92.4863 51.9226 94.1905 56.8149 94.2017L93.4622 94.2017C94.7898 86.2073 97.3192 78.4005 100.985 70.983C102.602 67.7692 104.671 64.7438 107.145 61.9769L56.8149 61.9769C51.9282 61.9881 47.2456 63.6884 43.7924 66.7056C40.3392 69.7228 38.3968 73.8111 38.3904 78.0752ZM112.685 75.5071C109.726 81.491 107.622 87.7697 106.42 94.2017L139.527 94.2017C144.429 94.2017 149.13 92.5027 152.595 89.4784C156.061 86.4541 158.008 82.3522 158.008 78.0752C158.008 73.7982 156.061 69.6964 152.595 66.6721C149.13 63.6478 144.429 61.9488 139.527 61.9488L131.23 61.9488C123.933 61.9488 116.991 67.0147 112.685 75.5071Z",
16
+ "opacity": 0.8
17
+ }
18
+ ],
19
+ } as const;
20
+
21
+ /** Prefix with an instance/request identifier when more than one mark is inline. */
22
+ export function knotGradientIds(id: string): [string, string] {
23
+ if (!/^[a-zA-Z_][a-zA-Z0-9_:.-]*$/.test(id)) throw new Error("A safe, unique knot id is required");
24
+ return [`${id}-a`, `${id}-b`];
25
+ }
26
+ const escape = (text: string) => text.replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]!);
27
+
28
+ /** Safe static markup for Hono's raw(), static sites and SVG responses. */
29
+ export function knotSvg({ id, size = 32, label }: { id: string; size?: number; label?: string }): string {
30
+ if (!Number.isFinite(size) || size <= 0) throw new Error("Knot size must be positive");
31
+ const ids = knotGradientIds(id);
32
+ const accessibility = label ? `role="img" aria-label="${escape(label)}"` : 'aria-hidden="true"';
33
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${knot.viewBox}" fill="none" focusable="false" ${accessibility}><defs>${knot.gradients.map((g,i)=>`<linearGradient id="${ids[i]}" x1="${g.x1}" y1="${g.y1}" x2="${g.x2}" y2="${g.y2}" gradientUnits="userSpaceOnUse">${knot.stops.map((color,index)=>`<stop offset="${index}" stop-color="${color}"/>`).join("")}</linearGradient>`).join("")}</defs>${knot.paths.map((path,i)=>`<path d="${path.d}" opacity="${path.opacity}" fill="url(#${ids[i]})"/>`).join("")}</svg>`;
34
+ }
package/src/logo.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { knot, knotGradientIds } from "./knot.js";
2
3
 
3
4
  export interface LogoProps {
4
5
  size?: "sm" | "md" | "lg" | "xl";
@@ -14,42 +15,25 @@ const sizeConfig = {
14
15
  xl: { icon: 48, text: "text-3xl", suffix: "text-lg", gap: "gap-3" },
15
16
  } as const;
16
17
 
17
- export function TangleKnot({
18
- size = 32,
19
- className,
20
- }: {
18
+ export interface TangleKnotProps {
21
19
  size?: number;
22
20
  className?: string;
23
- }) {
24
- const uid = React.useId();
25
- const g1 = `tk-a-${uid}`;
26
- const g2 = `tk-b-${uid}`;
21
+ /** Omit for a decorative mark. Supply a name when the mark conveys identity. */
22
+ label?: string;
23
+ }
27
24
 
28
- return (
29
- <svg
30
- width={size}
31
- height={size}
32
- viewBox="20 45 156 140"
33
- fill="none"
34
- xmlns="http://www.w3.org/2000/svg"
35
- className={className}
36
- aria-label="Tangle"
37
- >
38
- <title>Tangle</title>
39
- <defs>
40
- <linearGradient id={g1} x1="36.79" y1="58.78" x2="145.99" y2="183.59" gradientUnits="userSpaceOnUse">
41
- <stop stopColor="#8E59FF" />
42
- <stop offset="1" stopColor="#6888F9" />
43
- </linearGradient>
44
- <linearGradient id={g2} x1="36.78" y1="169.38" x2="145.99" y2="44.57" gradientUnits="userSpaceOnUse">
45
- <stop stopColor="#8E59FF" />
46
- <stop offset="1" stopColor="#6888F9" />
47
- </linearGradient>
48
- </defs>
49
- <path d="M170.74 150.082C170.747 142.848 167.464 135.908 161.612 130.784C155.76 125.66 147.818 122.772 139.528 122.754L104.921 122.754C104.704 119.939 104.567 117.062 104.567 114.078C104.567 111.095 104.429 108.231 104.236 105.403L139.528 105.403C147.746 105.288 155.582 102.359 161.347 97.2467C167.112 92.135 170.343 85.2506 170.343 78.0787C170.343 70.9069 167.112 64.0225 161.347 58.9107C155.582 53.799 147.746 50.8696 139.528 50.7545L56.817 50.7545C48.5993 50.8696 40.7628 53.799 34.9981 58.9107C29.2334 64.0225 26.0025 70.9069 26.0025 78.0787C26.0025 85.2506 29.2334 92.135 34.9981 97.2467C40.7628 102.359 48.5993 105.288 56.817 105.403L89.3238 106.163C91.5854 106.163 91.7785 111.095 91.7785 114.078C91.7785 117.062 91.9156 119.925 92.1091 122.754L56.817 122.754C48.5993 122.869 40.7628 125.798 34.9981 130.91C29.2334 136.022 26.0025 142.906 26.0025 150.078C26.0025 157.25 29.2334 164.134 34.9981 169.246C40.7628 174.358 48.5993 177.287 56.817 177.402L139.528 177.402C147.816 177.384 155.757 174.497 161.609 169.374C167.461 164.252 170.745 157.314 170.74 150.082ZM157.952 78.0752C157.954 82.3443 156.016 86.4397 152.562 89.463C149.108 92.4863 144.42 94.1905 139.528 94.2017L102.881 94.2017C101.554 86.2073 99.0244 78.4005 95.3585 70.9829C93.7421 67.7691 91.6728 64.7438 89.1983 61.9769L139.528 61.9769C144.415 61.988 149.097 63.6884 152.55 66.7056C156.003 69.7228 157.946 73.811 157.952 78.0752ZM56.817 94.2017C51.9157 94.2017 47.2151 92.5027 43.7493 89.4784C40.2835 86.4541 38.3364 82.3522 38.3364 78.0752C38.3364 73.7982 40.2835 69.6964 43.7493 66.6721C47.2151 63.6478 51.9157 61.9487 56.817 61.9487L65.1139 61.9487C72.4433 61.9487 79.3856 67.0146 83.7074 75.5071C86.6658 81.491 88.77 87.7697 89.9724 94.2017L56.817 94.2017ZM38.3929 150.082C38.3907 145.812 40.3293 141.717 43.7834 138.694C47.2374 135.67 51.9248 133.966 56.817 133.955L93.4637 133.955C94.7912 141.949 97.3206 149.756 100.987 157.174C102.603 160.388 104.672 163.413 107.147 166.18L56.817 166.18C51.9304 166.169 47.2479 164.468 43.7948 161.451C40.3416 158.434 38.3993 154.346 38.3929 150.082ZM112.686 152.65C109.728 146.666 107.623 140.387 106.421 133.955L139.528 133.955C144.429 133.955 149.13 135.654 152.596 138.678C156.062 141.703 158.009 145.804 158.009 150.082C158.009 154.359 156.062 158.46 152.596 161.485C149.13 164.509 144.429 166.208 139.528 166.208L131.231 166.208C123.934 166.208 116.992 161.142 112.686 152.65Z" fill={`url(#${g1})`} />
50
- <path opacity="0.8" d="M170.74 78.0752C170.747 85.3089 167.463 92.2492 161.612 97.3728C155.76 102.496 147.817 105.384 139.527 105.403L104.92 105.403C104.702 108.217 104.565 111.095 104.565 114.078C104.565 117.062 104.428 119.925 104.235 122.754L139.527 122.754C147.745 122.869 155.582 125.798 161.347 130.91C167.112 136.022 170.342 142.906 170.342 150.078C170.342 157.25 167.112 164.134 161.347 169.246C155.582 174.358 147.745 177.287 139.527 177.402L56.8149 177.402C48.597 177.287 40.7604 174.358 34.9956 169.246C29.2307 164.134 25.9998 157.25 25.9998 150.078C25.9998 142.906 29.2307 136.022 34.9956 130.91C40.7604 125.798 48.597 122.869 56.8149 122.754L89.3223 121.994C91.5839 121.994 91.777 117.062 91.777 114.078C91.777 111.095 91.9141 108.231 92.1076 105.403L56.8149 105.403C48.597 105.288 40.7604 102.359 34.9956 97.2468C29.2307 92.135 25.9998 85.2506 25.9998 78.0788C25.9998 70.9069 29.2307 64.0225 34.9956 58.9108C40.7604 53.799 48.597 50.8696 56.8149 50.7545L139.527 50.7545C147.816 50.7731 155.757 53.6602 161.609 58.7823C167.46 63.9043 170.744 70.8428 170.74 78.0752ZM157.952 150.082C157.954 145.812 156.015 141.717 152.561 138.694C149.107 135.67 144.42 133.966 139.527 133.955L102.88 133.955C101.552 141.949 99.023 149.756 95.3571 157.174C93.7406 160.388 91.6712 163.413 89.1968 166.18L139.527 166.18C144.414 166.169 149.097 164.468 152.55 161.451C156.003 158.434 157.945 154.346 157.952 150.082ZM56.8149 133.955C51.9134 133.955 47.2127 135.654 43.7469 138.678C40.2811 141.703 38.334 145.805 38.334 150.082C38.334 154.359 40.2811 158.46 43.7469 161.485C47.2127 164.509 51.9134 166.208 56.8149 166.208L65.1119 166.208C72.4414 166.208 79.3838 161.142 83.7057 152.65C86.6642 146.666 88.7684 140.387 89.9708 133.955L56.8149 133.955ZM38.3904 78.0752C38.3883 82.3443 40.3269 86.4398 43.781 89.4631C47.2351 92.4863 51.9226 94.1905 56.8149 94.2017L93.4622 94.2017C94.7898 86.2073 97.3192 78.4005 100.985 70.983C102.602 67.7692 104.671 64.7438 107.145 61.9769L56.8149 61.9769C51.9282 61.9881 47.2456 63.6884 43.7924 66.7056C40.3392 69.7228 38.3968 73.8111 38.3904 78.0752ZM112.685 75.5071C109.726 81.491 107.622 87.7697 106.42 94.2017L139.527 94.2017C144.429 94.2017 149.13 92.5027 152.595 89.4784C156.061 86.4541 158.008 82.3522 158.008 78.0752C158.008 73.7982 156.061 69.6964 152.595 66.6721C149.13 63.6478 144.429 61.9488 139.527 61.9488L131.23 61.9488C123.933 61.9488 116.991 67.0147 112.685 75.5071Z" fill={`url(#${g2})`} />
51
- </svg>
52
- );
25
+ export function TangleKnot({size = 32, className, label = "Tangle"}: TangleKnotProps) {
26
+ const uid = React.useId();
27
+ const ids = knotGradientIds(`tk-${uid}`);
28
+ return <svg width={size} height={size} viewBox={knot.viewBox} fill="none"
29
+ xmlns="http://www.w3.org/2000/svg" className={className} focusable="false"
30
+ role={label ? "img" : undefined} aria-label={label || undefined} aria-hidden={!label || undefined}>
31
+ {label && <title>{label}</title>}
32
+ <defs>{knot.gradients.map((g,i)=><linearGradient key={ids[i]} id={ids[i]} {...g} gradientUnits="userSpaceOnUse">
33
+ {knot.stops.map((color,index)=><stop key={color} offset={index} stopColor={color}/>)}
34
+ </linearGradient>)}</defs>
35
+ {knot.paths.map((path,i)=><path key={i} {...path} fill={`url(#${ids[i]})`}/>)}
36
+ </svg>;
53
37
  }
54
38
 
55
39
  export function Logo({
@@ -9,3 +9,5 @@
9
9
  @import "./theme.css";
10
10
  @import "./named-themes.css";
11
11
  @import "./globals.css";
12
+
13
+ @import "./presentation.css";
@@ -1,6 +1,6 @@
1
1
  /* GENERATED by scripts/gen-compat.mjs. Do not edit.
2
2
  * Source: tokens.css + named-themes.css
3
- * SHA-256: d10503737fc4c0704770bb4a56ec1ecf92d8cc4a875de7553f9b30fcb8317b07
3
+ * SHA-256: f65ac1c530c2769494fc2e77b17ccd1cddabae8d0b0b0bcc5071fa5e695cddd7
4
4
  * Light-default compatibility; all explicit modes remain canonical.
5
5
  */
6
6
  :where(:root),
@@ -319,6 +319,25 @@
319
319
  --tracking-tight: -0.02em;
320
320
  --tracking-snug: -0.01em;
321
321
  --tracking-wide: 0.08em;
322
+ --tracking-display: -0.03em;
323
+ --hero-gutter: 1.5rem;
324
+ --hero-padding-top: clamp(5rem, 3rem + 5vw, 7rem);
325
+ --hero-padding-bottom: clamp(4rem, 2rem + 5vw, 6rem);
326
+ --display-max-width: 56rem;
327
+ --reading-max-width: 42rem;
328
+ --hero-action-gap: 0.75rem;
329
+ --hero-content-gap: 1.5rem;
330
+ --display-control-height: 2.875rem;
331
+ --display-control-radius: 0.625rem;
332
+ --display-control-font-size: 0.9375rem;
333
+ --display-control-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 4px 16px hsl(var(--primary) / 0.2);
334
+ --display-gradient: linear-gradient(135deg, hsl(var(--foreground)) 10%, var(--accent-text) 70%);
335
+ --hero-grid-pitch: 3.5rem;
336
+ --hero-atmosphere-opacity: 0.16;
337
+ --hero-knot-opacity: 0.07;
338
+ --duration-expressive-entrance: 300ms;
339
+ --duration-expressive-ambient: 14s;
340
+ --ease-expressive: cubic-bezier(0.16, 1, 0.3, 1);
322
341
  --avatar-size: 2rem;
323
342
  --timeline-dot-size: 0.625rem;
324
343
  --indicator-dot-size: 0.25rem;
@@ -358,6 +377,10 @@
358
377
  --motion-dismiss: var(--duration-instant) var(--ease-exit);
359
378
  --motion-stream: var(--duration-stream) var(--ease-expo);
360
379
  --motion-arrive: var(--duration-arrive) var(--ease-expo);
380
+ --interaction-feedback-budget: 100ms;
381
+ --interaction-pending-delay: 200ms;
382
+ --interaction-long-work-delay: 1000ms;
383
+ --interaction-mutation-budget: 500ms;
361
384
  --stagger-step: 50ms;
362
385
  --stagger-index: 0;
363
386
  --arrive-distance: 8px;
@@ -0,0 +1,120 @@
1
+ /* Framework-neutral opt-in presentation. Load fonts from the application's origin. */
2
+ [data-typography="expressive"],
3
+ [data-typography="expressive"] :is([data-theme], [data-sandbox-ui], [data-sandbox-theme], .dark, .light) {
4
+ --font-display: "Outfit", ui-sans-serif, system-ui, sans-serif;
5
+ --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
6
+ --font-mono: "Geist Mono", ui-monospace, monospace;
7
+ font-family: var(--font-sans);
8
+ }
9
+ .tangle-intro {
10
+ position: relative;
11
+ isolation: isolate;
12
+ overflow: clip;
13
+ padding: var(--hero-padding-top) var(--hero-gutter) var(--hero-padding-bottom);
14
+ text-align: center;
15
+ color: hsl(var(--foreground));
16
+ background: hsl(var(--background));
17
+ }
18
+ .tangle-intro::before {
19
+ content: "";
20
+ position: absolute;
21
+ inset: 0;
22
+ z-index: -1;
23
+ pointer-events: none;
24
+ opacity: var(--hero-atmosphere-opacity);
25
+ background: radial-gradient(ellipse at 50% 0%, hsl(var(--primary)), transparent 70%);
26
+ }
27
+ .tangle-intro[data-grid]::after {
28
+ content: "";
29
+ position: absolute;
30
+ inset: 0;
31
+ z-index: -1;
32
+ pointer-events: none;
33
+ background-image: linear-gradient(var(--border-subtle) 1px, transparent 1px), linear-gradient(90deg, var(--border-subtle) 1px, transparent 1px);
34
+ background-size: var(--hero-grid-pitch) var(--hero-grid-pitch);
35
+ mask-image: radial-gradient(ellipse at top, black, transparent 75%);
36
+ }
37
+ .tangle-display {
38
+ margin: 0 auto;
39
+ max-width: var(--display-max-width);
40
+ overflow-wrap: anywhere;
41
+ font-family: var(--font-display);
42
+ font-size: var(--font-size-display);
43
+ line-height: var(--line-height-display);
44
+ letter-spacing: var(--tracking-display);
45
+ font-weight: 700;
46
+ color: hsl(var(--foreground));
47
+ }
48
+ .tangle-display[data-gradient] {
49
+ background-image: var(--display-gradient);
50
+ background-clip: text;
51
+ -webkit-background-clip: text;
52
+ color: transparent;
53
+ }
54
+ .tangle-intro-description {
55
+ margin: var(--hero-content-gap) auto 0;
56
+ max-width: var(--reading-max-width);
57
+ color: hsl(var(--muted-foreground));
58
+ font-size: clamp(1rem, 0.8rem + 1vw, 1.25rem);
59
+ line-height: 1.625;
60
+ }
61
+ .tangle-intro-actions {
62
+ display: flex;
63
+ flex-wrap: wrap;
64
+ justify-content: center;
65
+ gap: var(--hero-action-gap);
66
+ margin-top: 2.5rem;
67
+ }
68
+ .tangle-control-display {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ gap: var(--hero-action-gap);
73
+ min-height: var(--display-control-height);
74
+ padding: 0 1.5rem;
75
+ border: 1px solid transparent;
76
+ border-radius: var(--display-control-radius);
77
+ background: hsl(var(--primary));
78
+ color: hsl(var(--primary-foreground));
79
+ font: 600 var(--display-control-font-size)/1 var(--font-sans);
80
+ text-decoration: none;
81
+ white-space: nowrap;
82
+ box-shadow: var(--display-control-shadow);
83
+ cursor: pointer;
84
+ transition: transform var(--duration-fast) ease-out;
85
+ }
86
+ .tangle-control-display[data-variant="outline"] {
87
+ background: hsl(var(--card));
88
+ color: hsl(var(--card-foreground));
89
+ border-color: hsl(var(--border));
90
+ box-shadow: none;
91
+ }
92
+ .tangle-control-display:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 3px; }
93
+ .tangle-control-display:disabled, .tangle-control-display[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
94
+ @media (hover: hover) and (pointer: fine) {
95
+ .tangle-control-display:not(:disabled):not([aria-disabled="true"]):hover { transform: translateY(-1px); }
96
+ }
97
+ .tangle-control-display:not(:disabled):not([aria-disabled="true"]):active { transform: translateY(1px); }
98
+ .tangle-intro-knot {
99
+ position: absolute;
100
+ z-index: -1;
101
+ pointer-events: none;
102
+ width: clamp(30rem, 45vw, 40rem);
103
+ height: auto;
104
+ right: -10rem;
105
+ top: 1.5rem;
106
+ opacity: var(--hero-knot-opacity);
107
+ rotate: -14deg;
108
+ }
109
+ .tangle-intro-knot > svg { width: 100%; height: auto; }
110
+ .tangle-intro[data-motion] { animation: tangle-intro-enter var(--duration-expressive-entrance) var(--ease-expressive) both; }
111
+ .tangle-intro[data-motion] .tangle-intro-knot { animation: tangle-intro-drift var(--duration-expressive-ambient) ease-in-out infinite; }
112
+ @keyframes tangle-intro-enter { from { opacity: 0; transform: translateY(var(--arrive-distance)); } to { opacity: 1; transform: none; } }
113
+ @keyframes tangle-intro-drift { 50% { transform: translateY(-0.75rem) scale(1.04); } }
114
+ @media (prefers-reduced-motion: reduce) {
115
+ .tangle-intro, .tangle-intro *, .tangle-control-display { animation: none !important; transition: none !important; transform: none !important; }
116
+ }
117
+ @media (forced-colors: active) {
118
+ .tangle-display[data-gradient] { background: none; color: CanvasText; }
119
+ .tangle-intro::before, .tangle-intro::after { display: none; }
120
+ }
@@ -494,6 +494,27 @@
494
494
  --tracking-snug: -0.01em; /* section titles */
495
495
  --tracking-wide: 0.08em; /* uppercase eyebrows / stat labels */
496
496
 
497
+ /* Expressive roles are opt-in; operational type and controls keep their scale. */
498
+ --tracking-display: -0.03em;
499
+ --hero-gutter: 1.5rem;
500
+ --hero-padding-top: clamp(5rem, 3rem + 5vw, 7rem);
501
+ --hero-padding-bottom: clamp(4rem, 2rem + 5vw, 6rem);
502
+ --display-max-width: 56rem;
503
+ --reading-max-width: 42rem;
504
+ --hero-action-gap: 0.75rem;
505
+ --hero-content-gap: 1.5rem;
506
+ --display-control-height: 2.875rem;
507
+ --display-control-radius: 0.625rem;
508
+ --display-control-font-size: 0.9375rem;
509
+ --display-control-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 4px 16px hsl(var(--primary) / 0.2);
510
+ --display-gradient: linear-gradient(135deg, hsl(var(--foreground)) 10%, var(--accent-text) 70%);
511
+ --hero-grid-pitch: 3.5rem;
512
+ --hero-atmosphere-opacity: 0.16;
513
+ --hero-knot-opacity: 0.07;
514
+ --duration-expressive-entrance: 300ms;
515
+ --duration-expressive-ambient: 14s;
516
+ --ease-expressive: cubic-bezier(0.16, 1, 0.3, 1);
517
+
497
518
  /* Component spacing — override per-app for tighter/looser layouts */
498
519
  --avatar-size: 2rem; /* 32px — chat avatars */
499
520
  --timeline-dot-size: 0.625rem; /* 10px — timeline connector dots */
@@ -539,6 +560,13 @@
539
560
  --motion-dismiss: var(--duration-instant) var(--ease-exit);
540
561
  --motion-stream: var(--duration-stream) var(--ease-expo);
541
562
  --motion-arrive: var(--duration-arrive) var(--ease-expo);
563
+
564
+ /* Elapsed interaction thresholds, not animation durations. See
565
+ * docs/design-bible.md#interaction. Reduced motion leaves these unchanged. */
566
+ --interaction-feedback-budget: 100ms;
567
+ --interaction-pending-delay: 200ms;
568
+ --interaction-long-work-delay: 1000ms;
569
+ --interaction-mutation-budget: 500ms;
542
570
  --stagger-step: 50ms;
543
571
  --stagger-index: 0;
544
572
  --arrive-distance: 8px;