ds-tis 1.0.0-beta.10

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.
Files changed (79) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +125 -0
  3. package/css/base/forced-colors.css +62 -0
  4. package/css/base/icons.css +73 -0
  5. package/css/base/index.css +4 -0
  6. package/css/base/reset.css +80 -0
  7. package/css/base/typography.css +211 -0
  8. package/css/components/accordion.css +153 -0
  9. package/css/components/alert.css +161 -0
  10. package/css/components/avatar.css +76 -0
  11. package/css/components/badge.css +83 -0
  12. package/css/components/breadcrumb.css +58 -0
  13. package/css/components/button.css +364 -0
  14. package/css/components/card.css +159 -0
  15. package/css/components/checkbox.css +296 -0
  16. package/css/components/combobox.css +330 -0
  17. package/css/components/divider.css +20 -0
  18. package/css/components/form-field.css +137 -0
  19. package/css/components/index.css +28 -0
  20. package/css/components/input.css +356 -0
  21. package/css/components/link.css +67 -0
  22. package/css/components/menu.css +246 -0
  23. package/css/components/modal.css +236 -0
  24. package/css/components/pagination.css +132 -0
  25. package/css/components/radio.css +280 -0
  26. package/css/components/select.css +399 -0
  27. package/css/components/skeleton.css +52 -0
  28. package/css/components/spinner.css +59 -0
  29. package/css/components/tabs.css +79 -0
  30. package/css/components/textarea.css +218 -0
  31. package/css/components/toggle.css +202 -0
  32. package/css/components/tooltip.css +116 -0
  33. package/css/design-system.css +13 -0
  34. package/css/tokens/generated/component.css +720 -0
  35. package/css/tokens/generated/foundation.css +282 -0
  36. package/css/tokens/generated/index.css +5 -0
  37. package/css/tokens/generated/theme-dark.css +243 -0
  38. package/css/tokens/generated/theme-light.css +244 -0
  39. package/css/tokens/index.css +6 -0
  40. package/css/utilities/elevation.css +24 -0
  41. package/css/utilities/index.css +2 -0
  42. package/css/utilities/layout.css +132 -0
  43. package/docs/agent-consumer-usage.en.md +322 -0
  44. package/docs/agent-consumer-usage.md +294 -0
  45. package/docs/api/adrs.json +186 -0
  46. package/docs/api/components.json +2071 -0
  47. package/docs/api/consumer-context.json +66 -0
  48. package/docs/api/foundations.json +94 -0
  49. package/docs/api/tokens.json +6839 -0
  50. package/docs/llms-full.txt +23699 -0
  51. package/docs/llms.txt +109 -0
  52. package/docs/templates/contact.html +364 -0
  53. package/docs/templates/dashboard.html +318 -0
  54. package/docs/templates/index.html +232 -0
  55. package/docs/templates/login.html +286 -0
  56. package/docs/templates/settings.html +365 -0
  57. package/docs/templates/signup.html +350 -0
  58. package/js/accordion.js +192 -0
  59. package/js/combobox.js +263 -0
  60. package/js/menu.js +301 -0
  61. package/js/modal.js +256 -0
  62. package/js/package.json +3 -0
  63. package/js/tabs.js +200 -0
  64. package/js/theme/apply.js +75 -0
  65. package/js/theme/brand-contrast-audit.js +133 -0
  66. package/js/theme/color.js +149 -0
  67. package/js/theme/config-schema.js +40 -0
  68. package/js/theme/contrast.js +76 -0
  69. package/js/theme/export.js +118 -0
  70. package/js/theme/index.js +21 -0
  71. package/js/theme/overlay.js +29 -0
  72. package/js/theme/package.json +3 -0
  73. package/js/theme/palette.js +103 -0
  74. package/js/theme/radius.js +76 -0
  75. package/js/theme/semantic-mapper.js +138 -0
  76. package/js/theme/typography.js +33 -0
  77. package/js/theme/url-state.js +52 -0
  78. package/js/tooltip.js +227 -0
  79. package/package.json +139 -0
