@aurea-uds/react 0.31.0 → 0.33.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.
@@ -0,0 +1,92 @@
1
+ import type { AureaStrings } from "./pure.js";
2
+ /** Os três canais de 0 a 255. */
3
+ export interface Rgb {
4
+ r: number;
5
+ g: number;
6
+ b: number;
7
+ }
8
+ /** Matiz de 0 a 360; saturação e brilho de 0 a 1 (o HSV). */
9
+ export interface Hsb {
10
+ h: number;
11
+ s: number;
12
+ b: number;
13
+ }
14
+ /** Matiz de 0 a 360; saturação e luminosidade de 0 a 1. */
15
+ export interface Hsl {
16
+ h: number;
17
+ s: number;
18
+ l: number;
19
+ }
20
+ /** A cor de dentro das peças: HSB com o alfa (0 = transparente, 1 = opaca). */
21
+ export interface Cor extends Hsb {
22
+ a: number;
23
+ }
24
+ export type ColorSpace = "rgb" | "hsl" | "hsb";
25
+ export type ColorChannel = "hue" | "saturation" | "brightness" | "lightness" | "red" | "green" | "blue" | "alpha";
26
+ /** Os três canais de cada espaço, na ordem dele. */
27
+ export declare const COLOR_CHANNELS: Readonly<Record<ColorSpace, readonly [ColorChannel, ColorChannel, ColorChannel]>>;
28
+ /** `#rgb`, `#rgba`, `#rrggbb` ou `#rrggbbaa` (com ou sem `#`) → os canais e o alfa; o resto → `null`. */
29
+ export declare function parseHex(texto: string): (Rgb & {
30
+ a: number;
31
+ }) | null;
32
+ /** Os canais → `#rrggbb` (minúscula, como o `<input type="color">`); com alfa abaixo de 1, `#rrggbbaa`. */
33
+ export declare function toHex({ r, g, b, a }: Rgb & {
34
+ a?: number;
35
+ }): string;
36
+ /** Normaliza o texto hex; texto inválido → `null`. */
37
+ export declare const normalizeHex: (texto: string) => string | null;
38
+ /** Texto de cor → a cor de dentro: hex (3, 4, 6 ou 8 dígitos), `rgb()`, `hsl()` ou `hsb()`, com alfa. O resto →
39
+ * `null`. O matiz de `hsl()`/`hsb()` cinza é guardado (o hex o perderia). */
40
+ export declare function parseColor(texto: string): Cor | null;
41
+ /** A cor de dentro → hex (`#rrggbb`, ou `#rrggbbaa` com transparência). */
42
+ export declare const corToHex: (c: Cor) => string;
43
+ export declare function rgbToHsb({ r, g, b }: Rgb): Hsb;
44
+ export declare function hsbToRgb({ h, s, b }: Hsb): Rgb;
45
+ export declare function rgbToHsl({ r, g, b }: Rgb): Hsl;
46
+ export declare function hslToRgb({ h, s, l }: Hsl): Rgb;
47
+ /** A faixa de cada canal, a mesma da referência: matiz 0–360 (passo 1, salto 15); saturação, brilho e luminosidade
48
+ * 0–100 (passo 1, salto 10); vermelho, verde e azul 0–255 (passo 1, salto 17); alfa 0–1 (passo 0,01, salto 0,1). */
49
+ export declare function channelRange(ch: ColorChannel): {
50
+ min: number;
51
+ max: number;
52
+ step: number;
53
+ page: number;
54
+ };
55
+ /** O espaço em que um canal vive: luminosidade só no HSL, brilho só no HSB, vermelho/verde/azul só no RGB; matiz,
56
+ * saturação e alfa no espaço pedido (sem pedido, HSB). */
57
+ export declare function spaceOf(ch: ColorChannel, pedido?: ColorSpace): ColorSpace;
58
+ /** O valor de um canal, na faixa dele. */
59
+ export declare function getChannel(c: Cor, ch: ColorChannel, space?: ColorSpace): number;
60
+ /** A cor com um canal trocado (o valor é limitado à faixa). O matiz e a saturação de antes ficam quando a cor nova
61
+ * vira cinza ou preta. */
62
+ export declare function setChannel(c: Cor, ch: ColorChannel, v: number, space?: ColorSpace): Cor;
63
+ /** A luminância relativa da WCAG 2 (0 = preto, 1 = branco), a conta do contraste. */
64
+ export declare function relativeLuminance({ r, g, b }: Rgb): number;
65
+ /** A tinta que contrasta MAIS sobre a cor: `"dark"` (preto) ou `"light"` (branco), pela razão de contraste da WCAG
66
+ * — e não por um palpite de "cor clara". O amarelo da Aurea pede preto; um azul médio também. */
67
+ export declare function contrastingInk(rgb: Rgb): "dark" | "light";
68
+ type Direcao = "to right" | "to left" | "to top";
69
+ /** A cor pura de um matiz, em hex — a cor do pino da trilha de matiz. */
70
+ export declare const pureHue: (h: number) => string;
71
+ /** O FUNDO da trilha de um canal, para o `background` do trilho do `Slider`. O gradiente vai de ponta a ponta do
72
+ * trilho, mas as paradas começam e terminam na FOLGA (`--slider-cap`, o recuo que o pino respeita): antes da
73
+ * primeira e depois da última, a cor chapada da ponta. Assim o pino no mínimo fica sobre a cor do mínimo, em
74
+ * qualquer canal. O alfa vai do transparente à cor opaca (o xadrez embaixo é a pele, e não gradiente). */
75
+ export declare function trackBackground(c: Cor, ch: ColorChannel, space?: ColorSpace, direcao?: Direcao): string;
76
+ /** O fundo da área de cor para um par de canais, com o terceiro fixo no valor da cor de agora. A técnica é a da
77
+ * biblioteca da referência (escrita aqui por nós): no RGB, uma camada por canal somadas pelo modo de mistura
78
+ * `screen`; no HSL e no HSB, uma camada por canal com a receita de paradas dele, e a cor pura embaixo quando o
79
+ * fixo é o matiz. `x` anda do começo ao fim da linha (na escrita da direita para a esquerda, o começo é a direita);
80
+ * `y`, de baixo para cima. */
81
+ export declare function areaBackground(c: Cor, space: ColorSpace, x: ColorChannel, y: ColorChannel, rtl?: boolean): {
82
+ background: string;
83
+ blend?: string;
84
+ };
85
+ export type ColorNameStrings = Pick<AureaStrings, "colorHue" | "colorTone" | "colorNameFormat">;
86
+ export type ColorHueName = keyof AureaStrings["colorHue"];
87
+ export type ColorToneName = keyof AureaStrings["colorTone"];
88
+ /** O nome da cor na língua das frases: "vivid blue", "dark red", "rosa pálido". */
89
+ export declare function colorName(rgb: Rgb, s: ColorNameStrings): string;
90
+ /** O valor de um canal escrito para o leitor de tela e a saída: "210°", "80%", "128", alfa "50%". */
91
+ export declare function formatChannel(ch: ColorChannel, v: number): string;
92
+ export {};
package/dist/color.js ADDED
@@ -0,0 +1,293 @@
1
+ /** Os três canais de cada espaço, na ordem dele. */
2
+ export const COLOR_CHANNELS = {
3
+ hsb: ["hue", "saturation", "brightness"], hsl: ["hue", "saturation", "lightness"], rgb: ["red", "green", "blue"],
4
+ };
5
+ const limitar = (n, min, max) => Math.min(max, Math.max(min, n));
6
+ // ── O texto ─────────────────────────────────────────────────────────────────────────────────────────────────
7
+ // Texto de cor é entrada do usuário: o tamanho se corta ANTES da expressão, para nenhum texto enorme custar caro
8
+ // (o defeito de uma biblioteca de cor do mercado, alerta de 08/09/2026, era exatamente esse).
9
+ const HEX = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
10
+ /** `#rgb`, `#rgba`, `#rrggbb` ou `#rrggbbaa` (com ou sem `#`) → os canais e o alfa; o resto → `null`. */
11
+ export function parseHex(texto) {
12
+ if (texto.length > 16)
13
+ return null;
14
+ const m = HEX.exec(texto.trim());
15
+ if (!m)
16
+ return null;
17
+ const h = m[1].length <= 4 ? m[1].replace(/./g, (c) => c + c) : m[1];
18
+ return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16),
19
+ a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1 };
20
+ }
21
+ /** Os canais → `#rrggbb` (minúscula, como o `<input type="color">`); com alfa abaixo de 1, `#rrggbbaa`. */
22
+ export function toHex({ r, g, b, a }) {
23
+ const dois = (n) => Math.round(limitar(n, 0, 255)).toString(16).padStart(2, "0");
24
+ const alfa = a === undefined || a >= 1 ? "" : dois(a * 255);
25
+ return `#${dois(r)}${dois(g)}${dois(b)}${alfa}`;
26
+ }
27
+ /** Normaliza o texto hex; texto inválido → `null`. */
28
+ export const normalizeHex = (texto) => {
29
+ const rgb = parseHex(texto);
30
+ return rgb ? toHex(rgb) : null;
31
+ };
32
+ // As funções do CSS que a referência aceita na entrada: rgb/rgba, hsl/hsla, hsb/hsba — vírgula ou espaço, `%` onde
33
+ // o CSS põe, e o alfa por `,` ou `/`. Números sem expoente e sem sinal: é texto de pessoa, não de máquina.
34
+ const NUM = "(\\d{1,3}(?:\\.\\d+)?)";
35
+ const FUNCAO = new RegExp(`^(rgb|hsl|hsb)a?\\(\\s*${NUM}(deg|%)?\\s*[, ]\\s*${NUM}(%)?\\s*[, ]\\s*${NUM}(%)?\\s*(?:[,/]\\s*(\\d*(?:\\.\\d+)?)(%)?\\s*)?\\)$`, "i");
36
+ /** Texto de cor → a cor de dentro: hex (3, 4, 6 ou 8 dígitos), `rgb()`, `hsl()` ou `hsb()`, com alfa. O resto →
37
+ * `null`. O matiz de `hsl()`/`hsb()` cinza é guardado (o hex o perderia). */
38
+ export function parseColor(texto) {
39
+ if (texto.length > 64)
40
+ return null;
41
+ const t = texto.trim();
42
+ const hex = parseHex(t);
43
+ if (hex)
44
+ return { ...rgbToHsb(hex), a: hex.a };
45
+ const m = FUNCAO.exec(t);
46
+ if (!m)
47
+ return null;
48
+ const [, fn, v1, , v2, , v3, , va, pa] = m;
49
+ const n1 = Number(v1), n2 = Number(v2), n3 = Number(v3);
50
+ const a = va === undefined || va === "" ? 1 : limitar(pa ? Number(va) / 100 : Number(va), 0, 1);
51
+ const tipo = fn.toLowerCase();
52
+ if (tipo === "rgb") {
53
+ if (n1 > 255 || n2 > 255 || n3 > 255)
54
+ return null;
55
+ return { ...rgbToHsb({ r: n1, g: n2, b: n3 }), a };
56
+ }
57
+ if (n1 > 360 || n2 > 100 || n3 > 100)
58
+ return null;
59
+ if (tipo === "hsb")
60
+ return { h: n1 % 360, s: n2 / 100, b: n3 / 100, a };
61
+ return { ...hslParaHsb(n1 % 360, n2 / 100, n3 / 100), a };
62
+ }
63
+ /** A cor de dentro → hex (`#rrggbb`, ou `#rrggbbaa` com transparência). */
64
+ export const corToHex = (c) => toHex({ ...hsbToRgb(c), a: c.a });
65
+ // ── As conversões ───────────────────────────────────────────────────────────────────────────────────────────
66
+ // O matiz pela fórmula de seis setores (a mesma do HSV e do HSL).
67
+ function matiz(r, g, b, max, d) {
68
+ if (d === 0)
69
+ return 0;
70
+ const h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
71
+ return h * 60;
72
+ }
73
+ export function rgbToHsb({ r, g, b }) {
74
+ const [R, G, B] = [r / 255, g / 255, b / 255];
75
+ const max = Math.max(R, G, B), d = max - Math.min(R, G, B);
76
+ return { h: matiz(R, G, B, max, d), s: max === 0 ? 0 : d / max, b: max };
77
+ }
78
+ export function hsbToRgb({ h, s, b }) {
79
+ const f = (n) => {
80
+ const k = (n + h / 60) % 6;
81
+ return 255 * (b - b * s * Math.max(0, Math.min(k, 4 - k, 1)));
82
+ };
83
+ return { r: f(5), g: f(3), b: f(1) };
84
+ }
85
+ export function rgbToHsl({ r, g, b }) {
86
+ const [R, G, B] = [r / 255, g / 255, b / 255];
87
+ const max = Math.max(R, G, B), min = Math.min(R, G, B), d = max - min, l = (max + min) / 2;
88
+ return { h: matiz(R, G, B, max, d), s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
89
+ }
90
+ export function hslToRgb({ h, s, l }) {
91
+ const a = s * Math.min(l, 1 - l);
92
+ const f = (n) => {
93
+ const k = (n + h / 30) % 12;
94
+ return 255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)));
95
+ };
96
+ return { r: f(0), g: f(8), b: f(4) };
97
+ }
98
+ // HSB ↔ HSL direto, com o matiz passando por fora (pelo RGB ele se perderia no cinza).
99
+ function hsbParaHsl(c) {
100
+ const l = c.b * (1 - c.s / 2);
101
+ return { l, s: l === 0 || l === 1 ? 0 : (c.b - l) / Math.min(l, 1 - l) };
102
+ }
103
+ function hslParaHsb(h, s, l, anterior) {
104
+ const b = l + s * Math.min(l, 1 - l);
105
+ return { h, s: b === 0 ? (anterior?.s ?? 0) : 2 * (1 - l / b), b };
106
+ }
107
+ // ── Os canais ───────────────────────────────────────────────────────────────────────────────────────────────
108
+ /** A faixa de cada canal, a mesma da referência: matiz 0–360 (passo 1, salto 15); saturação, brilho e luminosidade
109
+ * 0–100 (passo 1, salto 10); vermelho, verde e azul 0–255 (passo 1, salto 17); alfa 0–1 (passo 0,01, salto 0,1). */
110
+ export function channelRange(ch) {
111
+ if (ch === "hue")
112
+ return { min: 0, max: 360, step: 1, page: 15 };
113
+ if (ch === "red" || ch === "green" || ch === "blue")
114
+ return { min: 0, max: 255, step: 1, page: 17 };
115
+ if (ch === "alpha")
116
+ return { min: 0, max: 1, step: 0.01, page: 0.1 };
117
+ return { min: 0, max: 100, step: 1, page: 10 };
118
+ }
119
+ /** O espaço em que um canal vive: luminosidade só no HSL, brilho só no HSB, vermelho/verde/azul só no RGB; matiz,
120
+ * saturação e alfa no espaço pedido (sem pedido, HSB). */
121
+ export function spaceOf(ch, pedido) {
122
+ if (ch === "lightness")
123
+ return "hsl";
124
+ if (ch === "brightness")
125
+ return "hsb";
126
+ if (ch === "red" || ch === "green" || ch === "blue")
127
+ return "rgb";
128
+ return pedido === "rgb" && ch !== "alpha" ? "hsb" : pedido ?? "hsb";
129
+ }
130
+ /** O valor de um canal, na faixa dele. */
131
+ export function getChannel(c, ch, space = spaceOf(ch)) {
132
+ if (ch === "alpha")
133
+ return c.a;
134
+ if (ch === "hue")
135
+ return c.h;
136
+ if (space === "rgb") {
137
+ const rgb = hsbToRgb(c);
138
+ return ch === "red" ? rgb.r : ch === "green" ? rgb.g : rgb.b;
139
+ }
140
+ if (space === "hsl") {
141
+ const v = hsbParaHsl(c);
142
+ return (ch === "lightness" ? v.l : v.s) * 100;
143
+ }
144
+ return (ch === "brightness" ? c.b : c.s) * 100;
145
+ }
146
+ /** A cor com um canal trocado (o valor é limitado à faixa). O matiz e a saturação de antes ficam quando a cor nova
147
+ * vira cinza ou preta. */
148
+ export function setChannel(c, ch, v, space = spaceOf(ch)) {
149
+ const { min, max } = channelRange(ch);
150
+ const n = limitar(v, min, max);
151
+ if (ch === "alpha")
152
+ return { ...c, a: n };
153
+ if (ch === "hue")
154
+ return { ...c, h: n };
155
+ if (space === "rgb") {
156
+ const rgb = { ...hsbToRgb(c), [ch === "red" ? "r" : ch === "green" ? "g" : "b"]: n };
157
+ const novo = rgbToHsb(rgb);
158
+ return { h: novo.s === 0 || novo.b === 0 ? c.h : novo.h, s: novo.b === 0 ? c.s : novo.s, b: novo.b, a: c.a };
159
+ }
160
+ if (space === "hsl") {
161
+ const atual = hsbParaHsl(c);
162
+ const s = ch === "saturation" ? n / 100 : atual.s, l = ch === "lightness" ? n / 100 : atual.l;
163
+ return { ...hslParaHsb(c.h, s, l, c), a: c.a };
164
+ }
165
+ return { ...c, [ch === "brightness" ? "b" : "s"]: n / 100 };
166
+ }
167
+ // ── O contraste ─────────────────────────────────────────────────────────────────────────────────────────────
168
+ /** A luminância relativa da WCAG 2 (0 = preto, 1 = branco), a conta do contraste. */
169
+ export function relativeLuminance({ r, g, b }) {
170
+ const lin = (c) => { const s = c / 255; return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; };
171
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
172
+ }
173
+ /** A tinta que contrasta MAIS sobre a cor: `"dark"` (preto) ou `"light"` (branco), pela razão de contraste da WCAG
174
+ * — e não por um palpite de "cor clara". O amarelo da Aurea pede preto; um azul médio também. */
175
+ export function contrastingInk(rgb) {
176
+ const L = relativeLuminance(rgb);
177
+ return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? "dark" : "light";
178
+ }
179
+ const opaca = (c) => toHex(hsbToRgb(c));
180
+ // A cor pura de um matiz (saturação e brilho cheios).
181
+ const puro = (h) => opaca({ h, s: 1, b: 1, a: 1 });
182
+ /** A cor pura de um matiz, em hex — a cor do pino da trilha de matiz. */
183
+ export const pureHue = (h) => puro(h);
184
+ /** O FUNDO da trilha de um canal, para o `background` do trilho do `Slider`. O gradiente vai de ponta a ponta do
185
+ * trilho, mas as paradas começam e terminam na FOLGA (`--slider-cap`, o recuo que o pino respeita): antes da
186
+ * primeira e depois da última, a cor chapada da ponta. Assim o pino no mínimo fica sobre a cor do mínimo, em
187
+ * qualquer canal. O alfa vai do transparente à cor opaca (o xadrez embaixo é a pele, e não gradiente). */
188
+ export function trackBackground(c, ch, space = spaceOf(ch), direcao = "to right") {
189
+ const { min, max } = channelRange(ch);
190
+ let cores;
191
+ if (ch === "hue")
192
+ cores = [0, 60, 120, 180, 240, 300, 360].map(puro);
193
+ else if (ch === "alpha")
194
+ cores = [toHex({ ...hsbToRgb(c), a: 0 }), opaca(c)];
195
+ else if (ch === "lightness")
196
+ cores = [min, (max - min) / 2, max].map((v) => opaca(setChannel(c, ch, v, space)));
197
+ else
198
+ cores = [min, max].map((v) => opaca(setChannel(c, ch, v, space)));
199
+ const k = cores.length - 1;
200
+ const paradas = cores.map((cor, i) => `${cor} calc(var(--slider-cap, 0px) + (100% - 2 * var(--slider-cap, 0px)) * ${i / k})`);
201
+ return `linear-gradient(${direcao}, ${paradas.join(", ")})`;
202
+ }
203
+ /** O fundo da área de cor para um par de canais, com o terceiro fixo no valor da cor de agora. A técnica é a da
204
+ * biblioteca da referência (escrita aqui por nós): no RGB, uma camada por canal somadas pelo modo de mistura
205
+ * `screen`; no HSL e no HSB, uma camada por canal com a receita de paradas dele, e a cor pura embaixo quando o
206
+ * fixo é o matiz. `x` anda do começo ao fim da linha (na escrita da direita para a esquerda, o começo é a direita);
207
+ * `y`, de baixo para cima. */
208
+ export function areaBackground(c, space, x, y, rtl = false) {
209
+ const fim = rtl ? "to left" : "to right";
210
+ const z = COLOR_CHANNELS[space].find((ch) => ch !== x && ch !== y);
211
+ const zv = getChannel(c, z, space);
212
+ if (space === "rgb") {
213
+ const so = (ch, v) => {
214
+ const rgb = { r: 0, g: 0, b: 0 };
215
+ rgb[ch === "red" ? "r" : ch === "green" ? "g" : "b"] = v;
216
+ return toHex(rgb);
217
+ };
218
+ return { background: [`linear-gradient(${fim}, ${so(x, 0)}, ${so(x, 255)})`, `linear-gradient(to top, ${so(y, 0)}, ${so(y, 255)})`,
219
+ so(z, zv)].join(", "), blend: "screen" };
220
+ }
221
+ const base = setChannel({ h: 0, s: 1, b: 1, a: 1 }, z, zv, space);
222
+ const receita = (ch) => ch === "hue" ? [0, 60, 120, 180, 240, 300, 360].map((h) => opaca(setChannel(base, "hue", h, space))).join(", ")
223
+ : ch === "saturation" ? `${opaca(setChannel(base, "saturation", 0, space))}, transparent`
224
+ : ch === "lightness" ? "#000, transparent, #fff"
225
+ : "#000, transparent";
226
+ const camadas = COLOR_CHANNELS[space].filter((ch) => ch !== z)
227
+ .map((ch) => `linear-gradient(${ch === x ? fim : "to top"}, ${receita(ch)})`).reverse();
228
+ if (z === "hue")
229
+ camadas.push(opaca(base));
230
+ return { background: camadas.join(", ") };
231
+ }
232
+ // A CLARIDADE ("escuro", "claro") é pelo HSL: a luminosidade dele põe o amarelo puro e o azul puro no MESMO meio
233
+ // (0,5), então as palavras dizem a mistura com branco ou preto, e não a claridade própria de cada matiz. A FORÇA
234
+ // ("vivo", "pálido", "acinzentado") é pelo HSB: no HSL um rosa pastel tem saturação 1 e sairia "vivo".
235
+ function matizPeloNome(h, l) {
236
+ if (h >= 15 && h < 70 && l < 0.35)
237
+ return "brown";
238
+ if ((h >= 330 || h < 15) && l > 0.7)
239
+ return "pink";
240
+ if (h < 15 || h >= 345)
241
+ return "red";
242
+ if (h < 45)
243
+ return "orange";
244
+ if (h < 70)
245
+ return "yellow";
246
+ // O ciano é estreito (170–190): o azul-claro do CSS (`#add8e6`, 194,7°) e o azul-céu (197°) são azuis para quem vê.
247
+ if (h < 170)
248
+ return "green";
249
+ if (h < 190)
250
+ return "cyan";
251
+ if (h < 255)
252
+ return "blue";
253
+ if (h < 290)
254
+ return "purple";
255
+ return "magenta";
256
+ }
257
+ /** O nome da cor na língua das frases: "vivid blue", "dark red", "rosa pálido". */
258
+ export function colorName(rgb, s) {
259
+ const { h, l } = rgbToHsl(rgb);
260
+ const { s: forca, b: brilho } = rgbToHsb(rgb);
261
+ let hue;
262
+ let claridade;
263
+ let croma;
264
+ if (l >= 0.95)
265
+ hue = "white";
266
+ else if (l <= 0.08)
267
+ hue = "black";
268
+ else if (forca < 0.12)
269
+ hue = "gray";
270
+ else {
271
+ hue = matizPeloNome(h, l);
272
+ croma = forca >= 0.8 && brilho >= 0.8 ? "vivid" : forca < 0.4 ? (brilho >= 0.85 ? "pale" : "grayish") : undefined;
273
+ }
274
+ // "Pálido" já diz claro; o branco, o preto, o marrom e o rosa já dizem a claridade no próprio nome.
275
+ if (croma !== "pale" && hue !== "white" && hue !== "black" && hue !== "brown" && hue !== "pink") {
276
+ claridade = l < 0.2 ? "veryDark" : l < 0.35 ? "dark" : l > 0.85 ? "veryLight" : l > 0.7 ? "light" : undefined;
277
+ }
278
+ return s.colorNameFormat
279
+ .replace("{lightness}", claridade ? s.colorTone[claridade] : "")
280
+ .replace("{chroma}", croma ? s.colorTone[croma] : "")
281
+ .replace("{hue}", s.colorHue[hue])
282
+ .replace(/\s+/g, " ").trim();
283
+ }
284
+ /** O valor de um canal escrito para o leitor de tela e a saída: "210°", "80%", "128", alfa "50%". */
285
+ export function formatChannel(ch, v) {
286
+ if (ch === "hue")
287
+ return `${Math.round(v)}°`;
288
+ if (ch === "alpha")
289
+ return `${Math.round(v * 100)}%`;
290
+ if (ch === "red" || ch === "green" || ch === "blue")
291
+ return `${Math.round(v)}`;
292
+ return `${Math.round(v)}%`;
293
+ }
@@ -1,11 +1,21 @@
1
- import { type ReactNode } from "react";
2
- export declare function Accordion({ items }: {
3
- items: Array<{
4
- id: string;
5
- title: ReactNode;
6
- content: ReactNode;
7
- }>;
8
- }): import("react").JSX.Element;
1
+ import React, { type ReactNode } from "react";
2
+ export type AccordionVariant = "default" | "surface";
3
+ export interface AccordionItem {
4
+ id: string;
5
+ title: ReactNode;
6
+ content: ReactNode;
7
+ /** O item que não abre: anunciado como desligado e fora do Tab. */
8
+ disabled?: boolean;
9
+ }
10
+ export declare function Accordion({ items, variant, multiple, defaultValue, className }: {
11
+ items: AccordionItem[];
12
+ variant?: AccordionVariant;
13
+ /** Deixa vários itens abertos ao mesmo tempo. Sem isto, abrir um fecha o outro. */
14
+ multiple?: boolean;
15
+ /** Os itens que começam abertos (o `id` de cada um). */
16
+ defaultValue?: string[];
17
+ className?: string;
18
+ }): React.JSX.Element;
9
19
  export declare function Collapsible({ trigger, children, open, defaultOpen, onOpenChange, disabled, className }: {
10
20
  trigger: ReactNode;
11
21
  children: ReactNode;
@@ -14,4 +24,4 @@ export declare function Collapsible({ trigger, children, open, defaultOpen, onOp
14
24
  onOpenChange?: (open: boolean) => void;
15
25
  disabled?: boolean;
16
26
  className?: string;
17
- }): import("react").JSX.Element;
27
+ }): React.JSX.Element;
@@ -1,10 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // Fase 9 (achado A5): este arquivo saiu do index.tsx de 970 linhas. Um módulo por categoria
4
+ // do registry — a taxonomia já existia e é gateada. A ordem de import entre eles é um DAG:
5
+ // internal → system → actions → feedback → inputs → navigation → layout → data-display → resto.
6
+ import React from "react";
3
7
  import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
