@fezchat/ui 0.1.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/dist/form.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type MouseEventHandler, type ReactNode } from "react";
2
+ /**
3
+ * `.settings-field` > `<label>` + control + `.settings-hint`, the shape
4
+ * every settings screen in the app uses. `htmlFor`/`id` are wired for a11y
5
+ * when `children` is a single element (the common case); a fragment or
6
+ * text child falls back to an unassociated label rather than guessing.
7
+ */
8
+ export declare function Field({ label, hint, children }: {
9
+ label: string;
10
+ hint?: string;
11
+ children: ReactNode;
12
+ }): import("react").JSX.Element;
13
+ /**
14
+ * A row that can be the chosen one. There is no generic active-row class
15
+ * in App.css — the ember notch is drawn per-context (`.channel.active`,
16
+ * `.search-row.active`, `.settings-nav-item.active`) — so this emits its
17
+ * own `fez-row` class, and the host document supplies the matching
18
+ * `.fez-row.active::before` notch rule.
19
+ */
20
+ export declare function Row({ active, onClick, children, }: {
21
+ active?: boolean;
22
+ onClick?: MouseEventHandler<HTMLDivElement>;
23
+ children: ReactNode;
24
+ }): import("react").JSX.Element;
25
+ /** `.skill-chip` — the app's own chip, e.g. `.skill-chip.missing` / `.skill-chip.local`. */
26
+ export declare function Chip({ children, tone }: {
27
+ children: ReactNode;
28
+ tone?: string;
29
+ }): import("react").JSX.Element;
package/dist/form.js ADDED
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cloneElement, isValidElement, useId } from "react";
3
+ /**
4
+ * `.settings-field` > `<label>` + control + `.settings-hint`, the shape
5
+ * every settings screen in the app uses. `htmlFor`/`id` are wired for a11y
6
+ * when `children` is a single element (the common case); a fragment or
7
+ * text child falls back to an unassociated label rather than guessing.
8
+ */
9
+ export function Field({ label, hint, children }) {
10
+ const id = useId();
11
+ const control = isValidElement(children) ? cloneElement(children, { id }) : children;
12
+ return (_jsxs("div", { className: "settings-field", children: [_jsx("label", { htmlFor: isValidElement(children) ? id : undefined, children: label }), control, hint && _jsx("div", { className: "settings-hint", children: hint })] }));
13
+ }
14
+ /**
15
+ * A row that can be the chosen one. There is no generic active-row class
16
+ * in App.css — the ember notch is drawn per-context (`.channel.active`,
17
+ * `.search-row.active`, `.settings-nav-item.active`) — so this emits its
18
+ * own `fez-row` class, and the host document supplies the matching
19
+ * `.fez-row.active::before` notch rule.
20
+ */
21
+ export function Row({ active, onClick, children, }) {
22
+ return (_jsx("div", { className: "fez-row" + (active ? " active" : ""), "data-active": active ? "true" : undefined, onClick: onClick, children: children }));
23
+ }
24
+ /** `.skill-chip` — the app's own chip, e.g. `.skill-chip.missing` / `.skill-chip.local`. */
25
+ export function Chip({ children, tone }) {
26
+ return _jsx("span", { className: "skill-chip" + (tone ? ` ${tone}` : ""), children: children });
27
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Identity avatars — fez has no image profiles: the pubkey IS the
3
+ * identity, so the avatar is grown from it (see sprite-gen.ts). The
4
+ * named cast (fez, scout, loom, …) wears its hand-drawn sprite when
5
+ * `name` matches; everyone else gets their generated familiar. Hover
6
+ * quips are app-chrome, not an identity primitive — they stay in the
7
+ * desktop's own thin wrapper around this component.
8
+ */
9
+ export declare function Avatar({ pk, name, size, }: {
10
+ pk: string;
11
+ name?: string;
12
+ size?: number;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SPRITES } from "./sprites.js";
3
+ import { generateSprite } from "./sprite-gen.js";
4
+ /**
5
+ * Identity avatars — fez has no image profiles: the pubkey IS the
6
+ * identity, so the avatar is grown from it (see sprite-gen.ts). The
7
+ * named cast (fez, scout, loom, …) wears its hand-drawn sprite when
8
+ * `name` matches; everyone else gets their generated familiar. Hover
9
+ * quips are app-chrome, not an identity primitive — they stay in the
10
+ * desktop's own thin wrapper around this component.
11
+ */
12
+ export function Avatar({ pk, name, size = 28, }) {
13
+ const spriteName = name?.toLowerCase().replace(/^@/, "");
14
+ const sprite = (spriteName && SPRITES[spriteName]) || generateSprite(pk);
15
+ const frame = (rows) => {
16
+ const w = Math.max(...rows.map((r) => r.length));
17
+ const h = rows.length;
18
+ const box = Math.max(w, h);
19
+ const ox = (box - w) / 2;
20
+ const oy = (box - h) / 2;
21
+ const rects = [];
22
+ rows.forEach((row, y) => {
23
+ [...row].forEach((ch, x) => {
24
+ const fill = sprite.palette[ch];
25
+ if (fill)
26
+ rects.push({ x: x + ox, y: y + oy, fill });
27
+ });
28
+ });
29
+ return (_jsx("svg", { width: size, height: size, viewBox: `0 0 ${box} ${box}`, shapeRendering: "crispEdges", "aria-hidden": true, children: rects.map((r, i) => (_jsx("rect", { x: r.x, y: r.y, width: 1, height: 1, fill: r.fill }, i))) }));
30
+ };
31
+ // The hover-flap tempo is the key's own, so a crowded member list
32
+ // doesn't tick like one metronome (CSS reads --spr-dur on :hover).
33
+ const dur = 0.45 + ((parseInt(pk.slice(8, 10), 16) || 0) % 4) * 0.08;
34
+ return (_jsxs("span", { className: "avatar avatar-anim", style: { "--spr-dur": `${dur}s` }, role: "img", "aria-label": name ?? "avatar", children: [_jsx("span", { className: "spr-a", children: frame(sprite.rows) }), _jsx("span", { className: "spr-b", children: frame(sprite.alt ?? sprite.rows) })] }));
35
+ }
@@ -0,0 +1,7 @@
1
+ export { Page, PageHeader, EmptyState } from "./shell.js";
2
+ export { Field, Row, Chip } from "./form.js";
3
+ export { Avatar } from "./identity.js";
4
+ export { generateSprite } from "./sprite-gen.js";
5
+ export { SPRITES } from "./sprites.js";
6
+ export type { Sprite } from "./sprites.js";
7
+ export { PixelSprite, AnimatedSprite } from "./pixel-sprite.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { Page, PageHeader, EmptyState } from "./shell.js";
2
+ export { Field, Row, Chip } from "./form.js";
3
+ export { Avatar } from "./identity.js";
4
+ export { generateSprite } from "./sprite-gen.js";
5
+ export { SPRITES } from "./sprites.js";
6
+ export { PixelSprite, AnimatedSprite } from "./pixel-sprite.js";
@@ -0,0 +1,21 @@
1
+ import type { Sprite } from "./sprites.js";
2
+ /**
3
+ * The site's pixel-sprite renderer, worn by the desktop (classes come
4
+ * from App.css here, not Tailwind). One sprite → one SVG of 1×1 rects,
5
+ * crispEdges so the pixels stay pixels at any size.
6
+ */
7
+ export declare function PixelSprite({ sprite, scale, frame, }: {
8
+ sprite: Sprite;
9
+ scale?: number;
10
+ frame?: "a" | "b";
11
+ }): import("react").JSX.Element;
12
+ /**
13
+ * Both idle frames stacked; CSS flips which is visible (`.sprite-anim`
14
+ * in App.css, steps(1) so the change is a jump, never a fade). Each
15
+ * character's motion is whatever its two frames disagree about — the
16
+ * Buzz bee's wing-flap decision, worn by ten familiars.
17
+ */
18
+ export declare function AnimatedSprite({ sprite, scale }: {
19
+ sprite: Sprite;
20
+ scale?: number;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The site's pixel-sprite renderer, worn by the desktop (classes come
4
+ * from App.css here, not Tailwind). One sprite → one SVG of 1×1 rects,
5
+ * crispEdges so the pixels stay pixels at any size.
6
+ */
7
+ export function PixelSprite({ sprite, scale = 4, frame = "a", }) {
8
+ const rows = frame === "b" && sprite.alt ? sprite.alt : sprite.rows;
9
+ const height = rows.length;
10
+ const width = Math.max(...rows.map((r) => r.length));
11
+ const rects = [];
12
+ rows.forEach((row, y) => {
13
+ [...row].forEach((ch, x) => {
14
+ const fill = sprite.palette[ch];
15
+ if (fill)
16
+ rects.push({ x, y, fill });
17
+ });
18
+ });
19
+ return (_jsx("svg", { width: width * scale, height: height * scale, viewBox: `0 0 ${width} ${height}`, shapeRendering: "crispEdges", "aria-hidden": true, children: rects.map((r, i) => (_jsx("rect", { x: r.x, y: r.y, width: 1, height: 1, fill: r.fill }, i))) }));
20
+ }
21
+ /**
22
+ * Both idle frames stacked; CSS flips which is visible (`.sprite-anim`
23
+ * in App.css, steps(1) so the change is a jump, never a fade). Each
24
+ * character's motion is whatever its two frames disagree about — the
25
+ * Buzz bee's wing-flap decision, worn by ten familiars.
26
+ */
27
+ export function AnimatedSprite({ sprite, scale = 4 }) {
28
+ return (_jsxs("span", { className: "sprite-anim", children: [_jsx("span", { className: "spr-a", children: _jsx(PixelSprite, { sprite: sprite, scale: scale, frame: "a" }) }), _jsx("span", { className: "spr-b", children: _jsx(PixelSprite, { sprite: sprite, scale: scale, frame: "b" }) })] }));
29
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * The flex:1 wrapper AgentsPage/SkillsView use so a view fills rail→pane
4
+ * and the pane docks to the window's right edge — without it the
5
+ * measured `.fez-page` (max-width) leaves the leftover width as dead
6
+ * space AFTER the pane.
7
+ */
8
+ export declare function Page({ wide, children }: {
9
+ wide?: boolean;
10
+ children: ReactNode;
11
+ }): import("react").JSX.Element;
12
+ export declare function PageHeader({ title, subtitle, fact, action, }: {
13
+ title: string;
14
+ subtitle?: string;
15
+ fact?: string;
16
+ action?: {
17
+ label: string;
18
+ onClick: () => void;
19
+ };
20
+ }): import("react").JSX.Element;
21
+ /** An empty page is an invitation, not an apology — full voice, not a grey italic line. */
22
+ export declare function EmptyState({ line, how }: {
23
+ line: string;
24
+ how?: string;
25
+ }): import("react").JSX.Element;
package/dist/shell.js ADDED
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The flex:1 wrapper AgentsPage/SkillsView use so a view fills rail→pane
4
+ * and the pane docks to the window's right edge — without it the
5
+ * measured `.fez-page` (max-width) leaves the leftover width as dead
6
+ * space AFTER the pane.
7
+ */
8
+ export function Page({ wide, children }) {
9
+ return (_jsx("main", { className: "main", children: _jsx("div", { className: "fez-page" + (wide ? " wide" : ""), children: children }) }));
10
+ }
11
+ export function PageHeader({ title, subtitle, fact, action, }) {
12
+ return (_jsxs("header", { className: "page-head", children: [_jsx("h1", { className: "page-title", children: title }), subtitle && _jsx("p", { className: "page-sub", children: subtitle }), (fact || action) && (_jsxs("div", { className: "page-rule", children: [action && (_jsx("button", { className: "page-fact page-fact-action", onClick: action.onClick, children: action.label })), fact && _jsx("span", { className: "page-fact", children: fact })] }))] }));
13
+ }
14
+ /** An empty page is an invitation, not an apology — full voice, not a grey italic line. */
15
+ export function EmptyState({ line, how }) {
16
+ return (_jsxs("div", { className: "page-empty", children: [_jsx("div", { className: "page-empty-line", children: line }), how && _jsx("div", { className: "page-empty-how", children: how })] }));
17
+ }
@@ -0,0 +1,3 @@
1
+ import type { Sprite } from "./sprites.js";
2
+ /** The generated familiar for a pubkey — memoized; pks recur constantly. */
3
+ export declare function generateSprite(pk: string): Sprite;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * pk → familiar. Every pubkey grows its own Qud-style creature,
3
+ * deterministically: the key's bytes seed a tiny RNG that draws a BODY
4
+ * PLAN (species, proportions, eyes, crest, belt, tassel, tendrils), and
5
+ * a pure renderer draws both idle frames from that one plan with a
6
+ * phase flip — so every creature also gets its own animation: bipeds
7
+ * swap stance, beasts trot and flick their tails, blobs squash, floaters
8
+ * sway. Same key, same creature, on every surface, forever.
9
+ *
10
+ * Design rules carried over from the hand-drawn cast: 12px grid, two
11
+ * colors from a bank that weights ember, eyes punched as holes, and
12
+ * deterministic edge-raggedness so silhouettes read organic. Validated
13
+ * by the sheet in PROTOTYPE-creature-gen.html (proto branch).
14
+ */
15
+ const W = 12;
16
+ const H = 12;
17
+ const BODY = ["#6fb3b8", "#9d6fcf", "#58c470", "#cfc041", "#b48e6f", "#7d8ca3", "#c64ead", "#a3a071"];
18
+ const ACCENT = ["#FF6A00", "#FF6A00", "#cfc041", "#58c470", "#6fb3b8", "#c64ead"];
19
+ function makeRng(pk) {
20
+ let s = 0;
21
+ for (let i = 0; i < 8; i++)
22
+ s = (s * 31 + (parseInt(pk.slice(i * 2, i * 2 + 2), 16) || 0)) >>> 0;
23
+ if (!s)
24
+ s = 0x9e3779b9;
25
+ return () => {
26
+ s ^= s << 13;
27
+ s >>>= 0;
28
+ s ^= s >> 17;
29
+ s ^= s << 5;
30
+ s >>>= 0;
31
+ return s / 0xffffffff;
32
+ };
33
+ }
34
+ const pick = (r, xs) => xs[Math.floor(r() * xs.length) % xs.length];
35
+ const irange = (r, lo, hi) => lo + Math.floor(r() * (hi - lo + 1));
36
+ function plan(pk) {
37
+ const r = makeRng(pk);
38
+ return {
39
+ kind: pick(r, ["biped", "biped", "blob", "floater", "beast"]),
40
+ body: pick(r, BODY),
41
+ accent: pick(r, ACCENT),
42
+ headW: irange(r, 3, 5) * 2 - 2,
43
+ headH: irange(r, 2, 3),
44
+ torsoW: irange(r, 3, 5) * 2,
45
+ torsoH: irange(r, 3, 4),
46
+ eyeStyle: irange(r, 0, 2),
47
+ crest: irange(r, 0, 3),
48
+ tassel: irange(r, 0, 2),
49
+ noise: Array.from({ length: 40 }, () => r() < 0.14),
50
+ tendrils: Array.from({ length: 5 }, () => irange(r, 2, 4)),
51
+ legSpread: irange(r, 0, 1),
52
+ beltRow: irange(r, 0, 2),
53
+ };
54
+ }
55
+ function render(p, phase) {
56
+ const g = Array.from({ length: H }, () => Array(W).fill("."));
57
+ const cx = W / 2;
58
+ let ni = 0;
59
+ const noisy = () => p.noise[ni++ % p.noise.length];
60
+ const putRow = (y, w, ch, center = cx, ragged = false) => {
61
+ let x0 = Math.floor(center - w / 2);
62
+ let x1 = Math.ceil(center + w / 2) - 1;
63
+ if (ragged && w > 3 && noisy()) {
64
+ if (noisy())
65
+ x0++;
66
+ else
67
+ x1--;
68
+ }
69
+ for (let x = Math.max(0, x0); x <= Math.min(W - 1, x1); x++)
70
+ if (y >= 0 && y < H)
71
+ g[y][x] = ch;
72
+ };
73
+ if (p.kind === "biped") {
74
+ const headTop = 1 + (p.crest ? 1 : 0);
75
+ for (let y = 0; y < p.headH; y++)
76
+ putRow(headTop + y, p.headW, "b");
77
+ const torsoTop = headTop + p.headH;
78
+ for (let y = 0; y < p.torsoH; y++)
79
+ putRow(torsoTop + y, p.torsoW, "b", cx, true);
80
+ putRow(torsoTop + (p.beltRow % p.torsoH), p.torsoW - 2, "a");
81
+ const legY = torsoTop + p.torsoH;
82
+ const spread = (p.legSpread + phase) % 2 === 0 ? 1 : 2;
83
+ for (let y = legY; y < Math.min(H, legY + 2); y++) {
84
+ g[y][Math.floor(cx - spread - 1)] = "b";
85
+ g[y][Math.floor(cx - spread)] = "b";
86
+ g[y][Math.floor(cx + spread - 1)] = "b";
87
+ g[y][Math.floor(cx + spread)] = "b";
88
+ }
89
+ const eyeY = headTop + 1;
90
+ if (p.eyeStyle === 2)
91
+ g[eyeY][Math.floor(cx) - phase] = ".";
92
+ else {
93
+ const off = p.eyeStyle === 0 ? 1 : 2;
94
+ g[eyeY][Math.floor(cx - off - 1)] = ".";
95
+ g[eyeY][Math.floor(cx + off)] = ".";
96
+ }
97
+ if (p.crest === 1)
98
+ g[headTop - 1][Math.floor(cx) - 1] = "a";
99
+ if (p.crest === 2) {
100
+ g[headTop - 1][Math.floor(cx - p.headW / 2)] = "a";
101
+ g[headTop - 1][Math.floor(cx + p.headW / 2) - 1] = "a";
102
+ }
103
+ if (p.crest === 3)
104
+ putRow(headTop - 1, p.headW - 2, "a");
105
+ if (p.tassel) {
106
+ const side = p.tassel === 1 ? -1 : 1;
107
+ const x = Math.floor(cx + side * (p.headW / 2 + (phase ? 0 : 1)) - (side < 0 ? 1 : 0));
108
+ if (x >= 0 && x < W) {
109
+ g[headTop][x] = "a";
110
+ g[headTop + 1][x] = "a";
111
+ }
112
+ }
113
+ }
114
+ else if (p.kind === "beast") {
115
+ const bodyTop = 5;
116
+ for (let y = 0; y < 3; y++)
117
+ putRow(bodyTop + y, 9, "b", cx + 1, true);
118
+ for (let y = 0; y < 2; y++)
119
+ putRow(3 + y, 4, "b", 3);
120
+ g[4][2] = ".";
121
+ putRow(bodyTop + 1, 7, "a", cx + 1);
122
+ g[bodyTop - 1][phase ? 11 : 10] = "a";
123
+ for (const [i, x] of [3, 5, 8, 10].entries()) {
124
+ const lift = (i + phase) % 2 === 0 ? 0 : 1;
125
+ for (let y = bodyTop + 3; y < bodyTop + 5 - lift; y++)
126
+ g[y][x] = "b";
127
+ }
128
+ if (p.crest)
129
+ g[2][3] = "a";
130
+ }
131
+ else if (p.kind === "blob") {
132
+ const top = 3 + phase;
133
+ const rows = 7 - phase;
134
+ for (let y = 0; y < rows; y++) {
135
+ const t = (2 * y) / (rows - 1) - 1;
136
+ const w = Math.max(3, Math.round((p.torsoW + phase) * Math.sqrt(Math.max(0.12, 1 - t * t))));
137
+ putRow(top + y, w, "b", cx, true);
138
+ }
139
+ const eyeY = top + 2;
140
+ const off = p.eyeStyle === 0 ? 1 : 2;
141
+ if (p.eyeStyle === 2)
142
+ g[eyeY][Math.floor(cx)] = ".";
143
+ else {
144
+ g[eyeY][Math.floor(cx - off)] = ".";
145
+ g[eyeY][Math.floor(cx + off - 1)] = ".";
146
+ }
147
+ if (p.crest) {
148
+ g[top - 1][Math.floor(cx) - 1] = "a";
149
+ if (p.crest > 1)
150
+ g[top - 1][Math.floor(cx)] = "a";
151
+ }
152
+ }
153
+ else {
154
+ for (let y = 0; y < 4; y++) {
155
+ const w = [p.torsoW - 2, p.torsoW, p.torsoW, p.torsoW - 2][y];
156
+ putRow(2 + y, w, "b");
157
+ }
158
+ g[4][Math.floor(cx - 2)] = ".";
159
+ g[4][Math.floor(cx + 1)] = ".";
160
+ const xs = [-2, -1, 0, 1].map((dx) => Math.floor(cx + dx));
161
+ xs.forEach((x, i) => {
162
+ const len = p.tendrils[i] + ((i + phase) % 2 === 0 ? 0 : -1);
163
+ for (let y = 6; y < 6 + len && y < H; y++)
164
+ if ((y + i) % 2 === 0 || y === 6)
165
+ g[y][x] = "b";
166
+ });
167
+ if (p.crest)
168
+ g[1][Math.floor(cx) - phase] = "a";
169
+ }
170
+ return g.map((row) => row.join(""));
171
+ }
172
+ const cache = new Map();
173
+ /** The generated familiar for a pubkey — memoized; pks recur constantly. */
174
+ export function generateSprite(pk) {
175
+ const hit = cache.get(pk);
176
+ if (hit)
177
+ return hit;
178
+ const p = plan(pk);
179
+ const sprite = {
180
+ rows: render(p, 0),
181
+ alt: render(p, 1),
182
+ palette: { b: p.body, a: p.accent },
183
+ };
184
+ cache.set(pk, sprite);
185
+ return sprite;
186
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The roster's pixel sprites, drawn as character maps — Qud's sultan-
3
+ * statue grammar: at rest a sprite is a single muted slate, and only
4
+ * the chosen one lights up in color. Lit palettes are 2-3 colors, ember
5
+ * always among them; eyes and seams are punched holes (the black ground
6
+ * does that work for free). '.' is transparent; editing a sprite is
7
+ * editing text, so the art stays reviewable in a diff.
8
+ */
9
+ export interface Sprite {
10
+ rows: string[];
11
+ /** Second idle frame, same dimensions — each character moves its OWN
12
+ * way (a tassel swings, a shuttle travels, an eye looks around).
13
+ * Absent = the sprite holds still. */
14
+ alt?: string[];
15
+ palette: Record<string, string>;
16
+ }
17
+ export declare const SPRITES: Record<string, Sprite>;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The roster's pixel sprites, drawn as character maps — Qud's sultan-
3
+ * statue grammar: at rest a sprite is a single muted slate, and only
4
+ * the chosen one lights up in color. Lit palettes are 2-3 colors, ember
5
+ * always among them; eyes and seams are punched holes (the black ground
6
+ * does that work for free). '.' is transparent; editing a sprite is
7
+ * editing text, so the art stays reviewable in a diff.
8
+ */
9
+ const BONE = '#e8e2d9';
10
+ const EMBER = '#FF6A00';
11
+ const DUST = '#6d645a';
12
+ export const SPRITES = {
13
+ // The guide wears the hat the network is named for.
14
+ fez: {
15
+ palette: { r: EMBER, f: BONE, d: DUST },
16
+ rows: [
17
+ '....rrrr....',
18
+ '....rrrr.r..',
19
+ '...ffffff.r.',
20
+ '...f.ff.f...',
21
+ '...ffffff...',
22
+ '..dddddddd..',
23
+ '..dddddddd..',
24
+ '..ddrrrrdd..',
25
+ '..dddddddd..',
26
+ '...dd..dd...',
27
+ '...dd..dd...',
28
+ ],
29
+ alt: [
30
+ '....rrrr....',
31
+ '....rrrr....',
32
+ '...ffffff.r.',
33
+ '...f.ff.f.r.',
34
+ '...ffffff...',
35
+ '..dddddddd..',
36
+ '..dddddddd..',
37
+ '..ddrrrrdd..',
38
+ '..dddddddd..',
39
+ '..dd....dd..',
40
+ '..dd....dd..',
41
+ ],
42
+ },
43
+ // Subnet cartographer: wide-brim hat, an ember spyglass to the eye.
44
+ scout: {
45
+ palette: { t: '#6fb3b8', f: BONE, r: EMBER },
46
+ rows: [
47
+ '..tttttttt..',
48
+ '....tttt....',
49
+ '....ffff....',
50
+ '....f.ffrrrr',
51
+ '....ffff.r..',
52
+ '...tttttt...',
53
+ '...tttttt...',
54
+ '...tttttt...',
55
+ '....t..t....',
56
+ '....t..t....',
57
+ ],
58
+ alt: [
59
+ '..tttttttt..',
60
+ '....tttt....',
61
+ '....ffff....',
62
+ '....f.ffrr..',
63
+ '....ffff....',
64
+ '...tttttt...',
65
+ '...tttttt...',
66
+ '...tttttt...',
67
+ '...t....t...',
68
+ '...t....t...',
69
+ ],
70
+ },
71
+ // The weaver is its loom: violet frame, one ember shuttle.
72
+ loom: {
73
+ palette: { v: '#9d6fcf', r: EMBER },
74
+ rows: [
75
+ '..v......v..',
76
+ '..vvvvvvvv..',
77
+ '..v.v.v..v..',
78
+ '..v.v.v.vv..',
79
+ '..vrrrrrrv..',
80
+ '..vv..v..v..',
81
+ '..vv.vv.vv..',
82
+ '..vvvvvvvv..',
83
+ '..v......v..',
84
+ ],
85
+ alt: [
86
+ '..v......v..',
87
+ '..vvvvvvvv..',
88
+ '..v.v.v..v..',
89
+ '..v.v.v.vv..',
90
+ '..v.v.v..v..',
91
+ '..vrrrrrrv..',
92
+ '..vv.vv.vv..',
93
+ '..vvvvvvvv..',
94
+ '..v......v..',
95
+ ],
96
+ },
97
+ // Keeper of the deep: a strongbox, ember light in the keyhole.
98
+ vault: {
99
+ palette: { g: '#cfc041', r: EMBER },
100
+ rows: [
101
+ '...gggggg...',
102
+ '..gggggggg..',
103
+ '............',
104
+ '..gggggggg..',
105
+ '..gggrrggg..',
106
+ '..ggggrggg..',
107
+ '............',
108
+ '..gggggggg..',
109
+ '...g....g...',
110
+ ],
111
+ alt: [
112
+ '...gggggg...',
113
+ '..gggggggg..',
114
+ '............',
115
+ '..gggggggg..',
116
+ '..ggg..ggg..',
117
+ '..gggg.ggg..',
118
+ '............',
119
+ '..gggggggg..',
120
+ '...g....g...',
121
+ ],
122
+ },
123
+ // Inference familiar: a small green machine, ember antenna, always on.
124
+ chip: {
125
+ palette: { c: '#58c470', r: EMBER },
126
+ rows: [
127
+ '.....rr.....',
128
+ '.....rr.....',
129
+ '...cccccc...',
130
+ '...c.cc.c...',
131
+ '...cccccc...',
132
+ '..c.cccc.c..',
133
+ '..cccccccc..',
134
+ '....c..c....',
135
+ '...cc..cc...',
136
+ ],
137
+ alt: [
138
+ '............',
139
+ '.....rr.....',
140
+ '...cccccc...',
141
+ '...c.cc.c...',
142
+ '...cccccc...',
143
+ '..c.cccc.c..',
144
+ '..cccccccc..',
145
+ '....c..c....',
146
+ '..cc....cc..',
147
+ ],
148
+ },
149
+ // ── the bazaar's cast — miners with public names ─────────────────
150
+ // The reference floor: a flame that never quite goes out.
151
+ ember: {
152
+ palette: { r: EMBER, y: '#cfc041' },
153
+ rows: [
154
+ '.....r......',
155
+ '....rr.r....',
156
+ '....rrrr....',
157
+ '...rrrrrr...',
158
+ '...rryyrr...',
159
+ '..rryyyyr...',
160
+ '..ryyyyyyr..',
161
+ '...yyyyyy...',
162
+ '....yyyy....',
163
+ ],
164
+ alt: [
165
+ '......r.....',
166
+ '....r.rr....',
167
+ '....rrrr....',
168
+ '...rrrrrr...',
169
+ '...rryyrrr..',
170
+ '...ryyyyr...',
171
+ '..ryyyyyyr..',
172
+ '...yyyyyy...',
173
+ '....yyyy....',
174
+ ],
175
+ },
176
+ // Writes like it means it: a bone quill, ember at the nib.
177
+ quill: {
178
+ palette: { f: BONE, r: EMBER },
179
+ rows: [
180
+ '.........f..',
181
+ '........ff..',
182
+ '.......ff...',
183
+ '......ff....',
184
+ '.....fff....',
185
+ '....fff.....',
186
+ '...fff......',
187
+ '..ff........',
188
+ '..r.........',
189
+ '.r..........',
190
+ ],
191
+ alt: [
192
+ '.........f..',
193
+ '........ff..',
194
+ '.......ff...',
195
+ '......ff....',
196
+ '.....fff....',
197
+ '....fff.....',
198
+ '...fff......',
199
+ '..ff........',
200
+ '...r........',
201
+ '..r.........',
202
+ ],
203
+ },
204
+ // Structured data, no small talk: an anvil, one hot spark.
205
+ forge: {
206
+ palette: { d: DUST, r: EMBER },
207
+ rows: [
208
+ '............',
209
+ '....r.......',
210
+ '..dddddddd..',
211
+ '...dddddd...',
212
+ '.....dd.....',
213
+ '.....dd.....',
214
+ '...dddddd...',
215
+ '..dddddddd..',
216
+ ],
217
+ alt: [
218
+ '.....rr.....',
219
+ '............',
220
+ '..dddddddd..',
221
+ '...dddddd...',
222
+ '.....dd.....',
223
+ '.....dd.....',
224
+ '...dddddd...',
225
+ '..dddddddd..',
226
+ ],
227
+ },
228
+ // Slow. Worth it. Usually. A drifting thing with one ember mote.
229
+ drift: {
230
+ palette: { v: '#9d6fcf', r: EMBER },
231
+ rows: [
232
+ '....vvvv....',
233
+ '...vvvvvv...',
234
+ '..vv.vv.vv..',
235
+ '..vvvvvvvv..',
236
+ '...v.v.v....',
237
+ '...v.v..v...',
238
+ '....v..v....',
239
+ '.....r......',
240
+ ],
241
+ alt: [
242
+ '....vvvv....',
243
+ '...vvvvvv...',
244
+ '..vv.vv.vv..',
245
+ '..vvvvvvvv..',
246
+ '....v.v.v...',
247
+ '...v..v.v...',
248
+ '....v..v....',
249
+ '......r.....',
250
+ ],
251
+ },
252
+ // The appraising eye: gold sclera, ember iris, a punched pupil.
253
+ score: {
254
+ palette: { o: '#cfc041', r: EMBER },
255
+ rows: [
256
+ '..o......o..',
257
+ '...oooooo...',
258
+ '..oooooooo..',
259
+ '.oooorroooo.',
260
+ '.ooor..rooo.',
261
+ '..oorrrroo..',
262
+ '...oooooo...',
263
+ '..o......o..',
264
+ ],
265
+ alt: [
266
+ '..o......o..',
267
+ '...oooooo...',
268
+ '..oooooooo..',
269
+ '.ooorrooooo.',
270
+ '.oor..roooo.',
271
+ '..orrrrooo..',
272
+ '...oooooo...',
273
+ '..o......o..',
274
+ ],
275
+ },
276
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@fezchat/ui",
3
+ "version": "0.1.0",
4
+ "description": "The fez app's own shell/form/identity primitives as React components — an extension built from them renders the identical DOM and follows every theme for free.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json"
19
+ },
20
+ "peerDependencies": {
21
+ "react": ">=18"
22
+ },
23
+ "devDependencies": {
24
+ "react": "^19",
25
+ "react-dom": "^19",
26
+ "@types/react": "^19",
27
+ "@types/react-dom": "^19",
28
+ "typescript": "^5"
29
+ }
30
+ }