@@ -0,0 +1,149 @@
1
+ /* ============================================================
2
+ color.js — conversões de cor sem dependências (browser + Node)
3
+
4
+ Suporta a cadeia sRGB(hex) ↔ linear sRGB ↔ OKLab ↔ OKLCH.
5
+ OKLCH é usado como espaço de trabalho porque é perceptualmente
6
+ uniforme: variar lightness/chroma gera escalas com passos
7
+ visualmente regulares, ao contrário de HSL.
8
+
9
+ Referências: Björn Ottosson, "A perceptual color space for image
10
+ processing" (OKLab/OKLCH).
11
+ ============================================================ */
12
+
13
+ /** Limita n ao intervalo [min, max]. */
14
+ export function clamp(n, min, max) {
15
+ return Math.min(max, Math.max(min, n));
16
+ }
17
+
18
+ /** "#2563EB" | "2563eb" | "#25f" → { r, g, b } em 0..255. */
19
+ export function hexToRgb(hex) {
20
+ let h = String(hex).trim().replace(/^#/, '');
21
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
22
+ if (h.length !== 6 || /[^0-9a-fA-F]/.test(h)) {
23
+ throw new Error(`hex inválido: ${hex}`);
24
+ }
25
+ return {
26
+ r: parseInt(h.slice(0, 2), 16),
27
+ g: parseInt(h.slice(2, 4), 16),
28
+ b: parseInt(h.slice(4, 6), 16),
29
+ };
30
+ }
31
+
32
+ /** { r, g, b } 0..255 → "#RRGGBB" uppercase. */
33
+ export function rgbToHex({ r, g, b }) {
34
+ const to2 = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');
35
+ return ('#' + to2(r) + to2(g) + to2(b)).toUpperCase();
36
+ }
37
+
38
+ /** sRGB 8-bit channel → linear-light 0..1. */
39
+ function srgbToLinear(c) {
40
+ const cs = c / 255;
41
+ return cs <= 0.04045 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
42
+ }
43
+
44
+ /** linear-light 0..1 → sRGB 8-bit channel 0..255. */
45
+ function linearToSrgb(c) {
46
+ const cs = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
47
+ return clamp(Math.round(cs * 255), 0, 255);
48
+ }
49
+
50
+ /** { r, g, b } 0..255 → OKLab { L, a, b }. */
51
+ export function rgbToOklab({ r, g, b }) {
52
+ const lr = srgbToLinear(r);
53
+ const lg = srgbToLinear(g);
54
+ const lb = srgbToLinear(b);
55
+
56
+ const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
57
+ const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
58
+ const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
59
+
60
+ const l_ = Math.cbrt(l);
61
+ const m_ = Math.cbrt(m);
62
+ const s_ = Math.cbrt(s);
63
+
64
+ return {
65
+ L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
66
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
67
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
68
+ };
69
+ }
70
+
71
+ /** OKLab { L, a, b } → { r, g, b } 0..255 (com clamp de gamut). */
72
+ export function oklabToRgb({ L, a, b }) {
73
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
74
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
75
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
76
+
77
+ const l = l_ * l_ * l_;
78
+ const m = m_ * m_ * m_;
79
+ const s = s_ * s_ * s_;
80
+
81
+ const lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
82
+ const lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
83
+ const lb = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
84
+
85
+ return { r: linearToSrgb(lr), g: linearToSrgb(lg), b: linearToSrgb(lb) };
86
+ }
87
+
88
+ /** OKLab { L, a, b } → OKLCH { L, C, h } (h em graus 0..360). */
89
+ export function oklabToOklch({ L, a, b }) {
90
+ const C = Math.sqrt(a * a + b * b);
91
+ let h = Math.atan2(b, a) * (180 / Math.PI);
92
+ if (h < 0) h += 360;
93
+ return { L, C, h };
94
+ }
95
+
96
+ /** OKLCH { L, C, h } → OKLab { L, a, b }. */
97
+ export function oklchToOklab({ L, C, h }) {
98
+ const hr = (h * Math.PI) / 180;
99
+ return { L, a: Math.cos(hr) * C, b: Math.sin(hr) * C };
100
+ }
101
+
102
+ /** "#RRGGBB" → OKLCH { L, C, h }. */
103
+ export function hexToOklch(hex) {
104
+ return oklabToOklch(rgbToOklab(hexToRgb(hex)));
105
+ }
106
+
107
+ /** OKLCH { L, C, h } → "#RRGGBB" (gamut sRGB via clamp por canal). */
108
+ export function oklchToHex(oklch) {
109
+ return rgbToHex(oklabToRgb(oklchToOklab(oklch)));
110
+ }
111
+
112
+ /**
113
+ * Luminância relativa WCAG (0..1) a partir de { r, g, b } 0..255.
114
+ * Usada por contrast.js. Mesma fórmula da WCAG 2.x.
115
+ */
116
+ export function relativeLuminance({ r, g, b }) {
117
+ const lin = (c) => {
118
+ const cs = c / 255;
119
+ return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
120
+ };
121
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
122
+ }
123
+
124
+ /** Monta string rgba() a partir de hex + alpha 0..1. */
125
+ export function hexToRgba(hex, alpha) {
126
+ const { r, g, b } = hexToRgb(hex);
127
+ const a = clamp(alpha, 0, 1);
128
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
129
+ }
130
+
131
+ /** "rgba(r,g,b,a)" → { r, g, b, a }. */
132
+ export function parseRgba(str) {
133
+ const m = String(str).trim().match(
134
+ /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/,
135
+ );
136
+ if (!m) throw new Error(`rgba inválido: ${str}`);
137
+ return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
138
+ }
139
+
140
+ /** Cor semi-transparente composta sobre fundo sólido hex → hex resultante. */
141
+ export function compositeOnHex(overlay, underHex) {
142
+ const { r, g, b, a } = typeof overlay === 'string' ? parseRgba(overlay) : overlay;
143
+ const bg = hexToRgb(underHex);
144
+ return rgbToHex({
145
+ r: r * a + bg.r * (1 - a),
146
+ g: g * a + bg.g * (1 - a),
147
+ b: b * a + bg.b * (1 - a),
148
+ });
149
+ }
@@ -0,0 +1,40 @@
1
+ /* ============================================================
2
+ config-schema.js — normalização e defaults do ThemeConfig
3
+
4
+ ThemeConfig é a abstração de alto nível manipulada pela UI
5
+ (equivalente ao preset do shadcn create / accentColor do Radix).
6
+ ============================================================ */
7
+
8
+ /** Config padrão = paleta default do DS (blue), radius default, Inter/DM Mono. */
9
+ export const DEFAULT_CONFIG = {
10
+ brand: { seed: '#0056E0', chromaBoost: 1 },
11
+ radius: 'default',
12
+ typography: { sans: 'Inter', mono: 'DM Mono' },
13
+ mode: 'light',
14
+ };
15
+
16
+ const HEX_RE = /^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
17
+
18
+ /**
19
+ * Normaliza um config parcial contra os defaults e valida campos.
20
+ * Lança Error em entrada inválida (seed não-hex, mode inválido).
21
+ */
22
+ export function normalizeConfig(input = {}) {
23
+ const cfg = {
24
+ brand: { ...DEFAULT_CONFIG.brand, ...(input.brand || {}) },
25
+ radius: input.radius === 'pill' ? 'soft' : (input.radius ?? DEFAULT_CONFIG.radius),
26
+ typography: { ...DEFAULT_CONFIG.typography, ...(input.typography || {}) },
27
+ mode: input.mode ?? DEFAULT_CONFIG.mode,
28
+ };
29
+
30
+ if (!HEX_RE.test(String(cfg.brand.seed))) {
31
+ throw new Error(`brand.seed inválido: ${cfg.brand.seed}`);
32
+ }
33
+ if (!cfg.brand.seed.startsWith('#')) cfg.brand.seed = '#' + cfg.brand.seed;
34
+ cfg.brand.seed = cfg.brand.seed.toUpperCase();
35
+
36
+ if (cfg.mode !== 'light' && cfg.mode !== 'dark') {
37
+ throw new Error(`mode inválido: ${cfg.mode}`);
38
+ }
39
+ return cfg;
40
+ }
@@ -0,0 +1,76 @@
1
+ /* ============================================================
2
+ contrast.js — razão de contraste WCAG 2.2 e seleção acessível
3
+
4
+ WCAG 2.2 AA (AGENTS.md §8):
5
+ - texto normal: >= 4.5:1
6
+ - texto large / componentes UI: >= 3:1
7
+ ============================================================ */
8
+
9
+ import { hexToRgb, relativeLuminance, compositeOnHex } from './color.js';
10
+
11
+ export const WCAG_AA_TEXT = 4.5;
12
+ export const WCAG_AA_LARGE = 3;
13
+ export const WCAG_AA_UI = 3;
14
+
15
+ /**
16
+ * Razão de contraste WCAG entre duas cores. Aceita hex; bg rgba exige
17
+ * `under` (hex do fundo sobre o qual o overlay semi-transparente está).
18
+ */
19
+ export function contrastRatio(fg, bg, { under } = {}) {
20
+ let fgHex = fg;
21
+ let bgHex = bg;
22
+ if (String(fg).startsWith('rgba') || String(fg).startsWith('rgb(')) {
23
+ if (!under) throw new Error('contrastRatio: under é obrigatório quando fg é rgba');
24
+ fgHex = compositeOnHex(fg, under);
25
+ }
26
+ if (String(bg).startsWith('rgba') || String(bg).startsWith('rgb(')) {
27
+ if (!under) throw new Error('contrastRatio: under é obrigatório quando bg é rgba');
28
+ bgHex = compositeOnHex(bg, under);
29
+ }
30
+ const la = relativeLuminance(hexToRgb(fgHex));
31
+ const lb = relativeLuminance(hexToRgb(bgHex));
32
+ const lighter = Math.max(la, lb);
33
+ const darker = Math.min(la, lb);
34
+ return (lighter + 0.05) / (darker + 0.05);
35
+ }
36
+
37
+ /** true se o par atinge o threshold (default: texto normal 4.5:1). */
38
+ export function meetsContrast(hexA, hexB, threshold = WCAG_AA_TEXT) {
39
+ return contrastRatio(hexA, hexB) >= threshold;
40
+ }
41
+
42
+ /**
43
+ * Escolhe o foreground de maior contraste entre candidatos (default:
44
+ * quase-branco vs quase-preto neutros do DS). Retorna o hex vencedor
45
+ * e se ele passa no threshold.
46
+ */
47
+ export function pickAccessibleForeground(
48
+ bgHex,
49
+ { light = '#F8FAFC', dark = '#0F172A', threshold = WCAG_AA_TEXT } = {}
50
+ ) {
51
+ const rLight = contrastRatio(bgHex, light);
52
+ const rDark = contrastRatio(bgHex, dark);
53
+ const useDark = rDark >= rLight;
54
+ const fg = useDark ? dark : light;
55
+ const ratio = useDark ? rDark : rLight;
56
+ return { fg, ratio, passes: ratio >= threshold };
57
+ }
58
+
59
+ /**
60
+ * Avalia uma lista de pares foreground/background e retorna um relatório.
61
+ * Cada par: { name, fg, bg, threshold? }.
62
+ */
63
+ export function auditPairs(pairs) {
64
+ return pairs.map((p) => {
65
+ const threshold = p.threshold ?? WCAG_AA_TEXT;
66
+ const ratio = contrastRatio(p.fg, p.bg, p.under ? { under: p.under } : undefined);
67
+ return {
68
+ name: p.name,
69
+ fg: p.fg,
70
+ bg: p.bg,
71
+ ratio: Math.round(ratio * 100) / 100,
72
+ threshold,
73
+ passes: ratio >= threshold,
74
+ };
75
+ });
76
+ }
@@ -0,0 +1,118 @@
1
+ /* ============================================================
2
+ export.js — gera artefatos exportáveis do tema
3
+
4
+ Três formatos (como shadcn / Radix):
5
+ - CSS snippet → bloco [data-theme] pronto pra colar
6
+ - JSON config → ThemeConfig serializado
7
+ - DTCG patch → subset pra alimentar tokens/foundation/colors.json
8
+ ============================================================ */
9
+
10
+ import { normalizeConfig } from './config-schema.js';
11
+ import { mapThemeToVars } from './semantic-mapper.js';
12
+ import { generateBrandScale, STEPS } from './palette.js';
13
+ import { generateTonedOverlays } from './overlay.js';
14
+
15
+ /**
16
+ * Snippet CSS com as vars sob um seletor data-theme. Separa as props
17
+ * mode-invariant (scale, radius, fonts) das que variam por modo
18
+ * (overlays/foreground), emitindo blocos light e dark.
19
+ */
20
+ export function toCssSnippet(config, themeName = 'custom') {
21
+ const cfg = normalizeConfig(config);
22
+ const light = mapThemeToVars({ ...cfg, mode: 'light' }, 'light').vars;
23
+ const dark = mapThemeToVars({ ...cfg, mode: 'dark' }, 'dark').vars;
24
+
25
+ // Vars iguais nos dois modos vão pro bloco base; o resto pro override dark.
26
+ const base = {};
27
+ const darkOnly = {};
28
+ for (const [k, v] of Object.entries(light)) {
29
+ if (dark[k] === v) base[k] = v;
30
+ else base[k] = v;
31
+ }
32
+ for (const [k, v] of Object.entries(dark)) {
33
+ if (light[k] !== v) darkOnly[k] = v;
34
+ }
35
+
36
+ const fmt = (obj) =>
37
+ Object.entries(obj)
38
+ .map(([k, v]) => ` ${k}: ${v};`)
39
+ .join('\n');
40
+
41
+ let out = `[data-theme="${themeName}"] {\n${fmt(base)}\n}`;
42
+ if (Object.keys(darkOnly).length) {
43
+ out += `\n\n[data-theme="${themeName}"][data-mode="dark"] {\n${fmt(darkOnly)}\n}`;
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /** JSON pretty do ThemeConfig normalizado. */
49
+ export function toJsonConfig(config) {
50
+ return JSON.stringify(normalizeConfig(config), null, 2);
51
+ }
52
+
53
+ /**
54
+ * Patch DTCG para foundation.color.brand.* (retrocompatível).
55
+ * Preferir toDtcgThemePatch() para write-back completo.
56
+ */
57
+ export function toDtcgBrandPatch(config) {
58
+ const patch = toDtcgThemePatch(config);
59
+ return { foundation: { color: { brand: patch.foundation.color.brand } } };
60
+ }
61
+
62
+ /**
63
+ * Patch DTCG completo: foundation brand + overlays toned + alias semantic.content.brand.
64
+ * Não escreve arquivos — devolve objeto para revisão/write-back manual.
65
+ */
66
+ export function toDtcgThemePatch(config) {
67
+ const cfg = normalizeConfig(config);
68
+ const scale = generateBrandScale(cfg.brand.seed, {
69
+ chromaBoost: cfg.brand.chromaBoost ?? 1,
70
+ });
71
+ const lightOverlays = generateTonedOverlays(scale[600], 'light');
72
+ const darkOverlays = generateTonedOverlays(scale[400], 'dark');
73
+
74
+ const brand = {};
75
+ for (const step of STEPS) {
76
+ brand[step] = { $type: 'color', $value: scale[step] };
77
+ }
78
+
79
+ return {
80
+ foundation: {
81
+ color: {
82
+ brand,
83
+ overlay: {
84
+ 'brand-600': {
85
+ 12: { $type: 'color', $value: lightOverlays.default },
86
+ 20: { $type: 'color', $value: lightOverlays.hover },
87
+ 28: { $type: 'color', $value: lightOverlays.active },
88
+ },
89
+ 'brand-400': {
90
+ 15: { $type: 'color', $value: darkOverlays.default },
91
+ 25: { $type: 'color', $value: darkOverlays.hover },
92
+ 32: { $type: 'color', $value: darkOverlays.active },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ semantic: {
98
+ light: {
99
+ content: {
100
+ brand: {
101
+ $type: 'color',
102
+ $value: '{foundation.color.brand.700}',
103
+ $description: 'Conteúdo brand sobre superfície neutra (light).',
104
+ },
105
+ },
106
+ },
107
+ dark: {
108
+ content: {
109
+ brand: {
110
+ $type: 'color',
111
+ $value: '{foundation.color.brand.400}',
112
+ $description: 'Conteúdo brand sobre superfície neutra (dark).',
113
+ },
114
+ },
115
+ },
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,21 @@
1
+ /* ============================================================
2
+ index.js — API pública do theme engine
3
+
4
+ Uso no browser (playground) ou Node (scripts/testes):
5
+
6
+ import { applyTheme, toCssSnippet } from './js/theme/index.js';
7
+ applyTheme({ brand: { seed: '#EA580C' }, radius: 'round', mode: 'light' });
8
+ ============================================================ */
9
+
10
+ export * from './color.js';
11
+ export * from './contrast.js';
12
+ export * from './palette.js';
13
+ export * from './overlay.js';
14
+ export * from './radius.js';
15
+ export * from './typography.js';
16
+ export * from './config-schema.js';
17
+ export * from './semantic-mapper.js';
18
+ export * from './apply.js';
19
+ export * from './export.js';
20
+ export * from './url-state.js';
21
+ export * from './brand-contrast-audit.js';
@@ -0,0 +1,29 @@
1
+ /* ============================================================
2
+ overlay.js — gera os overlays toned brand-derived
3
+
4
+ Espelha foundation.color.overlay.brand-600.* (light) e
5
+ brand-400.* (dark). Os alphas são os mesmos do DS (ADR-007):
6
+ light: 600 @ 12/20/28%
7
+ dark: 400 @ 15/25/32%
8
+ ============================================================ */
9
+
10
+ import { hexToRgba } from './color.js';
11
+
12
+ export const LIGHT_ALPHAS = { default: 0.12, hover: 0.2, active: 0.28 };
13
+ export const DARK_ALPHAS = { default: 0.15, hover: 0.25, active: 0.32 };
14
+
15
+ /**
16
+ * Gera os 3 overlays toned de um modo a partir do hex base.
17
+ *
18
+ * @param {string} baseHex - brand.600 (light) ou brand.400 (dark).
19
+ * @param {'light'|'dark'} mode
20
+ * @returns {{default:string, hover:string, active:string}}
21
+ */
22
+ export function generateTonedOverlays(baseHex, mode) {
23
+ const alphas = mode === 'dark' ? DARK_ALPHAS : LIGHT_ALPHAS;
24
+ return {
25
+ default: hexToRgba(baseHex, alphas.default),
26
+ hover: hexToRgba(baseHex, alphas.hover),
27
+ active: hexToRgba(baseHex, alphas.active),
28
+ };
29
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,103 @@
1
+ /* ============================================================
2
+ palette.js — gera escala de marca 50..950 a partir de 1 seed
3
+
4
+ Estratégia (OKLCH):
5
+ 1. Converte a seed para OKLCH.
6
+ 2. Lightness (L): usa os alvos medidos da rampa default do DS.
7
+ L é o que define "quão claro/escuro" é um step — deve ser
8
+ estável independente da marca.
9
+ 3. Hue (h): preserva o *shift* de hue de cada step relativo ao
10
+ step âncora (500) da rampa default, somado ao hue da seed.
11
+ Rampas reais "esfriam/esquentam" levemente nos extremos; isso
12
+ reproduz esse comportamento para qualquer marca.
13
+ 4. Chroma (C): escala o chroma default de cada step pela razão
14
+ entre o chroma da seed e o chroma default do step âncora,
15
+ com clamp de gamut.
16
+
17
+ DEFAULT_L / DEFAULT_C / DEFAULT_H foram medidos do blue default do DS
18
+ (foundation.color.brand.*), então seed = #2563EB reproduz a paleta
19
+ existente quase bit-a-bit.
20
+ ============================================================ */
21
+
22
+ import { hexToOklch, oklchToHex, clamp } from './color.js';
23
+
24
+ export const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
25
+
26
+ /** Step âncora: onde a marca é mais reconhecível e onde a seed "encaixa". */
27
+ export const SEED_ANCHOR_STEP = 500;
28
+
29
+ /** Lightness OKLCH alvo por step (medido do blue default do DS). */
30
+ const DEFAULT_L = {
31
+ 50: 0.9705, 100: 0.9319, 200: 0.8823, 300: 0.8091, 400: 0.7137,
32
+ 500: 0.6231, 600: 0.5461, 700: 0.4882, 800: 0.4244, 900: 0.3791, 950: 0.2823,
33
+ };
34
+
35
+ /** Chroma OKLCH de referência por step (blue default do DS). */
36
+ const DEFAULT_C = {
37
+ 50: 0.0142, 100: 0.0316, 200: 0.0571, 300: 0.0956, 400: 0.1434,
38
+ 500: 0.188, 600: 0.2152, 700: 0.2172, 800: 0.1809, 900: 0.1378, 950: 0.0874,
39
+ };
40
+
41
+ /** Hue OKLCH por step (blue default do DS). */
42
+ const DEFAULT_H = {
43
+ 50: 254.6, 100: 255.59, 200: 254.13, 300: 251.81, 400: 254.62,
44
+ 500: 259.81, 600: 262.88, 700: 264.38, 800: 265.64, 900: 265.52, 950: 267.94,
45
+ };
46
+
47
+ /**
48
+ * Encontra o step cuja lightness default mais se aproxima de L.
49
+ * Usado para ancorar a seed no ponto certo da rampa, garantindo que
50
+ * uma seed que já é (ex) o brand-600 reproduza a paleta com scale 1.0.
51
+ */
52
+ function nearestStepByLightness(L) {
53
+ let best = STEPS[0];
54
+ let bestDiff = Infinity;
55
+ for (const step of STEPS) {
56
+ const diff = Math.abs(DEFAULT_L[step] - L);
57
+ if (diff < bestDiff) { bestDiff = diff; best = step; }
58
+ }
59
+ return best;
60
+ }
61
+
62
+ /**
63
+ * Gera a escala completa a partir de uma seed hex.
64
+ *
65
+ * @param {string} seedHex - cor primária, ex "#2563EB".
66
+ * @param {object} [opts]
67
+ * @param {number} [opts.chromaBoost=1] - multiplicador global de chroma.
68
+ * @returns {Record<number,string>} mapa step → hex.
69
+ */
70
+ export function generateBrandScale(seedHex, { chromaBoost = 1 } = {}) {
71
+ const seed = hexToOklch(seedHex);
72
+
73
+ // Ancora a seed no step de lightness mais próximo, para que o chroma
74
+ // e o hue relativos sejam medidos contra o step correto da rampa.
75
+ const anchorStep = nearestStepByLightness(seed.L);
76
+ const anchorC = DEFAULT_C[anchorStep];
77
+ const anchorH = DEFAULT_H[anchorStep];
78
+
79
+ const chromaScale = (seed.C / anchorC) * chromaBoost;
80
+ const hueShift = seed.h - anchorH;
81
+
82
+ const scale = {};
83
+ for (const step of STEPS) {
84
+ const L = DEFAULT_L[step];
85
+ const C = clamp(DEFAULT_C[step] * chromaScale, 0, 0.37);
86
+ let h = DEFAULT_H[step] + hueShift;
87
+ h = ((h % 360) + 360) % 360;
88
+ scale[step] = oklchToHex({ L, C, h });
89
+ }
90
+ return scale;
91
+ }
92
+
93
+ /**
94
+ * Escala baseada em hex explícitos por step (power users que já têm
95
+ * a rampa pronta). Retorna apenas os steps fornecidos, em uppercase.
96
+ */
97
+ export function scaleFromExplicit(partial) {
98
+ const out = {};
99
+ for (const step of STEPS) {
100
+ if (partial && partial[step]) out[step] = String(partial[step]).toUpperCase();
101
+ }
102
+ return out;
103
+ }
@@ -0,0 +1,76 @@
1
+ /* ============================================================
2
+ radius.js — presets de corner radius
3
+
4
+ Presets escalares (sharp → soft) multiplicam a escala Foundation.
5
+ Preset `soft` (2×): botões em pill; campos e containers internos
6
+ limitados a 1.5× (round); superfícies xl+ seguem 2×.
7
+ ============================================================ */
8
+
9
+ /** Steps Foundation escaláveis (px). */
10
+ export const RADIUS_STEPS = [2, 4, 8, 12, 16, 24];
11
+
12
+ /** Valor pill do DS (foundation.radius.999). */
13
+ export const RADIUS_PILL_VALUE = '999px';
14
+
15
+ /** Teto de radius para campos e containers no preset soft (1.5×). */
16
+ export const SOFT_INNER_CAP_PRESET = 'round';
17
+
18
+ /** Presets escalares (multiplicador sobre a escala default). */
19
+ export const RADIUS_PRESETS = {
20
+ sharp: 0,
21
+ tight: 0.5,
22
+ default: 1,
23
+ round: 1.5,
24
+ soft: 2,
25
+ };
26
+
27
+ /**
28
+ * Resolve preset escalar (string ou number).
29
+ */
30
+ export function resolveRadiusFactor(preset) {
31
+ if (typeof preset === 'number') return Math.max(0, preset);
32
+ return RADIUS_PRESETS[preset] ?? RADIUS_PRESETS.default;
33
+ }
34
+
35
+ /**
36
+ * Escala Foundation (rem) com multiplicador.
37
+ *
38
+ * @returns {Record<number,string>} step(px) → valor CSS.
39
+ */
40
+ export function generateRadiusScale(preset) {
41
+ const factor = resolveRadiusFactor(preset);
42
+ const out = {};
43
+ for (const step of RADIUS_STEPS) {
44
+ const px = step * factor;
45
+ out[step] = `${+(px / 16).toFixed(4)}rem`;
46
+ }
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * Tema completo de radius: Foundation + semantic + overrides Component.
52
+ *
53
+ * @returns {{
54
+ * foundation: Record<number,string>,
55
+ * semantic: Record<string,string>|null,
56
+ * component: Record<string,string>
57
+ * }}
58
+ */
59
+ export function generateRadiusTheme(preset) {
60
+ const foundation = generateRadiusScale(preset);
61
+ const component = {};
62
+ let semantic = null;
63
+
64
+ if (preset === 'soft') {
65
+ const inner = generateRadiusScale(SOFT_INNER_CAP_PRESET);
66
+ component['--ds-button-radius-default'] = RADIUS_PILL_VALUE;
67
+ semantic = {
68
+ sm: inner[4],
69
+ md: inner[8],
70
+ lg: inner[12],
71
+ xl: inner[16],
72
+ };
73
+ }
74
+
75
+ return { foundation, semantic, component };
76
+ }