4
8
  import { cx } from "./internal.js";
5
9
  import { soOValor } from "./pure.js";
6
10
  import { Icon } from "./system.js";
7
- export function Accordion({ items }) { return _jsx("div", { className: "accordion", children: items.map(i => _jsxs("details", { children: [_jsx("summary", { children: i.title }), _jsx("div", { className: "accordion-content", children: i.content })] }, i.id)) }); }
11
+ export function Accordion({ items, variant = "default", multiple = false, defaultValue, className }) {
12
+ const grupo = React.useId();
13
+ return _jsx("div", { className: cx("accordion", variant === "surface" && "accordion-surface", className), children: items.map(i => _jsxs("details", { className: "accordion-item", name: multiple ? undefined : grupo, open: defaultValue?.includes(i.id) || undefined, children: [_jsxs("summary", { "aria-disabled": i.disabled || undefined, tabIndex: i.disabled ? -1 : undefined, onClick: i.disabled ? (e) => e.preventDefault() : undefined, children: [_jsx("span", { className: "accordion-title", children: i.title }), _jsx("span", { className: "accordion-chevron", "aria-hidden": "true" })] }), _jsx("div", { className: "accordion-content", children: i.content })] }, i.id)) });
14
+ }
8
15
  // COLLAPSIBLE — uma seção que abre e fecha, com o estado do lado de quem usa.
