@spunto/design-system 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/package.json +7 -2
- package/src/brand/index.ts +31 -0
- package/src/brand/logo.test.tsx +52 -0
- package/src/brand/logo.ts +238 -0
- package/src/brand/spunto-logo.tsx +90 -0
package/README.md
CHANGED
|
@@ -470,6 +470,34 @@ it, the navs and footers (they hard-code URLs and a session), the product logos
|
|
|
470
470
|
and OG cards. The rule: *if it needs to know a product, a URL or a session, it's
|
|
471
471
|
the site; if it only lays out or types, it's the system.*
|
|
472
472
|
|
|
473
|
+
## Brand — `/brand`
|
|
474
|
+
|
|
475
|
+
The Spunto logo, behind its own entry: the one sub-export that knows Spunto by
|
|
476
|
+
construction, kept out of the root so a third-party consumer of the primitives
|
|
477
|
+
never ships the company's mark.
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import { SpuntoLogo } from "@spunto/design-system/brand"
|
|
481
|
+
|
|
482
|
+
<SpuntoLogo /> // the brand: flame tile, night ink
|
|
483
|
+
<SpuntoLogo variant="mark" tone="night" /> // S + dot only, what survives 16 px
|
|
484
|
+
<SpuntoLogo tone="ink" className="text-primary" /> // no tile, currentColor
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The drawing is a 7 × 7 grid — blocks of 10 on a pitch of 14 — and it tells the
|
|
488
|
+
product: the dot is your container, the two brackets are an S with the dot for
|
|
489
|
+
its middle stroke, three blocks on the left are Build, three lights on the right
|
|
490
|
+
are Run, the belt along the bottom is Ship, three squares on top are the
|
|
491
|
+
machines you already own. `tone` picks a declension (`flame`, `build`,
|
|
492
|
+
`ship`, `run`, `night`, `cream`, `pillars`, `ink`); `paint` overrides
|
|
493
|
+
any role on top of it.
|
|
494
|
+
|
|
495
|
+
The geometry lives as data in `src/brand/logo.ts`, React-free, so the same
|
|
496
|
+
shapes come out as an element (`SpuntoLogo`) and as a file
|
|
497
|
+
(`spuntoLogoSvg()`). `npm run brand:export -- <dir>` writes one `.svg` per
|
|
498
|
+
declension — favicon, per-product tiles — which is how a site gets its static
|
|
499
|
+
copies without ever hand-editing an SVG.
|
|
500
|
+
|
|
473
501
|
## Toasts — `SpuntoProvider` + `toast()`
|
|
474
502
|
|
|
475
503
|
`SpuntoProvider` is the design system's client umbrella provider. Mount it once
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"types": "./src/components/marketing/index.ts",
|
|
50
50
|
"import": "./src/components/marketing/index.ts"
|
|
51
51
|
},
|
|
52
|
+
"./brand": {
|
|
53
|
+
"types": "./src/brand/index.ts",
|
|
54
|
+
"import": "./src/brand/index.ts"
|
|
55
|
+
},
|
|
52
56
|
"./fonts": {
|
|
53
57
|
"types": "./src/components/fonts.tsx",
|
|
54
58
|
"import": "./src/components/fonts.tsx"
|
|
@@ -62,7 +66,8 @@
|
|
|
62
66
|
],
|
|
63
67
|
"scripts": {
|
|
64
68
|
"typecheck": "tsc --noEmit",
|
|
65
|
-
"test": "vitest run"
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"brand:export": "node scripts/export-logo.ts"
|
|
66
71
|
},
|
|
67
72
|
"dependencies": {
|
|
68
73
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @spunto/design-system/brand — the Spunto logo.
|
|
2
|
+
//
|
|
3
|
+
// Behind its own entry, like every domain sub-export: a `Button` knows nothing
|
|
4
|
+
// about Spunto, and a third-party consumer of the primitives should not carry
|
|
5
|
+
// the company's mark in their bundle. Here it *is* the point: the same drawing
|
|
6
|
+
// for the console, for Spunto Lite, for the showcase, each in its own tone.
|
|
7
|
+
//
|
|
8
|
+
// `SpuntoLogo` is the React element. `spuntoLogoSvg` is the same drawing as a
|
|
9
|
+
// static document — `scripts/export-logo.ts` uses it to write the favicon and
|
|
10
|
+
// the per-product tiles a site serves as files.
|
|
11
|
+
|
|
12
|
+
export { SpuntoLogo } from "./spunto-logo"
|
|
13
|
+
export type { SpuntoLogoProps } from "./spunto-logo"
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
SPUNTO_LOGO_TONES,
|
|
17
|
+
SPUNTO_LOGO_VIEWBOX,
|
|
18
|
+
SPUNTO_LOGO_TILE_RADIUS,
|
|
19
|
+
spuntoLogoSvg,
|
|
20
|
+
spuntoLogoShapes,
|
|
21
|
+
spuntoLogoTile,
|
|
22
|
+
spuntoLogoFill,
|
|
23
|
+
} from "./logo"
|
|
24
|
+
export type {
|
|
25
|
+
SpuntoLogoPaint,
|
|
26
|
+
SpuntoLogoTone,
|
|
27
|
+
SpuntoLogoVariant,
|
|
28
|
+
SpuntoLogoRole,
|
|
29
|
+
SpuntoLogoShape,
|
|
30
|
+
SpuntoLogoSvgOptions,
|
|
31
|
+
} from "./logo"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
3
|
+
|
|
4
|
+
import { SPUNTO_LOGO_TONES, spuntoLogoShapes, spuntoLogoSvg } from "./logo"
|
|
5
|
+
import { SpuntoLogo } from "./spunto-logo"
|
|
6
|
+
|
|
7
|
+
// The whole point of `logo.ts` being data is that the element and the file are
|
|
8
|
+
// the same drawing. These pin that: same shapes, same fills, in the same order.
|
|
9
|
+
// (React closes SVG elements with a tag, the file self-closes them — hence `/?>`.)
|
|
10
|
+
const attrs = (svg: string) =>
|
|
11
|
+
[...svg.matchAll(/<(rect|circle|path)\b([^>]*?)\/?>/g)].map(([, tag, a]) => {
|
|
12
|
+
const kv = Object.fromEntries([...a.matchAll(/(\w+)="([^"]*)"/g)].map(([, k, v]) => [k, v]))
|
|
13
|
+
return { tag, ...kv }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("SpuntoLogo", () => {
|
|
17
|
+
it("draws exactly what spuntoLogoSvg writes, for every tone", () => {
|
|
18
|
+
for (const tone of Object.keys(SPUNTO_LOGO_TONES) as Array<keyof typeof SPUNTO_LOGO_TONES>) {
|
|
19
|
+
const paint = SPUNTO_LOGO_TONES[tone]
|
|
20
|
+
expect(attrs(renderToStaticMarkup(<SpuntoLogo tone={tone} />))).toEqual(attrs(spuntoLogoSvg(paint)))
|
|
21
|
+
expect(attrs(renderToStaticMarkup(<SpuntoLogo tone={tone} variant="mark" />))).toEqual(
|
|
22
|
+
attrs(spuntoLogoSvg(paint, { variant: "mark" })),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("has no tile and takes currentColor in the ink tone", () => {
|
|
28
|
+
const svg = renderToStaticMarkup(<SpuntoLogo tone="ink" />)
|
|
29
|
+
expect(svg).not.toContain('width="128"')
|
|
30
|
+
expect(svg.match(/fill="currentColor"/g)).toHaveLength(spuntoLogoShapes("glyph").length)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("lets paint override a single role of a tone", () => {
|
|
34
|
+
const svg = renderToStaticMarkup(<SpuntoLogo tone="flame" paint={{ tile: "#be241f" }} />)
|
|
35
|
+
expect(svg).toContain('fill="#be241f"')
|
|
36
|
+
expect(svg).not.toContain('fill="#ec5100"')
|
|
37
|
+
expect(svg).toContain('fill="#0e0805"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("is hidden from assistive tech unless it is given a name", () => {
|
|
41
|
+
expect(renderToStaticMarkup(<SpuntoLogo />)).toContain('aria-hidden="true"')
|
|
42
|
+
const named = renderToStaticMarkup(<SpuntoLogo title="Spunto — home" />)
|
|
43
|
+
expect(named).toContain('role="img"')
|
|
44
|
+
expect(named).toContain("<title>Spunto — home</title>")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("keeps the mark to the S and the dot, and the glyph to the full story", () => {
|
|
48
|
+
// 2 brackets + 1 dot; then 3 fleet + 3 build + 3 run + 4 ship.
|
|
49
|
+
expect(spuntoLogoShapes("mark")).toHaveLength(3)
|
|
50
|
+
expect(spuntoLogoShapes("glyph")).toHaveLength(3 + 3 + 3 + 3 + 4)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// The Spunto logo, as data.
|
|
2
|
+
//
|
|
3
|
+
// This module is deliberately **pure**: no React, no DOM, no colour maths. It
|
|
4
|
+
// describes the mark once — a list of shapes on a grid, and the named paints
|
|
5
|
+
// that go on them — so that the same drawing can be emitted as a React element
|
|
6
|
+
// (`SpuntoLogo`) and as a static `.svg` file (`spuntoLogoSvg`, used by
|
|
7
|
+
// `scripts/export-logo.ts` to write the favicon and the per-product tiles). One
|
|
8
|
+
// source, two outputs, nothing to keep in sync by eye.
|
|
9
|
+
//
|
|
10
|
+
// ─── The drawing ───────────────────────────────────────────────────────────
|
|
11
|
+
//
|
|
12
|
+
// A 7 × 7 grid: blocks of 10 on a pitch of 14, inset 17 from the edge of a
|
|
13
|
+
// 128 × 128 tile. Everything snaps to it — the bars are blocks fused together,
|
|
14
|
+
// the lights are blocks turned round. That grid is what the logo inherited
|
|
15
|
+
// from its predecessor, and what keeps it in the family of the design system's
|
|
16
|
+
// quasi-square 4 px radius: nothing here is drawn freehand.
|
|
17
|
+
//
|
|
18
|
+
// What it says, read from the centre outwards:
|
|
19
|
+
//
|
|
20
|
+
// • the dot — your container. The one thing the platform is about.
|
|
21
|
+
// • the two brackets — an S (for Spunto) whose middle stroke *is* the dot:
|
|
22
|
+
// two L-shapes 180° apart, wrapping the container the
|
|
23
|
+
// way the platform wraps a workspace.
|
|
24
|
+
// • three blocks, left — Build. Layers stacked; an image being made.
|
|
25
|
+
// • three lights, right — Run. Round because they're alive: status lights.
|
|
26
|
+
// • the belt, bottom — Ship. Square, round, square, round: what leaves
|
|
27
|
+
// the build side and comes out running.
|
|
28
|
+
// • three squares, top — the fleet. The machines you already own, the
|
|
29
|
+
// roof everything else sits under.
|
|
30
|
+
//
|
|
31
|
+
// Each of those is a `role`, so a paint can colour the story (the `pillars`
|
|
32
|
+
// tone lights Build / Ship / Run in their own tokens) or flatten it to one ink.
|
|
33
|
+
|
|
34
|
+
export const SPUNTO_LOGO_VIEWBOX = 128
|
|
35
|
+
|
|
36
|
+
/** Corner radius of the tile, in viewBox units — quasi-square, like the system. */
|
|
37
|
+
export const SPUNTO_LOGO_TILE_RADIUS = 14
|
|
38
|
+
|
|
39
|
+
/** Every part of the drawing that can be painted independently. */
|
|
40
|
+
export type SpuntoLogoRole = "tile" | "core" | "build" | "run" | "ship" | "fleet"
|
|
41
|
+
|
|
42
|
+
export type SpuntoLogoShape =
|
|
43
|
+
| { role: SpuntoLogoRole; kind: "rect"; x: number; y: number; w: number; h: number; rx?: number }
|
|
44
|
+
| { role: SpuntoLogoRole; kind: "circle"; cx: number; cy: number; r: number }
|
|
45
|
+
| { role: SpuntoLogoRole; kind: "path"; d: string }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `glyph` is the whole story — the S, the container and its three pillars.
|
|
49
|
+
* `mark` is the S and the container alone, drawn larger: what survives at
|
|
50
|
+
* 16 px, so it is the favicon and the collapsed-sidebar icon.
|
|
51
|
+
*/
|
|
52
|
+
export type SpuntoLogoVariant = "glyph" | "mark"
|
|
53
|
+
|
|
54
|
+
const PITCH = 14
|
|
55
|
+
const BLOCK = 10
|
|
56
|
+
const INSET = 17
|
|
57
|
+
const CENTRE = SPUNTO_LOGO_VIEWBOX / 2
|
|
58
|
+
|
|
59
|
+
/** Grid column/row `i` (0–6) → its top-left coordinate. */
|
|
60
|
+
const at = (i: number) => INSET + PITCH * i
|
|
61
|
+
|
|
62
|
+
const square = (role: SpuntoLogoRole, col: number, row: number): SpuntoLogoShape => ({
|
|
63
|
+
role,
|
|
64
|
+
kind: "rect",
|
|
65
|
+
x: at(col),
|
|
66
|
+
y: at(row),
|
|
67
|
+
w: BLOCK,
|
|
68
|
+
h: BLOCK,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const light = (role: SpuntoLogoRole, col: number, row: number): SpuntoLogoShape => ({
|
|
72
|
+
role,
|
|
73
|
+
kind: "circle",
|
|
74
|
+
cx: at(col) + BLOCK / 2,
|
|
75
|
+
cy: at(row) + BLOCK / 2,
|
|
76
|
+
r: BLOCK / 2,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/* The two brackets and the dot, in both sizes. Coordinates are spelled out
|
|
80
|
+
rather than computed so the paths read as what they are: an L, its 180°
|
|
81
|
+
twin, and the space between them. In the glyph the brackets are one block
|
|
82
|
+
thick, like every other bar on the grid; in the mark they are 14 — the
|
|
83
|
+
pitch — because at favicon size a 10-unit bar is a single blurred pixel. */
|
|
84
|
+
const CORE = {
|
|
85
|
+
glyph: [
|
|
86
|
+
{ role: "core", kind: "path", d: "M31 31h52v10H41v42H31z" }, // Γ — top bar, left stem
|
|
87
|
+
{ role: "core", kind: "path", d: "M97 97H45V87h42V45h10z" }, // ⅃ — bottom bar, right stem
|
|
88
|
+
{ role: "core", kind: "circle", cx: CENTRE, cy: CENTRE, r: 13 },
|
|
89
|
+
],
|
|
90
|
+
mark: [
|
|
91
|
+
{ role: "core", kind: "path", d: "M18 18h72v14H32v58H18z" },
|
|
92
|
+
{ role: "core", kind: "path", d: "M110 110H38V96h58V38h14z" },
|
|
93
|
+
{ role: "core", kind: "circle", cx: CENTRE, cy: CENTRE, r: 18 },
|
|
94
|
+
],
|
|
95
|
+
} satisfies Record<SpuntoLogoVariant, SpuntoLogoShape[]>
|
|
96
|
+
|
|
97
|
+
const PILLARS: SpuntoLogoShape[] = [
|
|
98
|
+
// the fleet, on top
|
|
99
|
+
square("fleet", 2, 0),
|
|
100
|
+
square("fleet", 3, 0),
|
|
101
|
+
square("fleet", 4, 0),
|
|
102
|
+
// Build — blocks stacked on the left
|
|
103
|
+
square("build", 0, 2),
|
|
104
|
+
square("build", 0, 3),
|
|
105
|
+
square("build", 0, 4),
|
|
106
|
+
// Run — lights on the right
|
|
107
|
+
light("run", 6, 2),
|
|
108
|
+
light("run", 6, 3),
|
|
109
|
+
light("run", 6, 4),
|
|
110
|
+
// Ship — the belt along the bottom, build-side square to run-side round
|
|
111
|
+
square("ship", 1, 6),
|
|
112
|
+
light("ship", 2, 6),
|
|
113
|
+
square("ship", 3, 6),
|
|
114
|
+
light("ship", 4, 6),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The shapes of a variant, tile excluded (the tile is a paint decision, see
|
|
119
|
+
* `spuntoLogoTile`). Order is paint order.
|
|
120
|
+
*/
|
|
121
|
+
export function spuntoLogoShapes(variant: SpuntoLogoVariant = "glyph"): SpuntoLogoShape[] {
|
|
122
|
+
return variant === "mark" ? [...CORE.mark] : [...CORE.glyph, ...PILLARS]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The tile behind the drawing, when a paint asks for one. */
|
|
126
|
+
export function spuntoLogoTile(): SpuntoLogoShape {
|
|
127
|
+
return {
|
|
128
|
+
role: "tile",
|
|
129
|
+
kind: "rect",
|
|
130
|
+
x: 0,
|
|
131
|
+
y: 0,
|
|
132
|
+
w: SPUNTO_LOGO_VIEWBOX,
|
|
133
|
+
h: SPUNTO_LOGO_VIEWBOX,
|
|
134
|
+
rx: SPUNTO_LOGO_TILE_RADIUS,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Paint ─────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What colour goes on what. `ink` is the fallback for every drawn role; a
|
|
142
|
+
* role named explicitly overrides it. No `tile` means a transparent
|
|
143
|
+
* background — the drawing alone, for a nav bar or a print.
|
|
144
|
+
*/
|
|
145
|
+
export interface SpuntoLogoPaint {
|
|
146
|
+
tile?: string
|
|
147
|
+
ink: string
|
|
148
|
+
core?: string
|
|
149
|
+
build?: string
|
|
150
|
+
run?: string
|
|
151
|
+
ship?: string
|
|
152
|
+
fleet?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The declensions. Each is a design-system token turned into a fixed value:
|
|
157
|
+
* a logo is an artefact, not a surface, so it does not follow the theme — the
|
|
158
|
+
* flame tile is the same flame at night. The values are the light-theme
|
|
159
|
+
* register of `styles.css` (`--flame`, `--build`, `--ship`, `--run`,
|
|
160
|
+
* `--night`, `--background`) and the sand the marketing pages use for ink on
|
|
161
|
+
* night, converted out of oklch.
|
|
162
|
+
*
|
|
163
|
+
* The idea is one tone per product: Spunto Cloud is the brand and wears
|
|
164
|
+
* `flame`; a product whose marketing accent is `build` wears `build`; the
|
|
165
|
+
* design system, whose accent is ink, wears `night`. `pillars` is the same
|
|
166
|
+
* drawing with its story lit — Build, Ship and Run each in their own token —
|
|
167
|
+
* for a hero or a social card, not for a 24 px header.
|
|
168
|
+
*/
|
|
169
|
+
export const SPUNTO_LOGO_TONES = {
|
|
170
|
+
/** The brand: flame tile, night ink. */
|
|
171
|
+
flame: { tile: "#ec5100", ink: "#0e0805" },
|
|
172
|
+
/** `--build` tile — amber. */
|
|
173
|
+
build: { tile: "#f9a733", ink: "#0e0805" },
|
|
174
|
+
/** `--ship` tile — orange. */
|
|
175
|
+
ship: { tile: "#fb7c01", ink: "#0e0805" },
|
|
176
|
+
/** `--run` tile — one step short of flame. */
|
|
177
|
+
run: { tile: "#f35800", ink: "#0e0805" },
|
|
178
|
+
/** `--night` tile, sand ink — what the design system itself wears. */
|
|
179
|
+
night: { tile: "#0e0805", ink: "#e0d3bb" },
|
|
180
|
+
/** Cream tile, night ink, the container lit in flame — for a light print. */
|
|
181
|
+
cream: { tile: "#fbf6ee", ink: "#0e0805", core: "#ec5100" },
|
|
182
|
+
/** Night, with the three pillars in their own tokens (night register). */
|
|
183
|
+
pillars: {
|
|
184
|
+
tile: "#0e0805",
|
|
185
|
+
ink: "#fbf6ee",
|
|
186
|
+
fleet: "#8b837d",
|
|
187
|
+
build: "#ffb649",
|
|
188
|
+
ship: "#ff8b27",
|
|
189
|
+
run: "#ff6917",
|
|
190
|
+
},
|
|
191
|
+
} as const satisfies Record<string, SpuntoLogoPaint>
|
|
192
|
+
|
|
193
|
+
export type SpuntoLogoTone = keyof typeof SPUNTO_LOGO_TONES
|
|
194
|
+
|
|
195
|
+
/** The colour a role takes under a paint. `tile` returns `undefined` when there is none. */
|
|
196
|
+
export function spuntoLogoFill(paint: SpuntoLogoPaint, role: SpuntoLogoRole): string | undefined {
|
|
197
|
+
if (role === "tile") return paint.tile
|
|
198
|
+
return paint[role] ?? paint.ink
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Static SVG ────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export interface SpuntoLogoSvgOptions {
|
|
204
|
+
variant?: SpuntoLogoVariant
|
|
205
|
+
/** `<title>` for the file; omitted, the SVG carries `aria-hidden`. */
|
|
206
|
+
title?: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function shapeToSvg(shape: SpuntoLogoShape, fill: string): string {
|
|
210
|
+
switch (shape.kind) {
|
|
211
|
+
case "rect": {
|
|
212
|
+
const rx = shape.rx ? ` rx="${shape.rx}"` : ""
|
|
213
|
+
return `<rect x="${shape.x}" y="${shape.y}" width="${shape.w}" height="${shape.h}"${rx} fill="${fill}"/>`
|
|
214
|
+
}
|
|
215
|
+
case "circle":
|
|
216
|
+
return `<circle cx="${shape.cx}" cy="${shape.cy}" r="${shape.r}" fill="${fill}"/>`
|
|
217
|
+
case "path":
|
|
218
|
+
return `<path d="${shape.d}" fill="${fill}"/>`
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The logo as a standalone SVG document — what goes on disk as a favicon, an
|
|
224
|
+
* `<img src>`, an `og:image` source. Same shapes, same paints as the React
|
|
225
|
+
* component; only the serialisation differs.
|
|
226
|
+
*/
|
|
227
|
+
export function spuntoLogoSvg(paint: SpuntoLogoPaint, options: SpuntoLogoSvgOptions = {}): string {
|
|
228
|
+
const { variant = "glyph", title } = options
|
|
229
|
+
const shapes = paint.tile ? [spuntoLogoTile(), ...spuntoLogoShapes(variant)] : spuntoLogoShapes(variant)
|
|
230
|
+
const body = shapes
|
|
231
|
+
.map((s) => shapeToSvg(s, spuntoLogoFill(paint, s.role) ?? paint.ink))
|
|
232
|
+
.map((line) => ` ${line}`)
|
|
233
|
+
.join("\n")
|
|
234
|
+
const head = title
|
|
235
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SPUNTO_LOGO_VIEWBOX} ${SPUNTO_LOGO_VIEWBOX}" role="img" aria-labelledby="t">\n <title id="t">${title}</title>`
|
|
236
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SPUNTO_LOGO_VIEWBOX} ${SPUNTO_LOGO_VIEWBOX}" aria-hidden="true">`
|
|
237
|
+
return `${head}\n${body}\n</svg>\n`
|
|
238
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { SVGProps } from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils"
|
|
4
|
+
import {
|
|
5
|
+
SPUNTO_LOGO_TONES,
|
|
6
|
+
SPUNTO_LOGO_VIEWBOX,
|
|
7
|
+
spuntoLogoFill,
|
|
8
|
+
spuntoLogoShapes,
|
|
9
|
+
spuntoLogoTile,
|
|
10
|
+
type SpuntoLogoPaint,
|
|
11
|
+
type SpuntoLogoShape,
|
|
12
|
+
type SpuntoLogoTone,
|
|
13
|
+
type SpuntoLogoVariant,
|
|
14
|
+
} from "./logo"
|
|
15
|
+
|
|
16
|
+
/* ─── SpuntoLogo ────────────────────────────────────────────────────────────
|
|
17
|
+
The logo as a React element. Two axes:
|
|
18
|
+
|
|
19
|
+
`variant` — `glyph` (the whole story, from 24 px up) or `mark` (the S and
|
|
20
|
+
the container alone, for 16 px and collapsed sidebars).
|
|
21
|
+
|
|
22
|
+
`tone` — which declension. The seven named ones are fixed paints built
|
|
23
|
+
from the tokens (see `SPUNTO_LOGO_TONES`); `ink` is the odd one out: no
|
|
24
|
+
tile, and the drawing takes `currentColor`, so it follows the text colour
|
|
25
|
+
it sits in — the right choice for a header on a themed surface, where a
|
|
26
|
+
fixed flame tile would fight the page.
|
|
27
|
+
|
|
28
|
+
`paint` overrides any tone field by field. A product whose accent is not a
|
|
29
|
+
design-system token (the site's marketing `rust`, say) passes its own tile
|
|
30
|
+
here rather than getting a tone the package would then have to own. */
|
|
31
|
+
|
|
32
|
+
export interface SpuntoLogoProps extends Omit<SVGProps<SVGSVGElement>, "children" | "viewBox"> {
|
|
33
|
+
variant?: SpuntoLogoVariant
|
|
34
|
+
/** @default "flame" */
|
|
35
|
+
tone?: SpuntoLogoTone | "ink"
|
|
36
|
+
/** Per-role overrides on top of the tone. */
|
|
37
|
+
paint?: Partial<SpuntoLogoPaint>
|
|
38
|
+
/** Width and height, in px. Leave it out to size with `className`. */
|
|
39
|
+
size?: number
|
|
40
|
+
/**
|
|
41
|
+
* Accessible name. Give one when the logo is the only content of a link
|
|
42
|
+
* ("Spunto — home"); leave it out when a wordmark sits next to it, and the
|
|
43
|
+
* SVG is hidden from assistive tech.
|
|
44
|
+
*/
|
|
45
|
+
title?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INK: SpuntoLogoPaint = { ink: "currentColor" }
|
|
49
|
+
|
|
50
|
+
function Shape({ shape, fill }: { shape: SpuntoLogoShape; fill: string }) {
|
|
51
|
+
switch (shape.kind) {
|
|
52
|
+
case "rect":
|
|
53
|
+
return <rect x={shape.x} y={shape.y} width={shape.w} height={shape.h} rx={shape.rx} fill={fill} />
|
|
54
|
+
case "circle":
|
|
55
|
+
return <circle cx={shape.cx} cy={shape.cy} r={shape.r} fill={fill} />
|
|
56
|
+
case "path":
|
|
57
|
+
return <path d={shape.d} fill={fill} />
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function SpuntoLogo({
|
|
62
|
+
variant = "glyph",
|
|
63
|
+
tone = "flame",
|
|
64
|
+
paint: overrides,
|
|
65
|
+
size,
|
|
66
|
+
title,
|
|
67
|
+
className,
|
|
68
|
+
...rest
|
|
69
|
+
}: SpuntoLogoProps) {
|
|
70
|
+
const base: SpuntoLogoPaint = tone === "ink" ? INK : SPUNTO_LOGO_TONES[tone]
|
|
71
|
+
const paint: SpuntoLogoPaint = { ...base, ...overrides }
|
|
72
|
+
const shapes = paint.tile ? [spuntoLogoTile(), ...spuntoLogoShapes(variant)] : spuntoLogoShapes(variant)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<svg
|
|
76
|
+
viewBox={`0 0 ${SPUNTO_LOGO_VIEWBOX} ${SPUNTO_LOGO_VIEWBOX}`}
|
|
77
|
+
width={size}
|
|
78
|
+
height={size}
|
|
79
|
+
className={cn("shrink-0", className)}
|
|
80
|
+
role={title ? "img" : undefined}
|
|
81
|
+
aria-hidden={title ? undefined : true}
|
|
82
|
+
{...rest}
|
|
83
|
+
>
|
|
84
|
+
{title && <title>{title}</title>}
|
|
85
|
+
{shapes.map((shape, i) => (
|
|
86
|
+
<Shape key={i} shape={shape} fill={spuntoLogoFill(paint, shape.role) ?? paint.ink} />
|
|
87
|
+
))}
|
|
88
|
+
</svg>
|
|
89
|
+
)
|
|
90
|
+
}
|