@principal-ai/logo-component 0.1.16 → 0.1.17

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.
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import type { Theme } from '@principal-ade/industry-theme';
3
+ export type FileCityMark = 'P' | 'AI' | 'PAI' | 'lockup' | 'none';
4
+ /**
5
+ * How the per-file shading of the mark is chosen.
6
+ * - `scatter`: each file picks a random shade (the default texture)
7
+ * - `vertical` / `horizontal` / `diagonal`: a smooth directional gradient
8
+ * across the grid, dark → light.
9
+ */
10
+ export type FileCityGradient = 'scatter' | 'vertical' | 'horizontal' | 'diagonal';
11
+ export interface FileCityLogoProps {
12
+ width?: number;
13
+ height?: number;
14
+ /**
15
+ * Which brand mark the primary-colored files spell out. `'none'` is a
16
+ * plain city grid with an optional single highlighted file.
17
+ */
18
+ mark?: FileCityMark;
19
+ /**
20
+ * Primary color — the tint of the files that form the mark and the
21
+ * brighter building shades. Defaults to the theme's `primary`.
22
+ */
23
+ primary?: string;
24
+ /**
25
+ * Accent color used for the `trail` overlay so it stands apart from
26
+ * the primary-colored mark. Defaults to the theme's `accent` (falling
27
+ * back to the primary color).
28
+ */
29
+ accent?: string;
30
+ /**
31
+ * Base color the neutral building tints are derived from (the
32
+ * equivalent of the theme's text color in TrailCityDiagram).
33
+ */
34
+ color?: string;
35
+ /**
36
+ * Panel background behind the grid. Pass `"transparent"` to drop the
37
+ * panel and let the mark sit directly on its surface.
38
+ */
39
+ background?: string;
40
+ /**
41
+ * Optional theme — fills `primary` / `color` / `background` for any
42
+ * that aren't passed explicitly, so the logo can ride a theme like
43
+ * TrailCityDiagram does, or take raw colors like the other logos.
44
+ */
45
+ theme?: Theme;
46
+ /** Grid size for the `'none'` plain-city variant (columns === rows). */
47
+ cells?: number;
48
+ /** For `mark="none"`: highlight one center file in the primary color. */
49
+ highlight?: boolean;
50
+ /**
51
+ * Render part of the mark as a code trail — the lower stem + base of
52
+ * the P's bowl become a dashed path with marker dots instead of solid
53
+ * files, nodding to TrailCityDiagram. Only applies to marks containing
54
+ * a "P" (`"P"` and `"PAI"`).
55
+ */
56
+ trail?: boolean;
57
+ /**
58
+ * How the mark's files are shaded. `diagonal` (default) runs a smooth
59
+ * dark→light gradient corner to corner; `vertical` / `horizontal` do
60
+ * the same in other directions; `scatter` gives each file a random shade.
61
+ */
62
+ gradient?: FileCityGradient;
63
+ /** Round the panel corners. Default true. */
64
+ rounded?: boolean;
65
+ opacity?: number;
66
+ }
67
+ /**
68
+ * A compact, iconic "file city" mark — the same top-down grid of file
69
+ * squares as {@link TrailCityDiagram}, distilled to a logo: a small grid
70
+ * of smaller buildings, no trail / markers / snippet. The primary-colored
71
+ * files spell out the brand mark (P / AI / PAI) so the letters emerge
72
+ * from the muted city.
73
+ */
74
+ export declare const FileCityLogo: React.FC<FileCityLogoProps>;
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileCityLogo = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const VIEW = 100;
39
+ // 5-row pixel glyphs — the primary-colored files trace these.
40
+ // `1` = a mark (primary) file
41
+ // `H` = the letter's counter — a forced hole (background shows through,
42
+ // no city file) so the loop reads as a real hole
43
+ // `0` = a city file (muted backdrop)
44
+ const GLYPHS = {
45
+ // Closed bowl with a single hole punched in its center.
46
+ P: ['111', '1H1', '111', '100', '100'],
47
+ A: ['0110', '1HH1', '1111', '1001', '1001'],
48
+ I: ['111', '010', '010', '010', '111'],
49
+ };
50
+ const MARK_LETTERS = {
51
+ P: ['P'],
52
+ AI: ['A', 'I'],
53
+ PAI: ['P', 'A', 'I'],
54
+ };
55
+ /** Compose letters side by side (1 city column between them) into a
56
+ * single kind map: 1 = mark, 2 = void/hole, 0 = city. */
57
+ function composeMark(letters, gap = 1) {
58
+ const rows = 5;
59
+ const out = Array.from({ length: rows }, () => []);
60
+ letters.forEach((key, li) => {
61
+ if (li > 0)
62
+ for (let r = 0; r < rows; r++)
63
+ out[r].push(...new Array(gap).fill(0));
64
+ const glyph = GLYPHS[key];
65
+ for (let r = 0; r < rows; r++) {
66
+ for (const ch of glyph[r])
67
+ out[r].push(ch === '1' ? 1 : ch === 'H' ? 2 : 0);
68
+ }
69
+ });
70
+ return out;
71
+ }
72
+ function mulberry32(seed) {
73
+ let t = seed;
74
+ return () => {
75
+ t = (t + 0x6d2b79f5) | 0;
76
+ let r = t;
77
+ r = Math.imul(r ^ (r >>> 15), r | 1);
78
+ r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
79
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
80
+ };
81
+ }
82
+ function withAlpha(color, alpha) {
83
+ // Accept #rgb / #rrggbb — leave non-hex as-is and rely on the caller.
84
+ if (/^#([0-9a-f]{3}){1,2}$/i.test(color)) {
85
+ let hex = color.slice(1);
86
+ if (hex.length === 3)
87
+ hex = hex.split('').map(c => c + c).join('');
88
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255)
89
+ .toString(16)
90
+ .padStart(2, '0');
91
+ return `#${hex}${a}`;
92
+ }
93
+ return color;
94
+ }
95
+ function toRgb(color) {
96
+ if (!/^#([0-9a-f]{3}){1,2}$/i.test(color))
97
+ return null;
98
+ let hex = color.slice(1);
99
+ if (hex.length === 3)
100
+ hex = hex.split('').map(c => c + c).join('');
101
+ return [
102
+ parseInt(hex.slice(0, 2), 16),
103
+ parseInt(hex.slice(2, 4), 16),
104
+ parseInt(hex.slice(4, 6), 16),
105
+ ];
106
+ }
107
+ /** Blend two colors. `t` = 0 returns `a`, `t` = 1 returns `b`. Falls
108
+ * back to `a` when either side isn't a parseable hex. */
109
+ function mix(a, b, t) {
110
+ const ca = toRgb(a);
111
+ const cb = toRgb(b);
112
+ if (!ca || !cb)
113
+ return a;
114
+ const ch = (i) => Math.round(ca[i] + (cb[i] - ca[i]) * Math.max(0, Math.min(1, t)))
115
+ .toString(16)
116
+ .padStart(2, '0');
117
+ return `#${ch(0)}${ch(1)}${ch(2)}`;
118
+ }
119
+ /**
120
+ * A compact, iconic "file city" mark — the same top-down grid of file
121
+ * squares as {@link TrailCityDiagram}, distilled to a logo: a small grid
122
+ * of smaller buildings, no trail / markers / snippet. The primary-colored
123
+ * files spell out the brand mark (P / AI / PAI) so the letters emerge
124
+ * from the muted city.
125
+ */
126
+ const FileCityLogo = ({ width = 150, height = 150, mark = 'P', primary, accent, color, background, theme, cells = 5, highlight = true, trail = false, gradient = 'diagonal', rounded = true, opacity = 0.9, }) => {
127
+ var _a, _b, _c, _d;
128
+ const uid = (0, react_1.useId)().replace(/:/g, '');
129
+ const primaryColor = (_a = primary !== null && primary !== void 0 ? primary : theme === null || theme === void 0 ? void 0 : theme.colors.primary) !== null && _a !== void 0 ? _a : '#22d3ee';
130
+ const accentColor = (_b = accent !== null && accent !== void 0 ? accent : theme === null || theme === void 0 ? void 0 : theme.colors.accent) !== null && _b !== void 0 ? _b : primaryColor;
131
+ const baseColor = (_c = color !== null && color !== void 0 ? color : theme === null || theme === void 0 ? void 0 : theme.colors.text) !== null && _c !== void 0 ? _c : '#f8fafc';
132
+ const bgColor = (_d = background !== null && background !== void 0 ? background : theme === null || theme === void 0 ? void 0 : theme.colors.background) !== null && _d !== void 0 ? _d : '#0a0f14';
133
+ // Same palette weighting as TrailCityDiagram so the two read as a set.
134
+ const palette = [
135
+ withAlpha(primaryColor, 0.18),
136
+ withAlpha(primaryColor, 0.32),
137
+ withAlpha(baseColor, 0.08),
138
+ withAlpha(baseColor, 0.14),
139
+ withAlpha(baseColor, 0.22),
140
+ ];
141
+ // The mark files vary in shade so the letter doesn't read as one flat
142
+ // block, but every shade stays vivid so it still stands out from the
143
+ // muted city behind it.
144
+ const markPalette = [
145
+ primaryColor,
146
+ mix(primaryColor, '#ffffff', 0.22),
147
+ mix(primaryColor, '#ffffff', 0.1),
148
+ withAlpha(primaryColor, 0.82),
149
+ ];
150
+ // Same idea for `accent` cells (e.g. the AI in the lockup), so they
151
+ // read in the accent color while the P stays primary.
152
+ const accentPalette = [
153
+ accentColor,
154
+ mix(accentColor, '#ffffff', 0.22),
155
+ mix(accentColor, '#ffffff', 0.1),
156
+ withAlpha(accentColor, 0.82),
157
+ ];
158
+ // Dimmed accent — used for the A in the lockup so it sits back from the
159
+ // brighter I.
160
+ const accentDimPalette = [
161
+ withAlpha(accentColor, 0.5),
162
+ withAlpha(accentColor, 0.62),
163
+ withAlpha(accentColor, 0.42),
164
+ withAlpha(accentColor, 0.55),
165
+ ];
166
+ // Build a square grid. For `mark="none"` it's a plain `cells × cells`
167
+ // city; otherwise the glyph bitmap sits inside a square grid (side =
168
+ // longest glyph dimension + a margin ring) so the city reads square.
169
+ const margin = 1;
170
+ const trailEnabled = trail && (mark === 'P' || mark === 'PAI');
171
+ let side;
172
+ let offX = 0;
173
+ let offY = 0;
174
+ let kindAt;
175
+ if (mark === 'none') {
176
+ side = cells;
177
+ const hi = Math.floor((cells - 1) / 2);
178
+ kindAt = (c, r) => (highlight && c === hi && r === hi ? 'mark' : 'city');
179
+ }
180
+ else if (mark === 'lockup') {
181
+ // Brand lockup: P on the left (cols 0-2, vertically centered), a gap
182
+ // column, then a stacked "AI" filling the right 3 columns — A in the
183
+ // top 4 rows, I in the bottom 3.
184
+ side = 7;
185
+ const pGlyph = GLYPHS.P; // 3 × 5
186
+ const aGlyph = ['111', '1H1', '111', '101']; // 3 × 4
187
+ const iGlyph = ['111', '010', '111']; // 3 × 3
188
+ const at = (g, gc, gr) => {
189
+ if (gr < 0 || gr >= g.length)
190
+ return null;
191
+ const row = g[gr];
192
+ return gc < 0 || gc >= row.length ? null : row[gc];
193
+ };
194
+ kindAt = (c, r) => {
195
+ // P is primary; the stacked AI is accent.
196
+ const pch = at(pGlyph, c, r - 1); // P: cols 0-2, rows 1-5
197
+ if (pch != null) {
198
+ return pch === '1' ? 'mark' : pch === 'H' ? 'void' : 'city';
199
+ }
200
+ const ach = at(aGlyph, c - 4, r); // A: cols 4-6, rows 0-3 (dimmer)
201
+ if (ach != null) {
202
+ return ach === '1' ? 'accentDim' : ach === 'H' ? 'void' : 'city';
203
+ }
204
+ const ich = at(iGlyph, c - 4, r - 4); // I: cols 4-6, rows 4-6 (brighter)
205
+ if (ich != null) {
206
+ return ich === '1' ? 'accent' : ich === 'H' ? 'void' : 'city';
207
+ }
208
+ return 'city';
209
+ };
210
+ }
211
+ else {
212
+ const bitmap = composeMark(MARK_LETTERS[mark]);
213
+ const bw = bitmap[0].length;
214
+ const bh = bitmap.length;
215
+ side = Math.max(bw, bh) + margin * 2;
216
+ // With a trail, shove the letter to the left so the trail has room
217
+ // on the right; otherwise center it in the square.
218
+ offX = trailEnabled ? margin : Math.floor((side - bw) / 2);
219
+ offY = Math.floor((side - bh) / 2);
220
+ kindAt = (c, r) => {
221
+ const bc = c - offX;
222
+ const br = r - offY;
223
+ if (br < 0 || br >= bh || bc < 0 || bc >= bw)
224
+ return 'city';
225
+ const v = bitmap[br][bc];
226
+ return v === 1 ? 'mark' : v === 2 ? 'void' : 'city';
227
+ };
228
+ }
229
+ const cols = side;
230
+ const rows = side;
231
+ // Trail to the right of the letter — starts in the top-right corner,
232
+ // crosses 2 cells diagonally to its first node, zigzags down, and ends
233
+ // in the bottom-right corner. The dots sit on city files, which are
234
+ // forced to render under them.
235
+ const trailGrid = trailEnabled
236
+ ? [
237
+ [cols - 1, 0], // start: top-right corner
238
+ [cols - 3, 2], // first segment crosses 2 diagonally
239
+ [cols - 2, 4],
240
+ [cols - 1, rows - 1], // end: bottom-right corner
241
+ ]
242
+ : [];
243
+ const trailKeys = new Set(trailGrid.map(([c, r]) => `${c}-${r}`));
244
+ const centerOf = (c, r) => ({
245
+ x: startX + c * cell + cell / 2,
246
+ y: startY + r * cell + cell / 2,
247
+ });
248
+ // True when an edge neighbor is a letter file (mark or accent) — those
249
+ // city cells must always render so no empty square shares a side with
250
+ // the letter (and breaks a stroke). Diagonal corners are allowed to go
251
+ // empty, so the margins around the letter still get some missing squares.
252
+ const isLetterKind = (k) => k === 'mark' || k === 'accent' || k === 'accentDim';
253
+ const touchesMark = (c, r) => isLetterKind(kindAt(c - 1, r)) ||
254
+ isLetterKind(kindAt(c + 1, r)) ||
255
+ isLetterKind(kindAt(c, r - 1)) ||
256
+ isLetterKind(kindAt(c, r + 1));
257
+ // Per-file shade for a letter cell. `scatter` keeps the random-shade
258
+ // texture; the directional modes interpolate the letter's base color
259
+ // dark→light across the grid.
260
+ const baseFor = (kind) => kind === 'accent'
261
+ ? accentColor
262
+ : kind === 'accentDim'
263
+ ? mix(accentColor, bgColor, 0.45)
264
+ : primaryColor;
265
+ const gradientShade = (base, c, r) => {
266
+ const fx = cols > 1 ? c / (cols - 1) : 0;
267
+ const fy = rows > 1 ? r / (rows - 1) : 0;
268
+ const t = gradient === 'vertical'
269
+ ? fy
270
+ : gradient === 'horizontal'
271
+ ? fx
272
+ : (fx + fy) / 2; // diagonal
273
+ const lo = mix(base, '#000000', 0.12);
274
+ const hi = mix(base, '#ffffff', 0.4);
275
+ return mix(lo, hi, t);
276
+ };
277
+ // Square files centered in the panel; letterboxed when the mark is
278
+ // wider than it is tall (AI / PAI).
279
+ const pad = 12;
280
+ const avail = VIEW - pad * 2;
281
+ const cell = Math.min(avail / cols, avail / rows);
282
+ const sq = cell * 0.74; // smaller than the cell → gaps between buildings
283
+ const inset = (cell - sq) / 2;
284
+ const startX = (VIEW - cell * cols) / 2;
285
+ const startY = (VIEW - cell * rows) / 2;
286
+ const rng = mulberry32(7);
287
+ const files = [];
288
+ // Track the previous cell to the left (per row) and above (per column)
289
+ // so we never leave two empty squares back to back in either
290
+ // direction — that's what produced the long blank runs.
291
+ const colEmpty = new Array(cols).fill(false);
292
+ for (let r = 0; r < rows; r++) {
293
+ let lastEmpty = false;
294
+ for (let c = 0; c < cols; c++) {
295
+ const kind = kindAt(c, r);
296
+ const skipRoll = rng();
297
+ const colorRoll = rng();
298
+ // Void cells are the letter's counter — leave them empty so the
299
+ // background shows through as a real hole. Mark files always
300
+ // render; city files skip occasionally so the primary letters read
301
+ // against a sparser, muted backdrop — but only when the square to
302
+ // the left and the square above are both filled, so gaps stay
303
+ // isolated.
304
+ if (kind === 'void') {
305
+ lastEmpty = true;
306
+ colEmpty[c] = true;
307
+ continue;
308
+ }
309
+ if (kind === 'city' && !lastEmpty && !colEmpty[c] && !touchesMark(c, r) && !trailKeys.has(`${c}-${r}`) && skipRoll < 0.2) {
310
+ lastEmpty = true;
311
+ colEmpty[c] = true;
312
+ continue;
313
+ }
314
+ lastEmpty = false;
315
+ colEmpty[c] = false;
316
+ const isLetter = isLetterKind(kind);
317
+ const letterPalette = kind === 'accent'
318
+ ? accentPalette
319
+ : kind === 'accentDim'
320
+ ? accentDimPalette
321
+ : markPalette;
322
+ const letterColor = kind === 'accent' || kind === 'accentDim' ? accentColor : primaryColor;
323
+ const letterFill = gradient === 'scatter'
324
+ ? letterPalette[Math.floor(colorRoll * letterPalette.length)]
325
+ : gradientShade(baseFor(kind), c, r);
326
+ const x = startX + c * cell + inset;
327
+ const y = startY + r * cell + inset;
328
+ files.push(react_1.default.createElement("rect", { key: `${c}-${r}`, x: x, y: y, width: sq, height: sq, rx: Math.max(1, sq * 0.12), fill: isLetter
329
+ ? letterFill
330
+ : palette[Math.floor(colorRoll * palette.length)], stroke: isLetter ? withAlpha(letterColor, 0.55) : withAlpha(baseColor, 0.08), strokeWidth: isLetter ? 0.6 : 0.4 }));
331
+ }
332
+ }
333
+ // Trail overlay — a dashed accent path to the right of the letter with
334
+ // a marker dot on each node.
335
+ if (trailEnabled && trailGrid.length > 1) {
336
+ const linePts = trailGrid.map(([c, r]) => centerOf(c, r));
337
+ const d = linePts.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ');
338
+ const dot = Math.max(1.5, sq * 0.3);
339
+ files.push(react_1.default.createElement("path", { key: "trail-path", d: d, fill: "none", stroke: accentColor, strokeWidth: Math.max(1, cell * 0.12), strokeLinecap: "round", strokeLinejoin: "round", strokeDasharray: `${cell * 0.18} ${cell * 0.18}`, opacity: 0.9 }));
340
+ trailGrid.forEach(([c, r], i) => {
341
+ const p = centerOf(c, r);
342
+ files.push(react_1.default.createElement("circle", { key: `trail-dot-${i}`, cx: p.x, cy: p.y, r: dot, fill: mix(accentColor, '#ffffff', 0.15), stroke: bgColor, strokeWidth: 0.5 }));
343
+ });
344
+ }
345
+ return (react_1.default.createElement("svg", { width: width, height: height, viewBox: `0 0 ${VIEW} ${VIEW}`, xmlns: "http://www.w3.org/2000/svg", style: { opacity, display: 'block', overflow: 'visible' }, role: "img", "aria-label": mark === 'none'
346
+ ? 'A top-down grid of file squares'
347
+ : `A top-down grid of file squares spelling ${mark}` },
348
+ react_1.default.createElement("defs", null,
349
+ react_1.default.createElement("clipPath", { id: `panel-${uid}` },
350
+ react_1.default.createElement("rect", { x: 0, y: 0, width: VIEW, height: VIEW, rx: rounded ? 14 : 0 }))),
351
+ react_1.default.createElement("g", { clipPath: `url(#panel-${uid})` },
352
+ react_1.default.createElement("rect", { x: 0, y: 0, width: VIEW, height: VIEW, fill: bgColor, rx: rounded ? 14 : 0 }),
353
+ files)));
354
+ };
355
+ exports.FileCityLogo = FileCityLogo;
@@ -0,0 +1,318 @@
1
+ import React, { useId } from 'react';
2
+ const VIEW = 100;
3
+ // 5-row pixel glyphs — the primary-colored files trace these.
4
+ // `1` = a mark (primary) file
5
+ // `H` = the letter's counter — a forced hole (background shows through,
6
+ // no city file) so the loop reads as a real hole
7
+ // `0` = a city file (muted backdrop)
8
+ const GLYPHS = {
9
+ // Closed bowl with a single hole punched in its center.
10
+ P: ['111', '1H1', '111', '100', '100'],
11
+ A: ['0110', '1HH1', '1111', '1001', '1001'],
12
+ I: ['111', '010', '010', '010', '111'],
13
+ };
14
+ const MARK_LETTERS = {
15
+ P: ['P'],
16
+ AI: ['A', 'I'],
17
+ PAI: ['P', 'A', 'I'],
18
+ };
19
+ /** Compose letters side by side (1 city column between them) into a
20
+ * single kind map: 1 = mark, 2 = void/hole, 0 = city. */
21
+ function composeMark(letters, gap = 1) {
22
+ const rows = 5;
23
+ const out = Array.from({ length: rows }, () => []);
24
+ letters.forEach((key, li) => {
25
+ if (li > 0)
26
+ for (let r = 0; r < rows; r++)
27
+ out[r].push(...new Array(gap).fill(0));
28
+ const glyph = GLYPHS[key];
29
+ for (let r = 0; r < rows; r++) {
30
+ for (const ch of glyph[r])
31
+ out[r].push(ch === '1' ? 1 : ch === 'H' ? 2 : 0);
32
+ }
33
+ });
34
+ return out;
35
+ }
36
+ function mulberry32(seed) {
37
+ let t = seed;
38
+ return () => {
39
+ t = (t + 0x6d2b79f5) | 0;
40
+ let r = t;
41
+ r = Math.imul(r ^ (r >>> 15), r | 1);
42
+ r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
43
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
44
+ };
45
+ }
46
+ function withAlpha(color, alpha) {
47
+ // Accept #rgb / #rrggbb — leave non-hex as-is and rely on the caller.
48
+ if (/^#([0-9a-f]{3}){1,2}$/i.test(color)) {
49
+ let hex = color.slice(1);
50
+ if (hex.length === 3)
51
+ hex = hex.split('').map(c => c + c).join('');
52
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255)
53
+ .toString(16)
54
+ .padStart(2, '0');
55
+ return `#${hex}${a}`;
56
+ }
57
+ return color;
58
+ }
59
+ function toRgb(color) {
60
+ if (!/^#([0-9a-f]{3}){1,2}$/i.test(color))
61
+ return null;
62
+ let hex = color.slice(1);
63
+ if (hex.length === 3)
64
+ hex = hex.split('').map(c => c + c).join('');
65
+ return [
66
+ parseInt(hex.slice(0, 2), 16),
67
+ parseInt(hex.slice(2, 4), 16),
68
+ parseInt(hex.slice(4, 6), 16),
69
+ ];
70
+ }
71
+ /** Blend two colors. `t` = 0 returns `a`, `t` = 1 returns `b`. Falls
72
+ * back to `a` when either side isn't a parseable hex. */
73
+ function mix(a, b, t) {
74
+ const ca = toRgb(a);
75
+ const cb = toRgb(b);
76
+ if (!ca || !cb)
77
+ return a;
78
+ const ch = (i) => Math.round(ca[i] + (cb[i] - ca[i]) * Math.max(0, Math.min(1, t)))
79
+ .toString(16)
80
+ .padStart(2, '0');
81
+ return `#${ch(0)}${ch(1)}${ch(2)}`;
82
+ }
83
+ /**
84
+ * A compact, iconic "file city" mark — the same top-down grid of file
85
+ * squares as {@link TrailCityDiagram}, distilled to a logo: a small grid
86
+ * of smaller buildings, no trail / markers / snippet. The primary-colored
87
+ * files spell out the brand mark (P / AI / PAI) so the letters emerge
88
+ * from the muted city.
89
+ */
90
+ export const FileCityLogo = ({ width = 150, height = 150, mark = 'P', primary, accent, color, background, theme, cells = 5, highlight = true, trail = false, gradient = 'diagonal', rounded = true, opacity = 0.9, }) => {
91
+ var _a, _b, _c, _d;
92
+ const uid = useId().replace(/:/g, '');
93
+ const primaryColor = (_a = primary !== null && primary !== void 0 ? primary : theme === null || theme === void 0 ? void 0 : theme.colors.primary) !== null && _a !== void 0 ? _a : '#22d3ee';
94
+ const accentColor = (_b = accent !== null && accent !== void 0 ? accent : theme === null || theme === void 0 ? void 0 : theme.colors.accent) !== null && _b !== void 0 ? _b : primaryColor;
95
+ const baseColor = (_c = color !== null && color !== void 0 ? color : theme === null || theme === void 0 ? void 0 : theme.colors.text) !== null && _c !== void 0 ? _c : '#f8fafc';
96
+ const bgColor = (_d = background !== null && background !== void 0 ? background : theme === null || theme === void 0 ? void 0 : theme.colors.background) !== null && _d !== void 0 ? _d : '#0a0f14';
97
+ // Same palette weighting as TrailCityDiagram so the two read as a set.
98
+ const palette = [
99
+ withAlpha(primaryColor, 0.18),
100
+ withAlpha(primaryColor, 0.32),
101
+ withAlpha(baseColor, 0.08),
102
+ withAlpha(baseColor, 0.14),
103
+ withAlpha(baseColor, 0.22),
104
+ ];
105
+ // The mark files vary in shade so the letter doesn't read as one flat
106
+ // block, but every shade stays vivid so it still stands out from the
107
+ // muted city behind it.
108
+ const markPalette = [
109
+ primaryColor,
110
+ mix(primaryColor, '#ffffff', 0.22),
111
+ mix(primaryColor, '#ffffff', 0.1),
112
+ withAlpha(primaryColor, 0.82),
113
+ ];
114
+ // Same idea for `accent` cells (e.g. the AI in the lockup), so they
115
+ // read in the accent color while the P stays primary.
116
+ const accentPalette = [
117
+ accentColor,
118
+ mix(accentColor, '#ffffff', 0.22),
119
+ mix(accentColor, '#ffffff', 0.1),
120
+ withAlpha(accentColor, 0.82),
121
+ ];
122
+ // Dimmed accent — used for the A in the lockup so it sits back from the
123
+ // brighter I.
124
+ const accentDimPalette = [
125
+ withAlpha(accentColor, 0.5),
126
+ withAlpha(accentColor, 0.62),
127
+ withAlpha(accentColor, 0.42),
128
+ withAlpha(accentColor, 0.55),
129
+ ];
130
+ // Build a square grid. For `mark="none"` it's a plain `cells × cells`
131
+ // city; otherwise the glyph bitmap sits inside a square grid (side =
132
+ // longest glyph dimension + a margin ring) so the city reads square.
133
+ const margin = 1;
134
+ const trailEnabled = trail && (mark === 'P' || mark === 'PAI');
135
+ let side;
136
+ let offX = 0;
137
+ let offY = 0;
138
+ let kindAt;
139
+ if (mark === 'none') {
140
+ side = cells;
141
+ const hi = Math.floor((cells - 1) / 2);
142
+ kindAt = (c, r) => (highlight && c === hi && r === hi ? 'mark' : 'city');
143
+ }
144
+ else if (mark === 'lockup') {
145
+ // Brand lockup: P on the left (cols 0-2, vertically centered), a gap
146
+ // column, then a stacked "AI" filling the right 3 columns — A in the
147
+ // top 4 rows, I in the bottom 3.
148
+ side = 7;
149
+ const pGlyph = GLYPHS.P; // 3 × 5
150
+ const aGlyph = ['111', '1H1', '111', '101']; // 3 × 4
151
+ const iGlyph = ['111', '010', '111']; // 3 × 3
152
+ const at = (g, gc, gr) => {
153
+ if (gr < 0 || gr >= g.length)
154
+ return null;
155
+ const row = g[gr];
156
+ return gc < 0 || gc >= row.length ? null : row[gc];
157
+ };
158
+ kindAt = (c, r) => {
159
+ // P is primary; the stacked AI is accent.
160
+ const pch = at(pGlyph, c, r - 1); // P: cols 0-2, rows 1-5
161
+ if (pch != null) {
162
+ return pch === '1' ? 'mark' : pch === 'H' ? 'void' : 'city';
163
+ }
164
+ const ach = at(aGlyph, c - 4, r); // A: cols 4-6, rows 0-3 (dimmer)
165
+ if (ach != null) {
166
+ return ach === '1' ? 'accentDim' : ach === 'H' ? 'void' : 'city';
167
+ }
168
+ const ich = at(iGlyph, c - 4, r - 4); // I: cols 4-6, rows 4-6 (brighter)
169
+ if (ich != null) {
170
+ return ich === '1' ? 'accent' : ich === 'H' ? 'void' : 'city';
171
+ }
172
+ return 'city';
173
+ };
174
+ }
175
+ else {
176
+ const bitmap = composeMark(MARK_LETTERS[mark]);
177
+ const bw = bitmap[0].length;
178
+ const bh = bitmap.length;
179
+ side = Math.max(bw, bh) + margin * 2;
180
+ // With a trail, shove the letter to the left so the trail has room
181
+ // on the right; otherwise center it in the square.
182
+ offX = trailEnabled ? margin : Math.floor((side - bw) / 2);
183
+ offY = Math.floor((side - bh) / 2);
184
+ kindAt = (c, r) => {
185
+ const bc = c - offX;
186
+ const br = r - offY;
187
+ if (br < 0 || br >= bh || bc < 0 || bc >= bw)
188
+ return 'city';
189
+ const v = bitmap[br][bc];
190
+ return v === 1 ? 'mark' : v === 2 ? 'void' : 'city';
191
+ };
192
+ }
193
+ const cols = side;
194
+ const rows = side;
195
+ // Trail to the right of the letter — starts in the top-right corner,
196
+ // crosses 2 cells diagonally to its first node, zigzags down, and ends
197
+ // in the bottom-right corner. The dots sit on city files, which are
198
+ // forced to render under them.
199
+ const trailGrid = trailEnabled
200
+ ? [
201
+ [cols - 1, 0], // start: top-right corner
202
+ [cols - 3, 2], // first segment crosses 2 diagonally
203
+ [cols - 2, 4],
204
+ [cols - 1, rows - 1], // end: bottom-right corner
205
+ ]
206
+ : [];
207
+ const trailKeys = new Set(trailGrid.map(([c, r]) => `${c}-${r}`));
208
+ const centerOf = (c, r) => ({
209
+ x: startX + c * cell + cell / 2,
210
+ y: startY + r * cell + cell / 2,
211
+ });
212
+ // True when an edge neighbor is a letter file (mark or accent) — those
213
+ // city cells must always render so no empty square shares a side with
214
+ // the letter (and breaks a stroke). Diagonal corners are allowed to go
215
+ // empty, so the margins around the letter still get some missing squares.
216
+ const isLetterKind = (k) => k === 'mark' || k === 'accent' || k === 'accentDim';
217
+ const touchesMark = (c, r) => isLetterKind(kindAt(c - 1, r)) ||
218
+ isLetterKind(kindAt(c + 1, r)) ||
219
+ isLetterKind(kindAt(c, r - 1)) ||
220
+ isLetterKind(kindAt(c, r + 1));
221
+ // Per-file shade for a letter cell. `scatter` keeps the random-shade
222
+ // texture; the directional modes interpolate the letter's base color
223
+ // dark→light across the grid.
224
+ const baseFor = (kind) => kind === 'accent'
225
+ ? accentColor
226
+ : kind === 'accentDim'
227
+ ? mix(accentColor, bgColor, 0.45)
228
+ : primaryColor;
229
+ const gradientShade = (base, c, r) => {
230
+ const fx = cols > 1 ? c / (cols - 1) : 0;
231
+ const fy = rows > 1 ? r / (rows - 1) : 0;
232
+ const t = gradient === 'vertical'
233
+ ? fy
234
+ : gradient === 'horizontal'
235
+ ? fx
236
+ : (fx + fy) / 2; // diagonal
237
+ const lo = mix(base, '#000000', 0.12);
238
+ const hi = mix(base, '#ffffff', 0.4);
239
+ return mix(lo, hi, t);
240
+ };
241
+ // Square files centered in the panel; letterboxed when the mark is
242
+ // wider than it is tall (AI / PAI).
243
+ const pad = 12;
244
+ const avail = VIEW - pad * 2;
245
+ const cell = Math.min(avail / cols, avail / rows);
246
+ const sq = cell * 0.74; // smaller than the cell → gaps between buildings
247
+ const inset = (cell - sq) / 2;
248
+ const startX = (VIEW - cell * cols) / 2;
249
+ const startY = (VIEW - cell * rows) / 2;
250
+ const rng = mulberry32(7);
251
+ const files = [];
252
+ // Track the previous cell to the left (per row) and above (per column)
253
+ // so we never leave two empty squares back to back in either
254
+ // direction — that's what produced the long blank runs.
255
+ const colEmpty = new Array(cols).fill(false);
256
+ for (let r = 0; r < rows; r++) {
257
+ let lastEmpty = false;
258
+ for (let c = 0; c < cols; c++) {
259
+ const kind = kindAt(c, r);
260
+ const skipRoll = rng();
261
+ const colorRoll = rng();
262
+ // Void cells are the letter's counter — leave them empty so the
263
+ // background shows through as a real hole. Mark files always
264
+ // render; city files skip occasionally so the primary letters read
265
+ // against a sparser, muted backdrop — but only when the square to
266
+ // the left and the square above are both filled, so gaps stay
267
+ // isolated.
268
+ if (kind === 'void') {
269
+ lastEmpty = true;
270
+ colEmpty[c] = true;
271
+ continue;
272
+ }
273
+ if (kind === 'city' && !lastEmpty && !colEmpty[c] && !touchesMark(c, r) && !trailKeys.has(`${c}-${r}`) && skipRoll < 0.2) {
274
+ lastEmpty = true;
275
+ colEmpty[c] = true;
276
+ continue;
277
+ }
278
+ lastEmpty = false;
279
+ colEmpty[c] = false;
280
+ const isLetter = isLetterKind(kind);
281
+ const letterPalette = kind === 'accent'
282
+ ? accentPalette
283
+ : kind === 'accentDim'
284
+ ? accentDimPalette
285
+ : markPalette;
286
+ const letterColor = kind === 'accent' || kind === 'accentDim' ? accentColor : primaryColor;
287
+ const letterFill = gradient === 'scatter'
288
+ ? letterPalette[Math.floor(colorRoll * letterPalette.length)]
289
+ : gradientShade(baseFor(kind), c, r);
290
+ const x = startX + c * cell + inset;
291
+ const y = startY + r * cell + inset;
292
+ files.push(React.createElement("rect", { key: `${c}-${r}`, x: x, y: y, width: sq, height: sq, rx: Math.max(1, sq * 0.12), fill: isLetter
293
+ ? letterFill
294
+ : palette[Math.floor(colorRoll * palette.length)], stroke: isLetter ? withAlpha(letterColor, 0.55) : withAlpha(baseColor, 0.08), strokeWidth: isLetter ? 0.6 : 0.4 }));
295
+ }
296
+ }
297
+ // Trail overlay — a dashed accent path to the right of the letter with
298
+ // a marker dot on each node.
299
+ if (trailEnabled && trailGrid.length > 1) {
300
+ const linePts = trailGrid.map(([c, r]) => centerOf(c, r));
301
+ const d = linePts.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ');
302
+ const dot = Math.max(1.5, sq * 0.3);
303
+ files.push(React.createElement("path", { key: "trail-path", d: d, fill: "none", stroke: accentColor, strokeWidth: Math.max(1, cell * 0.12), strokeLinecap: "round", strokeLinejoin: "round", strokeDasharray: `${cell * 0.18} ${cell * 0.18}`, opacity: 0.9 }));
304
+ trailGrid.forEach(([c, r], i) => {
305
+ const p = centerOf(c, r);
306
+ files.push(React.createElement("circle", { key: `trail-dot-${i}`, cx: p.x, cy: p.y, r: dot, fill: mix(accentColor, '#ffffff', 0.15), stroke: bgColor, strokeWidth: 0.5 }));
307
+ });
308
+ }
309
+ return (React.createElement("svg", { width: width, height: height, viewBox: `0 0 ${VIEW} ${VIEW}`, xmlns: "http://www.w3.org/2000/svg", style: { opacity, display: 'block', overflow: 'visible' }, role: "img", "aria-label": mark === 'none'
310
+ ? 'A top-down grid of file squares'
311
+ : `A top-down grid of file squares spelling ${mark}` },
312
+ React.createElement("defs", null,
313
+ React.createElement("clipPath", { id: `panel-${uid}` },
314
+ React.createElement("rect", { x: 0, y: 0, width: VIEW, height: VIEW, rx: rounded ? 14 : 0 }))),
315
+ React.createElement("g", { clipPath: `url(#panel-${uid})` },
316
+ React.createElement("rect", { x: 0, y: 0, width: VIEW, height: VIEW, fill: bgColor, rx: rounded ? 14 : 0 }),
317
+ files)));
318
+ };
package/dist/esm/index.js CHANGED
@@ -7,5 +7,6 @@ export { TelemetryReveal } from './TelemetryReveal';
7
7
  export { TextReveal } from './TextReveal';
8
8
  export { OpenTypeTextReveal } from './OpenTypeTextReveal';
9
9
  export { TrailCityDiagram } from './TrailCityDiagram';
10
+ export { FileCityLogo } from './FileCityLogo';
10
11
  export { TELEMETRY_PRESETS } from './presets';
11
12
  export { STROKE_CHARACTERS, layoutText } from './strokeCharacters';
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export { TextReveal } from './TextReveal';
8
8
  export { OpenTypeTextReveal } from './OpenTypeTextReveal';
9
9
  export { TrailCityDiagram } from './TrailCityDiagram';
10
10
  export type { TrailCityDiagramProps } from './TrailCityDiagram';
11
+ export { FileCityLogo } from './FileCityLogo';
12
+ export type { FileCityLogoProps, FileCityMark, FileCityGradient } from './FileCityLogo';
11
13
  export { TELEMETRY_PRESETS } from './presets';
12
14
  export { STROKE_CHARACTERS, layoutText } from './strokeCharacters';
13
15
  export type { PathDefinition, ShapePreset } from './presets';
package/dist/index.esm.js CHANGED
@@ -7,5 +7,6 @@ export { TelemetryReveal } from './TelemetryReveal';
7
7
  export { TextReveal } from './TextReveal';
8
8
  export { OpenTypeTextReveal } from './OpenTypeTextReveal';
9
9
  export { TrailCityDiagram } from './TrailCityDiagram';
10
+ export { FileCityLogo } from './FileCityLogo';
10
11
  export { TELEMETRY_PRESETS } from './presets';
11
12
  export { STROKE_CHARACTERS, layoutText } from './strokeCharacters';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.layoutText = exports.STROKE_CHARACTERS = exports.TELEMETRY_PRESETS = exports.TrailCityDiagram = exports.OpenTypeTextReveal = exports.TextReveal = exports.TelemetryReveal = exports.WreathLogo = exports.SquareLogo = exports.ForksLogo = exports.LogoSmall = exports.Logo = void 0;
3
+ exports.layoutText = exports.STROKE_CHARACTERS = exports.TELEMETRY_PRESETS = exports.FileCityLogo = exports.TrailCityDiagram = exports.OpenTypeTextReveal = exports.TextReveal = exports.TelemetryReveal = exports.WreathLogo = exports.SquareLogo = exports.ForksLogo = exports.LogoSmall = exports.Logo = void 0;
4
4
  var Logo_1 = require("./Logo");
5
5
  Object.defineProperty(exports, "Logo", { enumerable: true, get: function () { return Logo_1.Logo; } });
6
6
  var LogoSmall_1 = require("./LogoSmall");
@@ -19,6 +19,8 @@ var OpenTypeTextReveal_1 = require("./OpenTypeTextReveal");
19
19
  Object.defineProperty(exports, "OpenTypeTextReveal", { enumerable: true, get: function () { return OpenTypeTextReveal_1.OpenTypeTextReveal; } });
20
20
  var TrailCityDiagram_1 = require("./TrailCityDiagram");
21
21
  Object.defineProperty(exports, "TrailCityDiagram", { enumerable: true, get: function () { return TrailCityDiagram_1.TrailCityDiagram; } });
22
+ var FileCityLogo_1 = require("./FileCityLogo");
23
+ Object.defineProperty(exports, "FileCityLogo", { enumerable: true, get: function () { return FileCityLogo_1.FileCityLogo; } });
22
24
  var presets_1 = require("./presets");
23
25
  Object.defineProperty(exports, "TELEMETRY_PRESETS", { enumerable: true, get: function () { return presets_1.TELEMETRY_PRESETS; } });
24
26
  var strokeCharacters_1 = require("./strokeCharacters");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/logo-component",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Animated wireframe sphere logo component",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -26,7 +26,7 @@
26
26
  "react": "^18.0.0 || ^19.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@principal-ade/industry-theme": "^0.1.19",
29
+ "@principal-ade/industry-theme": "^0.1.20",
30
30
  "@storybook/addon-essentials": "^8.6.14",
31
31
  "@storybook/blocks": "^8.6.14",
32
32
  "@storybook/react": "^8.6.14",