9
16
  //
10
17
  // Não é um Accordion de um item só, e a diferença é o motivo de existir: o `Accordion` é um
@@ -8,15 +8,17 @@ export declare function Status({ variant, state, children, className, ...props }
8
8
  variant?: StatusVariant;
9
9
  state?: UniversalState;
10
10
  }): React.JSX.Element;
11
- export type AlertVariant = "info" | "success" | "warning" | "danger";
12
- export declare function Alert({ variant, state, title, icon, onDismiss, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
11
+ export type AlertVariant = "info" | "success" | "warning" | "danger" | "accent";
12
+ export declare function Alert({ variant, state, title, icon, onDismiss, action, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
13
13
  variant?: AlertVariant;
14
14
  state?: UniversalState;
15
15
  title?: ReactNode;
16
16
  icon?: IconName;
17
17
  onDismiss?: () => void;
18
+ /** O botão do aviso ("Atualizar", "Desfazer"): depois do texto, antes do X. No celular, embaixo do texto. */
19
+ action?: ReactNode;
18
20
  }): React.JSX.Element;
19
- export type BannerVariant = AlertVariant;
21
+ export type BannerVariant = "info" | "success" | "warning" | "danger";
20
22
  export declare function Banner({ variant, state, title, icon, onDismiss, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
21
23
  variant?: BannerVariant;
22
24
  state?: UniversalState;
@@ -24,13 +26,14 @@ export declare function Banner({ variant, state, title, icon, onDismiss, childre
24
26
  icon?: IconName;
25
27
  onDismiss?: () => void;
26
28
  }): React.JSX.Element;
29
+ export type SpinnerSize = "sm" | "md" | "lg" | "xl";
27
30
  export declare function Spinner({ size, label, decorative, className, ...props }: HTMLAttributes<HTMLSpanElement> & RefAttributes<HTMLSpanElement> & {
28
- size?: Responsive<"sm" | "md" | "lg">;
31
+ size?: Responsive<SpinnerSize>;
29
32
  label?: string;
30
33
  decorative?: boolean;
31
34
  }): React.JSX.Element;
32
35
  export type DataStateValue = "loading" | "error" | "empty" | UniversalState;
33
- export declare function DataState({ state, message, skeleton, emptyTitle, emptyIcon, action, children, className, ...props }: HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
36
+ export type DataStateProps = HTMLAttributes<HTMLDivElement> & RefAttributes<HTMLDivElement> & {
34
37
  state?: DataStateValue;
35
38
  message?: ReactNode;
36
39
  skeleton?: ReactNode;
@@ -38,7 +41,12 @@ export declare function DataState({ state, message, skeleton, emptyTitle, emptyI
38
41
  emptyIcon?: IconName;
39
42
  action?: ReactNode;
40
43
  children: ReactNode | (() => ReactNode);
41
- }): React.JSX.Element;
44
+ /** A barreira de erro (MNT-25): o que quebrar ao desenhar o conteúdo vira a cara de erro, com "Tentar de novo". */
45
+ catchErrors?: boolean;
46
+ /** Recebe o erro que a barreira pegou — para o app registrar. */
47
+ onCatch?: (error: unknown, info: React.ErrorInfo) => void;
48
+ };
49
+ export declare function DataState({ catchErrors, onCatch, ...props }: DataStateProps): React.JSX.Element;
42
50
  export declare function EmptyState({ icon, title, titleAs: TitleTag, description, action, state }: {
43
51
  icon?: IconName;
44
52
  title: ReactNode;
@@ -8,7 +8,7 @@ import React from "react";
8
8
  import { Popover as BasePopover } from "@base-ui/react/popover";
9
9
  import { cx, useAureaStrings, usePortalContainer, stateSeverity } from "./internal.js";
10
10
  import { Icon } from "./system.js";
11
- import { Button, IconButton } from "./actions.js";
11
+ import { Button, IconButton, CloseButton } from "./actions.js";
12
12
  // `oracle` saiu daqui (achado M9, 26/07/2026): estava no tipo e no CSS, e NÃO estava na
13
13
  // ficha — ou seja, era superfície pública que o contrato não declarava. E é vocabulário do
14
14
  // app de origem (papel de agente), não do sistema, como as classes de domínio que o achado
@@ -36,7 +36,7 @@ export function Status({ variant, state, children, className, ...props }) { cons
36
36
  // referência e o aviso de largura de página nem sempre quer glifo — a assimetria é decidida,
37
37
  // não herdada.
38
38
  const ICONE_DA_VARIANTE = { info: "info",
39
- success: "check-circle", warning: "warning", danger: "x-circle" };
39
+ success: "check-circle", warning: "warning", danger: "x-circle", accent: "info" };
40
40
  // A grade do `.alert` tem TRÊS trilhas (auto 1fr auto): ícone, corpo, ação. Uma versão anterior
41
41
  // emitia o título solto na trilha 1 — a do ícone — e o corpo como item anônimo. Medido no
42
42
  // navegador em 21/08/2026: o título ocupava a coluna do ícone e crescia com o próprio texto,
@@ -51,23 +51,36 @@ const ICONE_DA_VARIANTE = { info: "info",
51
51
  // universal. Quem passa só `state` recebe a cor derivada da gravidade e o rótulo universal
52
52
  // quando não há filho; quem passa os dois manda, porque só o consumidor sabe se aquele
53
53
  // "esperando" dele é grave.
54
- export function Alert({ variant, state, title, icon, onDismiss, children, className, ...props }) { const s = useAureaStrings(); const v = variant ?? (state ? stateSeverity(state) : "info"); return _jsxs("div", { className: cx("alert", `alert-${v}`, className), role: v === "danger" ? "alert" : "status", "data-state": state, ...props, children: [_jsx(Icon, { name: icon ?? ICONE_DA_VARIANTE[v], weight: icon ? undefined : "fill" }), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children ?? (state ? s.universalState[state] : null)] }), onDismiss ? _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.close, onClick: onDismiss }) : _jsx("span", {})] }); }
54
+ // `action` (15/10/2026, o "Upgrade" da referência): o botão do aviso, depois do texto e antes do X; no celular ele desce
55
+ // para baixo do texto, na mesma coluna (a referência repete o botão; aqui ele só muda de lugar).
56
+ export function Alert({ variant, state, title, icon, onDismiss, action, children, className, ...props }) { const s = useAureaStrings(); const v = variant ?? (state ? stateSeverity(state) : "info"); return _jsxs("div", { className: cx("alert", `alert-${v}`, action != null && "alert-has-action", className), role: v === "danger" ? "alert" : "status", "data-state": state, ...props, children: [_jsx(Icon, { name: icon ?? ICONE_DA_VARIANTE[v], weight: icon ? undefined : "fill" }), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children ?? (state ? s.universalState[state] : null)] }), action != null && _jsx("div", { className: "alert-action", children: action }), onDismiss ? _jsx(CloseButton, { onClick: onDismiss }) : _jsx("span", {})] }); }
55
57
  export function Banner({ variant, state, title, icon, onDismiss, children, className, ...props }) { const s = useAureaStrings(); const v = variant ?? (state ? stateSeverity(state) : "info"); return _jsxs("div", { className: cx("banner", `banner-${v}`, className), role: v === "danger" ? "alert" : "status", "data-state": state, ...props, children: [icon ? _jsx(Icon, { name: icon }) : _jsx("span", {}), _jsxs("div", { children: [title && _jsx("strong", { children: title }), children ?? (state ? s.universalState[state] : null)] }), onDismiss ? _jsx(IconButton, { variant: "ghost", size: "sm", icon: "x", label: s.close, onClick: onDismiss }) : _jsx("span", {})] }); }
