@keboola/design 2.1.0 → 3.0.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,106 @@
1
+ 'use strict';
2
+
3
+ var react$1 = require('@testing-library/react');
4
+ var brandRegistry = require('@keboola/brand-registry');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/testing/renderWithTheme.tsx
9
+ var AIGradientsSvg = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "0", height: "0", style: { position: "absolute", visibility: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
10
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "aigradient-600", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
11
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#61E4FF" }),
12
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "50%", stopColor: "#1F8FFF" }),
13
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: "#7C5CFF" })
14
+ ] }),
15
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "aigradient-700", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
16
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#075fb8" }),
17
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: "#5b2cb8" })
18
+ ] })
19
+ ] }) });
20
+
21
+ // src/components/ThemeProvider/brandToCssVars.ts
22
+ var hexToRgbTriplet = (hex) => {
23
+ const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
24
+ if (!match) return null;
25
+ let body = match[1];
26
+ if (body.length === 3) {
27
+ body = body.split("").map((c) => c + c).join("");
28
+ }
29
+ const r = parseInt(body.slice(0, 2), 16);
30
+ const g = parseInt(body.slice(2, 4), 16);
31
+ const b = parseInt(body.slice(4, 6), 16);
32
+ return `${r} ${g} ${b}`;
33
+ };
34
+ var brandToCssVars = (brand) => {
35
+ const vars = {};
36
+ const colorMap = [
37
+ ["--color-background", brand.colors.background],
38
+ ["--color-foreground", brand.colors.foreground],
39
+ ["--color-muted", brand.colors.muted],
40
+ ["--color-muted-foreground", brand.colors.mutedForeground],
41
+ ["--color-border", brand.colors.border],
42
+ ["--color-primary", brand.colors.primary],
43
+ ["--color-primary-foreground", brand.colors.primaryForeground],
44
+ ["--color-destructive", brand.colors.destructive],
45
+ ["--color-destructive-foreground", brand.colors.destructiveForeground]
46
+ ];
47
+ for (const [name, hex] of colorMap) {
48
+ const triplet = hexToRgbTriplet(hex);
49
+ if (triplet !== null) vars[name] = triplet;
50
+ }
51
+ if (brand.radii) {
52
+ vars["--radius-sm"] = brand.radii.sm;
53
+ vars["--radius-md"] = brand.radii.md;
54
+ vars["--radius-lg"] = brand.radii.lg;
55
+ }
56
+ return vars;
57
+ };
58
+ var ThemeContext = react.createContext(null);
59
+ var ThemeProvider = ({ brand = brandRegistry.keboola, setBrand, children }) => {
60
+ react.useEffect(() => {
61
+ if (typeof document === "undefined") return;
62
+ const root = document.documentElement;
63
+ const previousBrand = root.getAttribute("data-brand");
64
+ const vars = brandToCssVars(brand);
65
+ const previousValues = {};
66
+ for (const name of Object.keys(vars)) {
67
+ previousValues[name] = root.style.getPropertyValue(name);
68
+ }
69
+ root.setAttribute("data-brand", brand.id);
70
+ for (const [name, value] of Object.entries(vars)) {
71
+ root.style.setProperty(name, value);
72
+ }
73
+ return () => {
74
+ for (const [name, previousValue] of Object.entries(previousValues)) {
75
+ if (root.style.getPropertyValue(name) !== vars[name]) continue;
76
+ if (previousValue === "") {
77
+ root.style.removeProperty(name);
78
+ } else {
79
+ root.style.setProperty(name, previousValue);
80
+ }
81
+ }
82
+ if (root.getAttribute("data-brand") === brand.id) {
83
+ if (previousBrand === null) {
84
+ root.removeAttribute("data-brand");
85
+ } else {
86
+ root.setAttribute("data-brand", previousBrand);
87
+ }
88
+ }
89
+ };
90
+ }, [brand]);
91
+ return /* @__PURE__ */ jsxRuntime.jsxs(ThemeContext, { value: { brand, setBrand }, children: [
92
+ /* @__PURE__ */ jsxRuntime.jsx(AIGradientsSvg, {}),
93
+ children
94
+ ] });
95
+ };
96
+ var renderWithTheme = (ui, options = {}) => {
97
+ const { brand = brandRegistry.keboola, ...rest } = options;
98
+ return react$1.render(ui, {
99
+ ...rest,
100
+ wrapper: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx(ThemeProvider, { brand, children })
101
+ });
102
+ };
103
+
104
+ exports.renderWithTheme = renderWithTheme;
105
+ //# sourceMappingURL=testing.cjs.map
106
+ //# sourceMappingURL=testing.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ThemeProvider/AIGradientSvg.tsx","../src/components/ThemeProvider/brandToCssVars.ts","../src/components/ThemeProvider/themeContext.ts","../src/components/ThemeProvider/ThemeProvider.tsx","../src/testing/renderWithTheme.tsx"],"names":["jsx","jsxs","createContext","keboola","useEffect","render"],"mappings":"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","file":"testing.cjs","sourcesContent":["export const AIGradientsSvg = () => (\n <svg width=\"0\" height=\"0\" style={{ position: 'absolute', visibility: 'hidden' }}>\n <defs>\n {/*aigradient-600*/}\n <linearGradient id=\"aigradient-600\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stopColor=\"#61E4FF\" />\n <stop offset=\"50%\" stopColor=\"#1F8FFF\" />\n <stop offset=\"100%\" stopColor=\"#7C5CFF\" />\n </linearGradient>\n\n {/*aigradient-700*/}\n <linearGradient id=\"aigradient-700\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stopColor=\"#075fb8\" />\n <stop offset=\"100%\" stopColor=\"#5b2cb8\" />\n </linearGradient>\n </defs>\n </svg>\n);\n","import type { Brand } from '@keboola/brand-registry';\n\n/**\n * Convert a `#RRGGBB` (or `#RGB`) string to a `R G B` triplet (space-separated).\n *\n * The canonical `:root` block in `styles.css` stores `--color-*` values as\n * space-separated channel values so Tailwind's `rgb(var(--color-*) / <alpha-value>)`\n * helper can compose the alpha modifier. Returns `null` for any input that\n * isn't a recognized hex string — callers should fall back to leaving the\n * slot untouched rather than injecting an invalid value.\n */\nconst hexToRgbTriplet = (hex: string): string | null => {\n const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());\n if (!match) return null;\n\n let body = match[1]!;\n if (body.length === 3) {\n body = body\n .split('')\n .map((c) => c + c)\n .join('');\n }\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n return `${r} ${g} ${b}`;\n};\n\n/**\n * Map every brand-overridable slot to the matching CSS variable on `:root`.\n *\n * Brands today only expose a subset of the semantic surface shipped by\n * `@keboola/design/styles` (`accent`, `popover`, `sidebar.*`, `success`,\n * `input` are not yet brand-overridable). Slots the brand doesn't define\n * are omitted — `ThemeProvider` only captures/restores the keys returned\n * here, so unset slots inherit whatever is already live on `<html>` (an\n * outer provider's value, or the `:root` defaults in `styles.css` if no\n * outer provider has touched them). This cascades the way CSS does: an\n * inner brand without `radii` keeps the outer's radii rather than\n * snapping back to `:root`.\n */\nexport const brandToCssVars = (brand: Brand): Record<string, string> => {\n const vars: Record<string, string> = {};\n\n const colorMap: [string, string][] = [\n ['--color-background', brand.colors.background],\n ['--color-foreground', brand.colors.foreground],\n ['--color-muted', brand.colors.muted],\n ['--color-muted-foreground', brand.colors.mutedForeground],\n ['--color-border', brand.colors.border],\n ['--color-primary', brand.colors.primary],\n ['--color-primary-foreground', brand.colors.primaryForeground],\n ['--color-destructive', brand.colors.destructive],\n ['--color-destructive-foreground', brand.colors.destructiveForeground],\n ];\n for (const [name, hex] of colorMap) {\n const triplet = hexToRgbTriplet(hex);\n if (triplet !== null) vars[name] = triplet;\n }\n\n if (brand.radii) {\n vars['--radius-sm'] = brand.radii.sm;\n vars['--radius-md'] = brand.radii.md;\n vars['--radius-lg'] = brand.radii.lg;\n }\n\n return vars;\n};\n","import { createContext } from 'react';\n\nimport type { Brand } from '@keboola/brand-registry';\n\nexport type ThemeContextValue = {\n brand: Brand;\n setBrand?: (brand: Brand) => void;\n};\n\nexport const ThemeContext = createContext<ThemeContextValue | null>(null);\n","import { type ReactNode, useEffect } from 'react';\n\nimport { type Brand, keboola } from '@keboola/brand-registry';\n\nimport { AIGradientsSvg } from './AIGradientSvg';\nimport { brandToCssVars } from './brandToCssVars';\nimport { ThemeContext } from './themeContext';\n\ntype Props = {\n /**\n * Brand whose tokens drive the chrome (`@keboola/design`'s semantic CSS\n * variables) inside this provider's subtree. Defaults to the canonical\n * `keboola` brand so existing apps that mount `<ThemeProvider>` without a\n * `brand` prop continue to render as today.\n */\n brand?: Brand;\n /**\n * Optional setter exposed through `useTheme()` so consumers can flip\n * brands at runtime (Storybook toolbar, a customer-impersonation switch).\n */\n setBrand?: (brand: Brand) => void;\n children: ReactNode;\n};\n\n/**\n * Responsibilities:\n * - Render hidden SVG gradient definitions (`aigradient-600`, `aigradient-700`).\n * - Set `data-brand=\"<brand.id>\"` on `<html>` and inject the matching `--color-*` /\n * `--radius-*` CSS variables so chrome re-skins for the supplied brand.\n * - Expose `{ brand, setBrand? }` via `useTheme()` (throws outside the provider).\n *\n * The slots a brand can override today are a subset of the canonical\n * `:root` block in `styles.css`. Slots the brand doesn't define fall back\n * to the `:root` defaults — we deliberately do not write empty strings.\n *\n * `data-brand` (brand id) is intentionally separate from `data-theme`\n * (light/dark mode, defined by the `[data-theme='dark']` block in\n * `styles.css`) so the two axes compose without clobbering each other.\n */\nexport const ThemeProvider = ({ brand = keboola, setBrand, children }: Props) => {\n useEffect(() => {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n\n // Capture the previous `data-brand` + per-property inline-style values\n // so cleanup can restore them. Cleanup only restores values that\n // weren't overwritten by some other provider in between — without that\n // check, an inner provider unmounting after an outer one wrote new\n // values would clobber the outer's brand back to bare `:root` (React\n // runs child effects before parent effects on mount, so the inner's\n // captured \"previous\" is stale by the time it unmounts).\n const previousBrand = root.getAttribute('data-brand');\n const vars = brandToCssVars(brand);\n const previousValues: Record<string, string> = {};\n for (const name of Object.keys(vars)) {\n previousValues[name] = root.style.getPropertyValue(name);\n }\n\n root.setAttribute('data-brand', brand.id);\n for (const [name, value] of Object.entries(vars)) {\n root.style.setProperty(name, value);\n }\n\n return () => {\n for (const [name, previousValue] of Object.entries(previousValues)) {\n // Only roll back if our value is still live — if someone else\n // (an outer provider, a sibling) has since written a different\n // value, leave it alone.\n if (root.style.getPropertyValue(name) !== vars[name]) continue;\n if (previousValue === '') {\n root.style.removeProperty(name);\n } else {\n root.style.setProperty(name, previousValue);\n }\n }\n if (root.getAttribute('data-brand') === brand.id) {\n if (previousBrand === null) {\n root.removeAttribute('data-brand');\n } else {\n root.setAttribute('data-brand', previousBrand);\n }\n }\n };\n }, [brand]);\n\n return (\n <ThemeContext value={{ brand, setBrand }}>\n <AIGradientsSvg />\n {children}\n </ThemeContext>\n );\n};\n","import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\n\nimport { type Brand, keboola } from '@keboola/brand-registry';\n\nimport { ThemeProvider } from '../components/ThemeProvider';\n\ntype Options = Omit<RenderOptions, 'wrapper'> & {\n /** Brand to drive the wrapping `<ThemeProvider>`. Defaults to `keboola`. */\n brand?: Brand;\n};\n\n/**\n * `@testing-library/react` `render` wrapped in a `<ThemeProvider brand={...}>`.\n *\n * Use in unit tests for any component that calls `useTheme()` or relies on\n * the brand-driven CSS variables. Defaults to the canonical Keboola brand so\n * callers can opt into a specific brand only when they need to assert\n * brand-dependent behaviour (e.g. chrome re-skins under `keboola-legacy`).\n */\nexport const renderWithTheme = (ui: ReactElement, options: Options = {}): RenderResult => {\n const { brand = keboola, ...rest } = options;\n return render(ui, {\n ...rest,\n wrapper: ({ children }) => <ThemeProvider brand={brand}>{children}</ThemeProvider>,\n });\n};\n"]}
@@ -0,0 +1,19 @@
1
+ import { ReactElement } from 'react';
2
+ import { RenderOptions, RenderResult } from '@testing-library/react';
3
+ import { Brand } from '@keboola/brand-registry';
4
+
5
+ type Options = Omit<RenderOptions, 'wrapper'> & {
6
+ /** Brand to drive the wrapping `<ThemeProvider>`. Defaults to `keboola`. */
7
+ brand?: Brand;
8
+ };
9
+ /**
10
+ * `@testing-library/react` `render` wrapped in a `<ThemeProvider brand={...}>`.
11
+ *
12
+ * Use in unit tests for any component that calls `useTheme()` or relies on
13
+ * the brand-driven CSS variables. Defaults to the canonical Keboola brand so
14
+ * callers can opt into a specific brand only when they need to assert
15
+ * brand-dependent behaviour (e.g. chrome re-skins under `keboola-legacy`).
16
+ */
17
+ declare const renderWithTheme: (ui: ReactElement, options?: Options) => RenderResult;
18
+
19
+ export { renderWithTheme };
@@ -0,0 +1,19 @@
1
+ import { ReactElement } from 'react';
2
+ import { RenderOptions, RenderResult } from '@testing-library/react';
3
+ import { Brand } from '@keboola/brand-registry';
4
+
5
+ type Options = Omit<RenderOptions, 'wrapper'> & {
6
+ /** Brand to drive the wrapping `<ThemeProvider>`. Defaults to `keboola`. */
7
+ brand?: Brand;
8
+ };
9
+ /**
10
+ * `@testing-library/react` `render` wrapped in a `<ThemeProvider brand={...}>`.
11
+ *
12
+ * Use in unit tests for any component that calls `useTheme()` or relies on
13
+ * the brand-driven CSS variables. Defaults to the canonical Keboola brand so
14
+ * callers can opt into a specific brand only when they need to assert
15
+ * brand-dependent behaviour (e.g. chrome re-skins under `keboola-legacy`).
16
+ */
17
+ declare const renderWithTheme: (ui: ReactElement, options?: Options) => RenderResult;
18
+
19
+ export { renderWithTheme };
@@ -0,0 +1,104 @@
1
+ import { render } from '@testing-library/react';
2
+ import { keboola } from '@keboola/brand-registry';
3
+ import { createContext, useEffect } from 'react';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+
6
+ // src/testing/renderWithTheme.tsx
7
+ var AIGradientsSvg = () => /* @__PURE__ */ jsx("svg", { width: "0", height: "0", style: { position: "absolute", visibility: "hidden" }, children: /* @__PURE__ */ jsxs("defs", { children: [
8
+ /* @__PURE__ */ jsxs("linearGradient", { id: "aigradient-600", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
9
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: "#61E4FF" }),
10
+ /* @__PURE__ */ jsx("stop", { offset: "50%", stopColor: "#1F8FFF" }),
11
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: "#7C5CFF" })
12
+ ] }),
13
+ /* @__PURE__ */ jsxs("linearGradient", { id: "aigradient-700", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [
14
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: "#075fb8" }),
15
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: "#5b2cb8" })
16
+ ] })
17
+ ] }) });
18
+
19
+ // src/components/ThemeProvider/brandToCssVars.ts
20
+ var hexToRgbTriplet = (hex) => {
21
+ const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
22
+ if (!match) return null;
23
+ let body = match[1];
24
+ if (body.length === 3) {
25
+ body = body.split("").map((c) => c + c).join("");
26
+ }
27
+ const r = parseInt(body.slice(0, 2), 16);
28
+ const g = parseInt(body.slice(2, 4), 16);
29
+ const b = parseInt(body.slice(4, 6), 16);
30
+ return `${r} ${g} ${b}`;
31
+ };
32
+ var brandToCssVars = (brand) => {
33
+ const vars = {};
34
+ const colorMap = [
35
+ ["--color-background", brand.colors.background],
36
+ ["--color-foreground", brand.colors.foreground],
37
+ ["--color-muted", brand.colors.muted],
38
+ ["--color-muted-foreground", brand.colors.mutedForeground],
39
+ ["--color-border", brand.colors.border],
40
+ ["--color-primary", brand.colors.primary],
41
+ ["--color-primary-foreground", brand.colors.primaryForeground],
42
+ ["--color-destructive", brand.colors.destructive],
43
+ ["--color-destructive-foreground", brand.colors.destructiveForeground]
44
+ ];
45
+ for (const [name, hex] of colorMap) {
46
+ const triplet = hexToRgbTriplet(hex);
47
+ if (triplet !== null) vars[name] = triplet;
48
+ }
49
+ if (brand.radii) {
50
+ vars["--radius-sm"] = brand.radii.sm;
51
+ vars["--radius-md"] = brand.radii.md;
52
+ vars["--radius-lg"] = brand.radii.lg;
53
+ }
54
+ return vars;
55
+ };
56
+ var ThemeContext = createContext(null);
57
+ var ThemeProvider = ({ brand = keboola, setBrand, children }) => {
58
+ useEffect(() => {
59
+ if (typeof document === "undefined") return;
60
+ const root = document.documentElement;
61
+ const previousBrand = root.getAttribute("data-brand");
62
+ const vars = brandToCssVars(brand);
63
+ const previousValues = {};
64
+ for (const name of Object.keys(vars)) {
65
+ previousValues[name] = root.style.getPropertyValue(name);
66
+ }
67
+ root.setAttribute("data-brand", brand.id);
68
+ for (const [name, value] of Object.entries(vars)) {
69
+ root.style.setProperty(name, value);
70
+ }
71
+ return () => {
72
+ for (const [name, previousValue] of Object.entries(previousValues)) {
73
+ if (root.style.getPropertyValue(name) !== vars[name]) continue;
74
+ if (previousValue === "") {
75
+ root.style.removeProperty(name);
76
+ } else {
77
+ root.style.setProperty(name, previousValue);
78
+ }
79
+ }
80
+ if (root.getAttribute("data-brand") === brand.id) {
81
+ if (previousBrand === null) {
82
+ root.removeAttribute("data-brand");
83
+ } else {
84
+ root.setAttribute("data-brand", previousBrand);
85
+ }
86
+ }
87
+ };
88
+ }, [brand]);
89
+ return /* @__PURE__ */ jsxs(ThemeContext, { value: { brand, setBrand }, children: [
90
+ /* @__PURE__ */ jsx(AIGradientsSvg, {}),
91
+ children
92
+ ] });
93
+ };
94
+ var renderWithTheme = (ui, options = {}) => {
95
+ const { brand = keboola, ...rest } = options;
96
+ return render(ui, {
97
+ ...rest,
98
+ wrapper: ({ children }) => /* @__PURE__ */ jsx(ThemeProvider, { brand, children })
99
+ });
100
+ };
101
+
102
+ export { renderWithTheme };
103
+ //# sourceMappingURL=testing.js.map
104
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ThemeProvider/AIGradientSvg.tsx","../src/components/ThemeProvider/brandToCssVars.ts","../src/components/ThemeProvider/themeContext.ts","../src/components/ThemeProvider/ThemeProvider.tsx","../src/testing/renderWithTheme.tsx"],"names":["jsxs","jsx","keboola"],"mappings":"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","file":"testing.js","sourcesContent":["export const AIGradientsSvg = () => (\n <svg width=\"0\" height=\"0\" style={{ position: 'absolute', visibility: 'hidden' }}>\n <defs>\n {/*aigradient-600*/}\n <linearGradient id=\"aigradient-600\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stopColor=\"#61E4FF\" />\n <stop offset=\"50%\" stopColor=\"#1F8FFF\" />\n <stop offset=\"100%\" stopColor=\"#7C5CFF\" />\n </linearGradient>\n\n {/*aigradient-700*/}\n <linearGradient id=\"aigradient-700\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stopColor=\"#075fb8\" />\n <stop offset=\"100%\" stopColor=\"#5b2cb8\" />\n </linearGradient>\n </defs>\n </svg>\n);\n","import type { Brand } from '@keboola/brand-registry';\n\n/**\n * Convert a `#RRGGBB` (or `#RGB`) string to a `R G B` triplet (space-separated).\n *\n * The canonical `:root` block in `styles.css` stores `--color-*` values as\n * space-separated channel values so Tailwind's `rgb(var(--color-*) / <alpha-value>)`\n * helper can compose the alpha modifier. Returns `null` for any input that\n * isn't a recognized hex string — callers should fall back to leaving the\n * slot untouched rather than injecting an invalid value.\n */\nconst hexToRgbTriplet = (hex: string): string | null => {\n const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());\n if (!match) return null;\n\n let body = match[1]!;\n if (body.length === 3) {\n body = body\n .split('')\n .map((c) => c + c)\n .join('');\n }\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n return `${r} ${g} ${b}`;\n};\n\n/**\n * Map every brand-overridable slot to the matching CSS variable on `:root`.\n *\n * Brands today only expose a subset of the semantic surface shipped by\n * `@keboola/design/styles` (`accent`, `popover`, `sidebar.*`, `success`,\n * `input` are not yet brand-overridable). Slots the brand doesn't define\n * are omitted — `ThemeProvider` only captures/restores the keys returned\n * here, so unset slots inherit whatever is already live on `<html>` (an\n * outer provider's value, or the `:root` defaults in `styles.css` if no\n * outer provider has touched them). This cascades the way CSS does: an\n * inner brand without `radii` keeps the outer's radii rather than\n * snapping back to `:root`.\n */\nexport const brandToCssVars = (brand: Brand): Record<string, string> => {\n const vars: Record<string, string> = {};\n\n const colorMap: [string, string][] = [\n ['--color-background', brand.colors.background],\n ['--color-foreground', brand.colors.foreground],\n ['--color-muted', brand.colors.muted],\n ['--color-muted-foreground', brand.colors.mutedForeground],\n ['--color-border', brand.colors.border],\n ['--color-primary', brand.colors.primary],\n ['--color-primary-foreground', brand.colors.primaryForeground],\n ['--color-destructive', brand.colors.destructive],\n ['--color-destructive-foreground', brand.colors.destructiveForeground],\n ];\n for (const [name, hex] of colorMap) {\n const triplet = hexToRgbTriplet(hex);\n if (triplet !== null) vars[name] = triplet;\n }\n\n if (brand.radii) {\n vars['--radius-sm'] = brand.radii.sm;\n vars['--radius-md'] = brand.radii.md;\n vars['--radius-lg'] = brand.radii.lg;\n }\n\n return vars;\n};\n","import { createContext } from 'react';\n\nimport type { Brand } from '@keboola/brand-registry';\n\nexport type ThemeContextValue = {\n brand: Brand;\n setBrand?: (brand: Brand) => void;\n};\n\nexport const ThemeContext = createContext<ThemeContextValue | null>(null);\n","import { type ReactNode, useEffect } from 'react';\n\nimport { type Brand, keboola } from '@keboola/brand-registry';\n\nimport { AIGradientsSvg } from './AIGradientSvg';\nimport { brandToCssVars } from './brandToCssVars';\nimport { ThemeContext } from './themeContext';\n\ntype Props = {\n /**\n * Brand whose tokens drive the chrome (`@keboola/design`'s semantic CSS\n * variables) inside this provider's subtree. Defaults to the canonical\n * `keboola` brand so existing apps that mount `<ThemeProvider>` without a\n * `brand` prop continue to render as today.\n */\n brand?: Brand;\n /**\n * Optional setter exposed through `useTheme()` so consumers can flip\n * brands at runtime (Storybook toolbar, a customer-impersonation switch).\n */\n setBrand?: (brand: Brand) => void;\n children: ReactNode;\n};\n\n/**\n * Responsibilities:\n * - Render hidden SVG gradient definitions (`aigradient-600`, `aigradient-700`).\n * - Set `data-brand=\"<brand.id>\"` on `<html>` and inject the matching `--color-*` /\n * `--radius-*` CSS variables so chrome re-skins for the supplied brand.\n * - Expose `{ brand, setBrand? }` via `useTheme()` (throws outside the provider).\n *\n * The slots a brand can override today are a subset of the canonical\n * `:root` block in `styles.css`. Slots the brand doesn't define fall back\n * to the `:root` defaults — we deliberately do not write empty strings.\n *\n * `data-brand` (brand id) is intentionally separate from `data-theme`\n * (light/dark mode, defined by the `[data-theme='dark']` block in\n * `styles.css`) so the two axes compose without clobbering each other.\n */\nexport const ThemeProvider = ({ brand = keboola, setBrand, children }: Props) => {\n useEffect(() => {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n\n // Capture the previous `data-brand` + per-property inline-style values\n // so cleanup can restore them. Cleanup only restores values that\n // weren't overwritten by some other provider in between — without that\n // check, an inner provider unmounting after an outer one wrote new\n // values would clobber the outer's brand back to bare `:root` (React\n // runs child effects before parent effects on mount, so the inner's\n // captured \"previous\" is stale by the time it unmounts).\n const previousBrand = root.getAttribute('data-brand');\n const vars = brandToCssVars(brand);\n const previousValues: Record<string, string> = {};\n for (const name of Object.keys(vars)) {\n previousValues[name] = root.style.getPropertyValue(name);\n }\n\n root.setAttribute('data-brand', brand.id);\n for (const [name, value] of Object.entries(vars)) {\n root.style.setProperty(name, value);\n }\n\n return () => {\n for (const [name, previousValue] of Object.entries(previousValues)) {\n // Only roll back if our value is still live — if someone else\n // (an outer provider, a sibling) has since written a different\n // value, leave it alone.\n if (root.style.getPropertyValue(name) !== vars[name]) continue;\n if (previousValue === '') {\n root.style.removeProperty(name);\n } else {\n root.style.setProperty(name, previousValue);\n }\n }\n if (root.getAttribute('data-brand') === brand.id) {\n if (previousBrand === null) {\n root.removeAttribute('data-brand');\n } else {\n root.setAttribute('data-brand', previousBrand);\n }\n }\n };\n }, [brand]);\n\n return (\n <ThemeContext value={{ brand, setBrand }}>\n <AIGradientsSvg />\n {children}\n </ThemeContext>\n );\n};\n","import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\n\nimport { type Brand, keboola } from '@keboola/brand-registry';\n\nimport { ThemeProvider } from '../components/ThemeProvider';\n\ntype Options = Omit<RenderOptions, 'wrapper'> & {\n /** Brand to drive the wrapping `<ThemeProvider>`. Defaults to `keboola`. */\n brand?: Brand;\n};\n\n/**\n * `@testing-library/react` `render` wrapped in a `<ThemeProvider brand={...}>`.\n *\n * Use in unit tests for any component that calls `useTheme()` or relies on\n * the brand-driven CSS variables. Defaults to the canonical Keboola brand so\n * callers can opt into a specific brand only when they need to assert\n * brand-dependent behaviour (e.g. chrome re-skins under `keboola-legacy`).\n */\nexport const renderWithTheme = (ui: ReactElement, options: Options = {}): RenderResult => {\n const { brand = keboola, ...rest } = options;\n return render(ui, {\n ...rest,\n wrapper: ({ children }) => <ThemeProvider brand={brand}>{children}</ThemeProvider>,\n });\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keboola/design",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Keboola UI platform design system — shadcn-aligned React primitives, Keboola-branded blocks, and the form bindings built on TanStack Form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,8 +21,8 @@
21
21
  "icons": [
22
22
  "./dist/icons.d.ts"
23
23
  ],
24
- "icon-names": [
25
- "./dist/icon-names.d.ts"
24
+ "testing": [
25
+ "./dist/testing.d.ts"
26
26
  ]
27
27
  }
28
28
  },
