@principal-ai/logo-component 0.1.15 → 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.
- package/dist/FileCityLogo.d.ts +74 -0
- package/dist/FileCityLogo.js +355 -0
- package/dist/TrailCityDiagram.d.ts +50 -0
- package/dist/TrailCityDiagram.js +277 -0
- package/dist/esm/FileCityLogo.js +318 -0
- package/dist/esm/TrailCityDiagram.js +241 -0
- package/dist/esm/index.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +2 -0
- package/dist/index.js +5 -1
- package/package.json +2 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.TrailCityDiagram = TrailCityDiagram;
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
39
|
+
const VIEW_W = 700;
|
|
40
|
+
const VIEW_H = 700;
|
|
41
|
+
const CITY_COLS = 12;
|
|
42
|
+
const CITY_ROWS = 12;
|
|
43
|
+
const CELL_W = 50;
|
|
44
|
+
const CELL_H = 50;
|
|
45
|
+
const CITY_OFFSET_X = 50;
|
|
46
|
+
const CITY_OFFSET_Y = 50;
|
|
47
|
+
/** Center point of a grid cell, in viewBox coords. */
|
|
48
|
+
function cellCenter(col, row) {
|
|
49
|
+
return {
|
|
50
|
+
x: CITY_OFFSET_X + col * CELL_W + CELL_W / 2,
|
|
51
|
+
y: CITY_OFFSET_Y + row * CELL_H + CELL_H / 2,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function buildCity(palette, pinned) {
|
|
55
|
+
const rng = mulberry32(7);
|
|
56
|
+
const cells = [];
|
|
57
|
+
for (let r = 0; r < CITY_ROWS; r++) {
|
|
58
|
+
for (let c = 0; c < CITY_COLS; c++) {
|
|
59
|
+
const isPinned = pinned.has(`${c},${r}`);
|
|
60
|
+
// Pinned cells (the ones markers sit on top of) always get a
|
|
61
|
+
// building so the marker has something to be centered on. Other
|
|
62
|
+
// cells skip occasionally to break up the grid.
|
|
63
|
+
const skipRoll = rng();
|
|
64
|
+
// Keep the RNG sequence stable so colors don't shift after dropping
|
|
65
|
+
// the per-building size jitter.
|
|
66
|
+
rng();
|
|
67
|
+
rng();
|
|
68
|
+
const colorRoll = rng();
|
|
69
|
+
if (!isPinned && skipRoll < 0.18)
|
|
70
|
+
continue;
|
|
71
|
+
const w = CELL_W - 8;
|
|
72
|
+
const h = CELL_H - 8;
|
|
73
|
+
cells.push({
|
|
74
|
+
x: CITY_OFFSET_X + c * CELL_W + (CELL_W - w) / 2,
|
|
75
|
+
y: CITY_OFFSET_Y + r * CELL_H + (CELL_H - h) / 2,
|
|
76
|
+
w,
|
|
77
|
+
h,
|
|
78
|
+
fill: palette[Math.floor(colorRoll * palette.length)],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return cells;
|
|
83
|
+
}
|
|
84
|
+
function mulberry32(seed) {
|
|
85
|
+
let t = seed;
|
|
86
|
+
return () => {
|
|
87
|
+
t = (t + 0x6d2b79f5) | 0;
|
|
88
|
+
let r = t;
|
|
89
|
+
r = Math.imul(r ^ (r >>> 15), r | 1);
|
|
90
|
+
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
|
|
91
|
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** VH leader from `a` (marker) to `b` (snippet anchor): exits `a` going
|
|
95
|
+
* vertically toward `b.y`, then turns and runs horizontally to `b.x`. */
|
|
96
|
+
function lRoutePath(a, b, radius = 12) {
|
|
97
|
+
const goingUp = b.y < a.y;
|
|
98
|
+
const ySign = goingUp ? -1 : 1;
|
|
99
|
+
const xSign = b.x < a.x ? -1 : 1;
|
|
100
|
+
const r = Math.min(radius, Math.abs(a.y - b.y) / 2, Math.abs(a.x - b.x) / 2);
|
|
101
|
+
return [
|
|
102
|
+
`M ${a.x} ${a.y}`,
|
|
103
|
+
`L ${a.x} ${b.y - ySign * r}`,
|
|
104
|
+
`Q ${a.x} ${b.y} ${a.x + xSign * r} ${b.y}`,
|
|
105
|
+
`L ${b.x} ${b.y}`,
|
|
106
|
+
].join(' ');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Static SVG version of the FileCity trail overlay — a top-down grid of
|
|
110
|
+
* building rectangles with numbered markers wired by a dashed path, plus
|
|
111
|
+
* an L-routed leader line to a snippet card. Mirrors the look of
|
|
112
|
+
* `TrailFilePath` + `TrailLeaderLine` from the 3D panel without the
|
|
113
|
+
* three.js projection — suitable for marketing surfaces.
|
|
114
|
+
*/
|
|
115
|
+
function TrailCityDiagram({ theme, className, hideSnippet = false, snippetVisible = true, hideTrail = false, trailVisible = true, highlightTrail = false, stampRowVisible = false, userStamped = false, }) {
|
|
116
|
+
var _a, _b, _c, _d, _e, _f;
|
|
117
|
+
const snippetGateStyle = {
|
|
118
|
+
opacity: snippetVisible ? 1 : 0,
|
|
119
|
+
transition: 'opacity 500ms ease',
|
|
120
|
+
transitionDelay: snippetVisible ? '700ms' : '0ms',
|
|
121
|
+
};
|
|
122
|
+
const trailGateStyle = {
|
|
123
|
+
opacity: trailVisible ? 1 : 0,
|
|
124
|
+
transition: 'opacity 500ms ease',
|
|
125
|
+
};
|
|
126
|
+
const stampRowGateStyle = {
|
|
127
|
+
opacity: stampRowVisible ? 1 : 0,
|
|
128
|
+
transition: 'opacity 500ms ease',
|
|
129
|
+
transitionDelay: stampRowVisible ? '700ms' : '0ms',
|
|
130
|
+
};
|
|
131
|
+
const colors = theme === null || theme === void 0 ? void 0 : theme.colors;
|
|
132
|
+
const uid = (0, react_1.useId)().replace(/:/g, '');
|
|
133
|
+
const accent = (_a = colors === null || colors === void 0 ? void 0 : colors.primary) !== null && _a !== void 0 ? _a : '#22d3ee';
|
|
134
|
+
const surface = (_b = colors === null || colors === void 0 ? void 0 : colors.surface) !== null && _b !== void 0 ? _b : '#0f1419';
|
|
135
|
+
const text = (_c = colors === null || colors === void 0 ? void 0 : colors.text) !== null && _c !== void 0 ? _c : '#f8fafc';
|
|
136
|
+
const muted = (_d = colors === null || colors === void 0 ? void 0 : colors.textMuted) !== null && _d !== void 0 ? _d : '#94a3b8';
|
|
137
|
+
const bg = (_e = colors === null || colors === void 0 ? void 0 : colors.background) !== null && _e !== void 0 ? _e : '#0a0f14';
|
|
138
|
+
const success = (_f = colors === null || colors === void 0 ? void 0 : colors.success) !== null && _f !== void 0 ? _f : '#10b981';
|
|
139
|
+
// Silvery white-grey ink for ACK stamps — brighter than `muted` so it
|
|
140
|
+
// reads cleanly against the dark city.
|
|
141
|
+
const silver = '#d1d8e0';
|
|
142
|
+
const palette = (0, react_1.useMemo)(() => [
|
|
143
|
+
withAlpha(accent, 0.18),
|
|
144
|
+
withAlpha(accent, 0.32),
|
|
145
|
+
withAlpha(text, 0.08),
|
|
146
|
+
withAlpha(text, 0.14),
|
|
147
|
+
withAlpha(text, 0.22),
|
|
148
|
+
], [accent, text]);
|
|
149
|
+
const markerCells = (0, react_1.useMemo)(() => [
|
|
150
|
+
{ col: 1, row: 10, label: '1' },
|
|
151
|
+
{ col: 3, row: 7, label: '2' },
|
|
152
|
+
{ col: 6, row: 9, label: '3' },
|
|
153
|
+
{ col: 4, row: 5, label: '4' },
|
|
154
|
+
], []);
|
|
155
|
+
const pinnedCells = (0, react_1.useMemo)(() => new Set(markerCells.map(m => `${m.col},${m.row}`)), [markerCells]);
|
|
156
|
+
const buildings = (0, react_1.useMemo)(() => buildCity(palette, pinnedCells), [palette, pinnedCells]);
|
|
157
|
+
const markers = (0, react_1.useMemo)(() => markerCells.map(m => {
|
|
158
|
+
const c = cellCenter(m.col, m.row);
|
|
159
|
+
return { x: c.x, y: c.y, label: m.label };
|
|
160
|
+
}), [markerCells]);
|
|
161
|
+
const trailPath = (0, react_1.useMemo)(() => {
|
|
162
|
+
const [first, ...rest] = markers;
|
|
163
|
+
if (!first)
|
|
164
|
+
return '';
|
|
165
|
+
return [`M ${first.x} ${first.y}`, ...rest.map(m => `L ${m.x} ${m.y}`)].join(' ');
|
|
166
|
+
}, [markers]);
|
|
167
|
+
const activeMarker = markers[markers.length - 1];
|
|
168
|
+
const snippetAnchor = { x: VIEW_W - 320, y: 100 };
|
|
169
|
+
const leaderPath = (0, react_1.useMemo)(() => activeMarker
|
|
170
|
+
? lRoutePath({ x: activeMarker.x, y: activeMarker.y }, snippetAnchor)
|
|
171
|
+
: '', [activeMarker, snippetAnchor.x, snippetAnchor.y]);
|
|
172
|
+
return (react_1.default.createElement("svg", { viewBox: `0 0 ${VIEW_W} ${VIEW_H}`, className: className, style: { display: 'block', width: '100%', height: 'auto', overflow: 'visible' }, role: "img", "aria-label": "A trail of code locations connected across a top-down city of files" },
|
|
173
|
+
react_1.default.createElement("defs", null,
|
|
174
|
+
react_1.default.createElement("linearGradient", { id: `fade-${uid}`, x1: "0", x2: "0", y1: "0", y2: "1" },
|
|
175
|
+
react_1.default.createElement("stop", { offset: "0%", stopColor: bg, stopOpacity: 0 }),
|
|
176
|
+
react_1.default.createElement("stop", { offset: "100%", stopColor: bg, stopOpacity: 0.7 }))),
|
|
177
|
+
react_1.default.createElement("rect", { x: 0, y: 0, width: VIEW_W, height: VIEW_H, fill: bg, rx: 12 }),
|
|
178
|
+
react_1.default.createElement("g", { style: {
|
|
179
|
+
opacity: highlightTrail ? 0.2 : 1,
|
|
180
|
+
transition: 'opacity 300ms ease',
|
|
181
|
+
} }, buildings.map((b, i) => (react_1.default.createElement("rect", { key: i, x: b.x, y: b.y, width: b.w, height: b.h, fill: b.fill, stroke: withAlpha(text, 0.08), strokeWidth: 0.5, rx: 2 })))),
|
|
182
|
+
!hideSnippet && (() => {
|
|
183
|
+
const activeCell = markerCells[markerCells.length - 1];
|
|
184
|
+
if (!activeCell)
|
|
185
|
+
return null;
|
|
186
|
+
return (react_1.default.createElement("g", { style: snippetGateStyle },
|
|
187
|
+
react_1.default.createElement("rect", { x: CITY_OFFSET_X + activeCell.col * CELL_W + 4, y: CITY_OFFSET_Y + activeCell.row * CELL_H + 4, width: CELL_W - 8, height: CELL_H - 8, fill: "none", stroke: accent, strokeWidth: 2, rx: 2, style: {
|
|
188
|
+
opacity: highlightTrail ? 0.2 : 1,
|
|
189
|
+
transition: 'opacity 300ms ease',
|
|
190
|
+
} })));
|
|
191
|
+
})(),
|
|
192
|
+
react_1.default.createElement("rect", { x: 0, y: VIEW_H - 80, width: VIEW_W, height: 80, fill: `url(#fade-${uid})`, pointerEvents: "none" }),
|
|
193
|
+
!hideTrail && (react_1.default.createElement("g", { style: trailGateStyle },
|
|
194
|
+
react_1.default.createElement("path", { d: trailPath, fill: "none", stroke: accent, strokeWidth: highlightTrail ? 3.5 : 2, strokeLinecap: "round", strokeDasharray: "6 5", opacity: highlightTrail ? 1 : 0.9, style: {
|
|
195
|
+
transition: 'stroke-width 300ms ease, opacity 300ms ease',
|
|
196
|
+
filter: highlightTrail
|
|
197
|
+
? `drop-shadow(0 0 8px ${withAlpha(accent, 0.7)})`
|
|
198
|
+
: undefined,
|
|
199
|
+
} }, highlightTrail && (react_1.default.createElement("animate", { attributeName: "stroke-dashoffset", from: 0, to: -44, dur: "1.4s", repeatCount: "indefinite" }))))),
|
|
200
|
+
!hideSnippet && (react_1.default.createElement("g", { style: snippetGateStyle },
|
|
201
|
+
react_1.default.createElement("path", { d: leaderPath, fill: "none", stroke: accent, strokeWidth: 1.5, strokeDasharray: "5 4", opacity: highlightTrail ? 0 : 0.7, style: { transition: 'opacity 300ms ease' } }),
|
|
202
|
+
activeMarker && (react_1.default.createElement("circle", { cx: activeMarker.x, cy: activeMarker.y, r: 3.5, fill: accent, opacity: highlightTrail ? 0 : 1, style: { transition: 'opacity 300ms ease' } })))),
|
|
203
|
+
!hideTrail && (react_1.default.createElement("g", { style: trailGateStyle }, markers.map((m, i) => {
|
|
204
|
+
const isActive = i === markers.length - 1;
|
|
205
|
+
const size = 22;
|
|
206
|
+
const half = size / 2;
|
|
207
|
+
return (react_1.default.createElement("g", { key: m.label },
|
|
208
|
+
isActive && (react_1.default.createElement("rect", { x: m.x - half - 4, y: m.y - half - 4, width: size + 8, height: size + 8, rx: 6, fill: accent, opacity: 0.18 },
|
|
209
|
+
react_1.default.createElement("animate", { attributeName: "opacity", values: "0.28;0;0.28", dur: "2.4s", repeatCount: "indefinite" }))),
|
|
210
|
+
react_1.default.createElement("rect", { x: m.x - half, y: m.y - half, width: size, height: size, rx: 4, fill: surface, stroke: accent, strokeWidth: 1.75 }),
|
|
211
|
+
react_1.default.createElement("text", { x: m.x, y: m.y + 1, textAnchor: "middle", dominantBaseline: "central", fontSize: 14, fontWeight: 700, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fill: text }, m.label)));
|
|
212
|
+
}))),
|
|
213
|
+
!hideSnippet && (react_1.default.createElement("g", { style: snippetGateStyle },
|
|
214
|
+
react_1.default.createElement("g", { style: {
|
|
215
|
+
opacity: highlightTrail ? 0.15 : 1,
|
|
216
|
+
transition: 'opacity 300ms ease',
|
|
217
|
+
} },
|
|
218
|
+
react_1.default.createElement("circle", { cx: snippetAnchor.x, cy: snippetAnchor.y, r: 3.5, fill: accent }),
|
|
219
|
+
react_1.default.createElement(SnippetCard, { x: snippetAnchor.x, y: snippetAnchor.y - 22, surface: surface, text: text, muted: muted, accent: accent })))),
|
|
220
|
+
react_1.default.createElement("g", { style: stampRowGateStyle }, [
|
|
221
|
+
{ initials: 'AJ', kind: 'LGTM', rotation: -6 },
|
|
222
|
+
{ initials: 'MK', kind: 'ACK', rotation: 4 },
|
|
223
|
+
{ initials: 'RT', kind: 'LGTM', rotation: -3 },
|
|
224
|
+
{ initials: null, kind: null, rotation: 0 },
|
|
225
|
+
].map((s, i) => {
|
|
226
|
+
const isEmpty = s.initials === null;
|
|
227
|
+
if (isEmpty && userStamped)
|
|
228
|
+
return null;
|
|
229
|
+
const W = 54;
|
|
230
|
+
const H = 36;
|
|
231
|
+
const gap = 6;
|
|
232
|
+
const rowX = snippetAnchor.x;
|
|
233
|
+
const rowY = snippetAnchor.y + 200;
|
|
234
|
+
const cx = rowX + i * (W + gap) + W / 2;
|
|
235
|
+
const cy = rowY + H / 2;
|
|
236
|
+
const ink = isEmpty ? muted : s.kind === 'LGTM' ? success : silver;
|
|
237
|
+
return (react_1.default.createElement("g", { key: i, transform: `translate(${cx}, ${cy}) rotate(${s.rotation})`, opacity: isEmpty ? 0.55 : 0.85 },
|
|
238
|
+
react_1.default.createElement("rect", { x: -W / 2, y: -H / 2, width: W, height: H, rx: 4, fill: isEmpty ? 'none' : withAlpha(ink, 0.08), stroke: ink, strokeWidth: 1.5, strokeDasharray: isEmpty ? '3 3' : undefined }),
|
|
239
|
+
!isEmpty && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
240
|
+
react_1.default.createElement("rect", { x: -W / 2 + 3, y: -H / 2 + 3, width: W - 6, height: H - 6, rx: 2, fill: "none", stroke: ink, strokeWidth: 0.75, opacity: 0.7 }),
|
|
241
|
+
react_1.default.createElement("text", { x: 0, y: -5, textAnchor: "middle", dominantBaseline: "central", fontSize: 11, fontWeight: 700, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fill: ink, letterSpacing: "0.1em" }, s.initials),
|
|
242
|
+
react_1.default.createElement("text", { x: 0, y: 7, textAnchor: "middle", dominantBaseline: "central", fontSize: 7, fontWeight: 700, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fill: ink, letterSpacing: "0.14em" }, s.kind))),
|
|
243
|
+
isEmpty && (react_1.default.createElement("text", { x: 0, y: 1, textAnchor: "middle", dominantBaseline: "central", fontSize: 18, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fill: ink, opacity: 0.7 }, "+"))));
|
|
244
|
+
}))));
|
|
245
|
+
}
|
|
246
|
+
function SnippetCard({ x, y, surface, text, muted, accent, }) {
|
|
247
|
+
const W = 280;
|
|
248
|
+
const H = 200;
|
|
249
|
+
const HEADER = 28;
|
|
250
|
+
const lines = [
|
|
251
|
+
{ w: 170, c: muted },
|
|
252
|
+
{ w: 220, c: text },
|
|
253
|
+
{ w: 130, c: accent },
|
|
254
|
+
{ w: 200, c: text },
|
|
255
|
+
{ w: 150, c: muted },
|
|
256
|
+
{ w: 100, c: text },
|
|
257
|
+
];
|
|
258
|
+
return (react_1.default.createElement("g", { transform: `translate(${x}, ${y})` },
|
|
259
|
+
react_1.default.createElement("rect", { width: W, height: H, rx: 10, fill: surface, stroke: withAlpha(accent, 0.55), strokeWidth: 1.5 }),
|
|
260
|
+
react_1.default.createElement("rect", { width: W, height: HEADER, rx: 10, fill: withAlpha(accent, 0.08) }),
|
|
261
|
+
react_1.default.createElement("text", { x: 16, y: HEADER / 2, dominantBaseline: "central", fontSize: 13, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fill: text }, "db/users.ts:42"),
|
|
262
|
+
react_1.default.createElement("g", { transform: `translate(16, ${HEADER + 18})` }, lines.map((l, i) => (react_1.default.createElement("rect", { key: i, x: 0, y: i * 20, width: l.w, height: 7, rx: 2, fill: l.c, opacity: 0.75 }))))));
|
|
263
|
+
}
|
|
264
|
+
function withAlpha(color, alpha) {
|
|
265
|
+
// Accept #rgb / #rrggbb / rgb()/rgba()/hsl()/named — leave non-hex
|
|
266
|
+
// as-is and rely on the caller. For hex, append a 2-digit alpha.
|
|
267
|
+
if (/^#([0-9a-f]{3}){1,2}$/i.test(color)) {
|
|
268
|
+
let hex = color.slice(1);
|
|
269
|
+
if (hex.length === 3)
|
|
270
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
271
|
+
const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255)
|
|
272
|
+
.toString(16)
|
|
273
|
+
.padStart(2, '0');
|
|
274
|
+
return `#${hex}${a}`;
|
|
275
|
+
}
|
|
276
|
+
return color;
|
|
277
|
+
}
|
|
@@ -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
|
+
};
|