56
- // A barra grampeava 0..100 e o `aria-valuenow` NÃO — medido ao publicar o contrato de API na
57
- // Parte E: com value=150 o desenho parava em 100% e o leitor de tela anunciava "150 de 100".
58
- // A causa é a de sempre: o grampo existia num lugar só. Agora é UMA expressão que serve os dois,
59
- // então não há como divergirem de novo.
60
- // SÓ DETERMINADA, de propósito: não há modo indeterminado nem `.progress` para ele no core. A
61
- // ficha dizia que havia e era falso — corrigido junto, porque contrato que promete o que não
62
- // existe custa mais que ausência.
63
- // O DEGRAU RESPONSIVO volta (merge de 28/08/2026): a ficha declara `responsive.size` e o
64
- // componente tinha perdido a capacidade quando este arquivo entrou da `main`. Mesma perda
65
- // silenciosa do `Input`, do `Tabs` e do `NumberField` — a ficha documentava, o código não fazia.
66
- export function Spinner({ size, label, decorative, className, ...props }) { const s = useAureaStrings(); return _jsx("span", { className: cx("spinner", peleDoEixo("spinner", size, "sm", "spinner"), className), ...(decorative ? { "aria-hidden": true } : { role: "status", "aria-label": label ?? s.loading }), ...props }); }
58
+ export function Spinner({ size = "md", label, decorative, className, ...props }) { const s = useAureaStrings(); return _jsx("span", { className: cx("spinner", peleDoEixo("spinner", size, "sm", "spinner"), className), ...(decorative ? { "aria-hidden": true } : { role: "status", "aria-label": label ?? s.loading }), ...props }); }
67
59
  // `children` como FUNÇÃO é de propósito para o caso `loading`: assim o consumidor não paga o