@@ -69,14 +69,14 @@
69
69
  "default": "./dist/icons.cjs"
70
70
  }
71
71
  },
72
- "./icon-names": {
72
+ "./testing": {
73
73
  "import": {
74
- "types": "./dist/icon-names.d.ts",
75
- "default": "./dist/icon-names.js"
74
+ "types": "./dist/testing.d.ts",
75
+ "default": "./dist/testing.js"
76
76
  },
77
77
  "require": {
78
- "types": "./dist/icon-names.d.cts",
79
- "default": "./dist/icon-names.cjs"
78
+ "types": "./dist/testing.d.cts",
79
+ "default": "./dist/testing.cjs"
80
80
  }
81
81
  }
82
82
  },
@@ -115,15 +115,16 @@
115
115
  "build:css": "tailwindcss --postcss -c tailwind.config.js -i src/styles.css -o dist/styles.css --minify",
116
116
  "build:check": "attw --pack . --exclude-entrypoints ./styles ./styles/compiled",
117
117
  "prepublishOnly": "yarn build && yarn build:check",
118
- "test": "vitest run --passWithNoTests --reporter=dot",
119
- "test:stories": "vitest run --config vitest.storybook.config.ts",
120
- "test:stories:coverage": "vitest run --config vitest.storybook.config.ts --coverage",
121
- "lint": "yarn lint:format && yarn lint:oxlint",
118
+ "test": "vitest run --config vite.config.ts --passWithNoTests --reporter=dot",
119
+ "test:stories": "vitest run --config vitest.config.ts",
120
+ "test:stories:coverage": "vitest run --config vitest.config.ts --coverage",
121
+ "lint": "yarn lint:format && yarn lint:oxlint && yarn lint:design",
122
122
  "lint:fix": "oxlint --fix . && oxfmt",