68
60
  // render do conteúdo enquanto ele não existe. Aceita nó também, porque a maioria das telas já
69
61
  // tem o conteúdo pronto e obrigar função seria cerimônia.
70
- export function DataState({ state, message, skeleton, emptyTitle, emptyIcon, action, children, className, ...props }) {
62
+ // MNT-25 (passo 10, 0.32.0): A BARREIRA DE ERRO, como variação do `DataState` (escolha do Victor,
63
+ // 12/10/2026: escrita na Aurea, sem dependência nova; variação, e não peça nova). Um erro de desenho no app
64
+ // de rede deixou a tela inteira em branco. Com `catchErrors`, o que quebra DENTRO do `DataState` vira a
65
+ // cara de erro dele (a mensagem e "Tentar de novo"), e o resto da tela fica. O React só pega erro de
66
+ // desenho com um componente de CLASSE (não há gancho para isso) — é o jeito que ele manda, e o único.
67
+ // `onCatch`, e não `onError`: `onError` já é o evento do HTML que o `DataState` repassa ao `<div>`.
68
+ // O que a barreira NÃO pega (é do React, não daqui): erro em evento de clique, em código assíncrono e no
69
+ // desenho no servidor.
70
+ class BarreiraDeErro extends React.Component {
71
+ state = { quebrou: false };
72
+ static getDerivedStateFromError() { return { quebrou: true }; }
73
+ componentDidCatch(erro, info) { this.props.onCatch?.(erro, info); }
74
+ tentar = () => this.setState({ quebrou: false });
75
+ render() { return this.state.quebrou ? this.props.falha(this.tentar) : this.props.children; }
76
+ }
77
+ export function DataState({ catchErrors, onCatch, ...props }) {
78
+ const s = useAureaStrings();
79
+ if (!catchErrors)
80
+ return _jsx(CorpoDoDataState, { ...props });
81
+ return _jsx(BarreiraDeErro, { onCatch: onCatch, falha: tentar => _jsxs("div", { className: cx("data-state", props.className), "data-state": "error", children: [_jsx(Alert, { variant: "danger", children: props.message ?? s.dataCrashed }), _jsx("div", { children: _jsx(Button, { size: "sm", onClick: tentar, children: s.dataRetry }) })] }), children: _jsx(CorpoDoDataState, { ...props }) });
82
+ }
83
+ function CorpoDoDataState({ state, message, skeleton, emptyTitle, emptyIcon, action, children, className, ...props }) {
71
84
  const s = useAureaStrings();
72
85
  const conteudo = () => typeof children === "function" ? children() : children;
73
86
  const caixa = (inner, ocupado) => _jsx("div", { className: cx("data-state", className), "aria-busy": ocupado || undefined, "data-state": state, ...props, children: inner });
@@ -127,7 +140,7 @@ export function NotificationCenter({ items, onItemClick, onMarkAllRead, label, i
127
140
  ? _jsx("button", { type: "button", className: "notification-item", "data-read": it.read || undefined, onClick: () => { it.onClick?.(); onItemClick?.(it); }, children: body })
128
141
  : _jsx("div", { className: "notification-item", "data-read": it.read || undefined, children: body });
129
142
  };
130
- return _jsxs(BasePopover.Root, { children: [_jsxs("span", { className: "notification-trigger", children: [_jsx(BasePopover.Trigger, { render: _jsx(IconButton, { variant: "ghost", icon: icon, label: unread ? `${title} (${unread})` : title }) }), unread > 0 && _jsx("span", { className: "notification-count", "aria-hidden": "true", children: unread > 99 ? "99+" : unread })] }), _jsx(BasePopover.Portal, { container: portal, children: _jsx(BasePopover.Positioner, { side: side, sideOffset: 8, children: _jsxs(BasePopover.Popup, { className: "popover notification-panel", "aria-label": title, children: [_jsxs("div", { className: "notification-head", children: [_jsx(BasePopover.Title, { render: _jsx("strong", {}), children: title }), unread > 0 && onMarkAllRead && _jsx(Button, { variant: "ghost", size: "sm", onClick: onMarkAllRead, children: s.notificationMarkAll })] }), items.length
143
+ return _jsxs(BasePopover.Root, { children: [_jsxs("span", { className: "notification-trigger", children: [_jsx(BasePopover.Trigger, { render: _jsx(IconButton, { variant: "ghost", icon: icon, label: unread ? `${title} (${unread})` : title }) }), unread > 0 && _jsx("span", { className: "notification-count", "aria-hidden": "true", children: unread > 99 ? "99+" : unread })] }), _jsx(BasePopover.Portal, { container: portal, children: _jsx(BasePopover.Positioner, { className: "positioner", side: side, sideOffset: 8, children: _jsxs(BasePopover.Popup, { className: "popover notification-panel", "aria-label": title, children: [_jsxs("div", { className: "notification-head", children: [_jsx(BasePopover.Title, { render: _jsx("strong", {}), children: title }), unread > 0 && onMarkAllRead && _jsx(Button, { variant: "ghost", size: "sm", onClick: onMarkAllRead, children: s.notificationMarkAll })] }), items.length
131
144
  ? _jsx("div", { className: "notification-list", children: groups.map((g, gi) => {
132
145
  const gid = baseId + gi;
133
146
  return _jsxs(React.Fragment, { children: [g.label && _jsx("p", { className: "notification-group-label", id: gid, children: g.label }), _jsx("ul", { className: "notification-sublist", "aria-labelledby": g.label ? gid : undefined, children: g.items.map(it => _jsx("li", { children: renderRow(it) }, it.id)) })] }, gi);
@@ -0,0 +1 @@
1
+ import "elkjs/lib/elk-worker.min.js";
@@ -0,0 +1,17 @@
1
+ // O TRABALHADOR DO MAPA (MNT-22, 0.32.0): o arrumador do `DependencyGraph` fora da tela principal, por
2
+ // um caminho da Aurea.
3
+ //
4
+ // Vite: import TrabalhadorDoMapa from "@aurea-uds/react/graph-worker?worker";
5
+ // <DependencyGraph layout="layered" layoutWorker={() => new TrabalhadorDoMapa()} … />
6
+ //
7
+ // Por que existe: a receita antiga importava `elkjs/lib/elk-worker.min.js` direto, e a trava do app de
8
+ // rede reprova motor importado por fora da Aurea (`motor-usado-por-fora`). Este módulo é UMA linha que
9
+ // importa o trabalhador do `elkjs` (peer opcional) — ele NÃO copia o `elkjs` para dentro do pacote, que
10
+ // seria redistribuí-lo (decisão do Victor, 10/10/2026).
11
+ //
12
+ // Como funciona: o `elk-worker.min.js`, quando roda num trabalhador (sem `document`, com `self`), se
13
+ // registra sozinho em `self.onmessage` antes de qualquer outra coisa (medido no 0.12.0). O empacotador
14
+ // junta os dois num arquivo de trabalhador, e o `elk-api` da Aurea conversa com ele.
15
+ //
16
+ // ⚠ Este arquivo é a única exceção ao `sideEffects: false` do pacote: o import abaixo É o efeito.
17
+ import "elkjs/lib/elk-worker.min.js";
package/dist/graph.d.ts CHANGED
@@ -67,9 +67,19 @@ export interface GraphEdgeItem {
67
67
  pattern?: GraphEdgePattern;
68
68
  weight?: GraphEdgeWeight;
69
69
  mark?: GraphEdgeMark;
70
+ /** O ESTADO da ligação (MNT-26), como o selo do nó: a palavra e o tom. O tom pinta o traço; a palavra
71
+ * entra na parte "Estado" da legenda, com a amostra na cor — quem não distingue a cor lê a palavra.
72
+ * `mark` é o SINAL do estado no meio da linha (o × do "Caído"), ao lado da marca do tipo, para o
73
+ * estado se ver também sem cor. O TIPO da ligação (cobre, fibra) continua no desenho, no `mark` da
74
+ * linha e no `legend`: são duas coisas, e a legenda as mostra separadas (decisão do Victor, 12/10/2026). */
75
+ status?: {
76
+ label: string;
77
+ tone?: GraphTone;
78
+ mark?: GraphEdgeMark;
79
+ };
70
80
  /** Um número no meio da linha: os membros de uma agregação (LAG). */
71
81
  count?: number;
72
- /** O nome desta espécie de linha na legenda automática (`legend`). */
82
+ /** O nome do TIPO desta linha na parte "Tipo de ligação" da legenda automática (`legend`). */
73
83
  legend?: string;
74
84
  /** Quando dois rótulos se encostam, fica o de prioridade MAIOR; no empate, o que vem antes na
75
85
  * lista (MNT-12.5). O nome da porta na ponta conta com a mesma prioridade da linha. */