123
123
  "knip:check": "yarn --cwd ../.. exec knip --workspace packages/design",
124
124
  "knip:fix": "yarn --cwd ../.. exec knip --workspace packages/design --fix-type types,exports,files",
125
125
  "lint:format": "oxfmt --check",
126
126
  "lint:oxlint": "oxlint . --quiet",
127
+ "lint:design": "designmd lint DESIGN.md",
127
128
  "type-check": "tsc"
128
129
  },
129
130
  "engines": {
@@ -131,6 +132,7 @@
131
132
  },
132
133
  "devDependencies": {
133
134
  "@arethetypeswrong/cli": "^0.18.2",
135
+ "@google/design.md": "0.2.0",
134
136
  "@keboola/oxlint-config": "workspace:^",
135
137
  "@keboola/postcss-config": "workspace:^",
136
138
  "@keboola/tsconfig": "workspace:^",
@@ -157,8 +159,15 @@
157
159
  "vite": "^8.0.16",
158
160
  "vitest": "^4.1.8"
159
161
  },
162
+ "peerDependencies": {
163
+ "@testing-library/react": "^16.3.2"
164
+ },
165
+ "peerDependenciesMeta": {
166
+ "@testing-library/react": {
167
+ "optional": true
168
+ }
169
+ },
160
170
  "dependencies": {
161
- "@awesome.me/kit-be8bcf7e5e": "1.0.223",
162
171
  "@base-ui/react": "1.5.0",
163
172
  "@codemirror/autocomplete": "^6.20.1",
164
173
  "@codemirror/commands": "^6.10.3",
@@ -172,8 +181,7 @@
172
181
  "@codemirror/merge": "^6.12.1",
173
182
  "@codemirror/state": "^6.6.0",
174
183
  "@codemirror/view": "^6.41.0",
175
- "@fortawesome/fontawesome-svg-core": "^6.7.2",
176
- "@fortawesome/react-fontawesome": "^0.2.2",
184
+ "@keboola/brand-registry": "^0.1.2",
177
185
  "@keboola/codemirror-lang-sfsql": "^1.2.0",
178
186
  "@keboola/codemirror-lang-sql": "^0.2.0",
179
187
  "@keboola/tailwind-config": "^0.1.1",
@@ -222,6 +230,7 @@
222
230
  "react": "^19.2.6",
223
231
  "react-day-picker": "^9.13.0",
224
232
  "react-dom": "^19.2.6",
233
+ "react-dropzone": "^14.2.3",
225
234
  "react-resizable-panels": "^3.0.4",
226
235
  "react-textarea-autosize": "^8.5.9",
227
236
  "sonner": "^2.0.7",
@@ -1,18 +0,0 @@
1
- 'use strict';
2
-
3
- var icons = require('@awesome.me/kit-be8bcf7e5e/icons');
4
-
5
- // src/icon-names.ts
6
- var getAvailableIconNames = () => {
7
- const names = /* @__PURE__ */ new Set();
8
- for (const def of Object.values(icons.fas)) {
9
- if (def !== null && typeof def === "object" && "iconName" in def && typeof def.iconName === "string") {
10
- names.add(def.iconName);
11
- }
12
- }
13
- return [...names].sort();
14
- };
15
-
16
- exports.getAvailableIconNames = getAvailableIconNames;
17
- //# sourceMappingURL=icon-names.cjs.map
18
- //# sourceMappingURL=icon-names.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/icon-names.ts"],"names":["fas"],"mappings":";;;;;AAMO,IAAM,wBAAwB,MAAgB;AACnD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,MAAA,CAAOA,SAAG,CAAA,EAAG;AACpC,IAAA,IACE,GAAA,KAAQ,IAAA,IACR,OAAO,GAAA,KAAQ,QAAA,IACf,cAAc,GAAA,IACd,OAAO,GAAA,CAAI,QAAA,KAAa,QAAA,EACxB;AACA,MAAA,KAAA,CAAM,GAAA,CAAI,IAAI,QAAQ,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,OAAO,CAAC,GAAG,KAAK,CAAA,CAAE,IAAA,EAAK;AACzB","file":"icon-names.cjs","sourcesContent":["import { fas } from '@awesome.me/kit-be8bcf7e5e/icons';\n\n/**\n * Reads available FontAwesome solid icon names from the FA kit.\n * Returns deduplicated, sorted kebab-case icon names (e.g. \"chart-bar\", \"circle-check\").\n */\nexport const getAvailableIconNames = (): string[] => {\n const names = new Set<string>();\n for (const def of Object.values(fas)) {\n if (\n def !== null &&\n typeof def === 'object' &&\n 'iconName' in def &&\n typeof def.iconName === 'string'\n ) {\n names.add(def.iconName);\n }\n }\n return [...names].sort();\n};\n"]}
@@ -1,7 +0,0 @@
1
- /**
2
- * Reads available FontAwesome solid icon names from the FA kit.
3
- * Returns deduplicated, sorted kebab-case icon names (e.g. "chart-bar", "circle-check").
4
- */
5
- declare const getAvailableIconNames: () => string[];
6
-
7
- export { getAvailableIconNames };
@@ -1,7 +0,0 @@
1
- /**
2
- * Reads available FontAwesome solid icon names from the FA kit.
3
- * Returns deduplicated, sorted kebab-case icon names (e.g. "chart-bar", "circle-check").
4
- */
5
- declare const getAvailableIconNames: () => string[];
6
-
7
- export { getAvailableIconNames };
@@ -1,16 +0,0 @@
1
- import { fas } from '@awesome.me/kit-be8bcf7e5e/icons';
2
-
3
- // src/icon-names.ts
4
- var getAvailableIconNames = () => {
5
- const names = /* @__PURE__ */ new Set();
6
- for (const def of Object.values(fas)) {
7
- if (def !== null && typeof def === "object" && "iconName" in def && typeof def.iconName === "string") {
8
- names.add(def.iconName);
9
- }
10
- }
11
- return [...names].sort();
12
- };
13
-
14
- export { getAvailableIconNames };
15
- //# sourceMappingURL=icon-names.js.map
16
- //# sourceMappingURL=icon-names.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/icon-names.ts"],"names":[],"mappings":";;;AAMO,IAAM,wBAAwB,MAAgB;AACnD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,MAAA,CAAO,GAAG,CAAA,EAAG;AACpC,IAAA,IACE,GAAA,KAAQ,IAAA,IACR,OAAO,GAAA,KAAQ,QAAA,IACf,cAAc,GAAA,IACd,OAAO,GAAA,CAAI,QAAA,KAAa,QAAA,EACxB;AACA,MAAA,KAAA,CAAM,GAAA,CAAI,IAAI,QAAQ,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,OAAO,CAAC,GAAG,KAAK,CAAA,CAAE,IAAA,EAAK;AACzB","file":"icon-names.js","sourcesContent":["import { fas } from '@awesome.me/kit-be8bcf7e5e/icons';\n\n/**\n * Reads available FontAwesome solid icon names from the FA kit.\n * Returns deduplicated, sorted kebab-case icon names (e.g. \"chart-bar\", \"circle-check\").\n */\nexport const getAvailableIconNames = (): string[] => {\n const names = new Set<string>();\n for (const def of Object.values(fas)) {\n if (\n def !== null &&\n typeof def === 'object' &&\n 'iconName' in def &&\n typeof def.iconName === 'string'\n ) {\n names.add(def.iconName);\n }\n }\n return [...names].sort();\n};\n"]}