@keboola/design 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.cjs CHANGED
@@ -19,35 +19,18 @@ var AIGradientsSvg = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "0", h
19
19
  ] }) });
20
20
 
21
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
22
  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
- }
23
+ const vars = {
24
+ "--color-background": brand.colors.background,
25
+ "--color-foreground": brand.colors.foreground,
26
+ "--color-muted": brand.colors.muted,
27
+ "--color-muted-foreground": brand.colors.mutedForeground,
28
+ "--color-border": brand.colors.border,
29
+ "--color-primary": brand.colors.primary,
30
+ "--color-primary-foreground": brand.colors.primaryForeground,
31
+ "--color-destructive": brand.colors.destructive,
32
+ "--color-destructive-foreground": brand.colors.destructiveForeground
33
+ };
51
34
  if (brand.radii) {
52
35
  vars["--radius-sm"] = brand.radii.sm;
53
36
  vars["--radius-md"] = brand.radii.md;
@@ -1 +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":";;;;;;;;AAAO,IAAM,iBAAiB,sBAC5BA,cAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,KAAI,MAAA,EAAO,GAAA,EAAI,KAAA,EAAO,EAAE,UAAU,UAAA,EAAY,UAAA,EAAY,QAAA,EAAS,EAC5E,0CAAC,MAAA,EAAA,EAEC,QAAA,EAAA;AAAA,kBAAAC,eAAA,CAAC,gBAAA,EAAA,EAAe,EAAA,EAAG,gBAAA,EAAiB,EAAA,EAAG,IAAA,EAAK,IAAG,IAAA,EAAK,EAAA,EAAG,MAAA,EAAO,EAAA,EAAG,MAAA,EAC/D,QAAA,EAAA;AAAA,oBAAAD,cAAA,CAAC,MAAA,EAAA,EAAK,MAAA,EAAO,IAAA,EAAK,SAAA,EAAU,SAAA,EAAU,CAAA;AAAA,oBACtCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAA,EAAO,KAAA,EAAM,WAAU,SAAA,EAAU,CAAA;AAAA,oBACvCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAA,EAAO,MAAA,EAAO,WAAU,SAAA,EAAU;AAAA,GAAA,EAC1C,CAAA;AAAA,kBAGAC,eAAA,CAAC,gBAAA,EAAA,EAAe,EAAA,EAAG,gBAAA,EAAiB,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,MAAA,EAAO,EAAA,EAAG,MAAA,EAC/D,QAAA,EAAA;AAAA,oBAAAD,cAAA,CAAC,MAAA,EAAA,EAAK,MAAA,EAAO,IAAA,EAAK,SAAA,EAAU,SAAA,EAAU,CAAA;AAAA,oBACtCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAA,EAAO,MAAA,EAAO,WAAU,SAAA,EAAU;AAAA,GAAA,EAC1C;AAAA,CAAA,EACF,CAAA,EACF,CAAA;;;ACLF,IAAM,eAAA,GAAkB,CAAC,GAAA,KAA+B;AACtD,EAAA,MAAM,KAAA,GAAQ,+BAAA,CAAgC,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAC7D,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AAEnB,EAAA,IAAI,IAAA,GAAO,MAAM,CAAC,CAAA;AAClB,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrB,IAAA,IAAA,GAAO,IAAA,CACJ,KAAA,CAAM,EAAE,CAAA,CACR,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA,CAChB,IAAA,CAAK,EAAE,CAAA;AAAA,EACZ;AACA,EAAA,MAAM,IAAI,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACvC,EAAA,MAAM,IAAI,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACvC,EAAA,MAAM,IAAI,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACvC,EAAA,OAAO,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,CAAC,IAAI,CAAC,CAAA,CAAA;AACvB,CAAA;AAeO,IAAM,cAAA,GAAiB,CAAC,KAAA,KAAyC;AACtE,EAAA,MAAM,OAA+B,EAAC;AAEtC,EAAA,MAAM,QAAA,GAA+B;AAAA,IACnC,CAAC,oBAAA,EAAsB,KAAA,CAAM,MAAA,CAAO,UAAU,CAAA;AAAA,IAC9C,CAAC,oBAAA,EAAsB,KAAA,CAAM,MAAA,CAAO,UAAU,CAAA;AAAA,IAC9C,CAAC,eAAA,EAAiB,KAAA,CAAM,MAAA,CAAO,KAAK,CAAA;AAAA,IACpC,CAAC,0BAAA,EAA4B,KAAA,CAAM,MAAA,CAAO,eAAe,CAAA;AAAA,IACzD,CAAC,gBAAA,EAAkB,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA;AAAA,IACtC,CAAC,iBAAA,EAAmB,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA;AAAA,IACxC,CAAC,4BAAA,EAA8B,KAAA,CAAM,MAAA,CAAO,iBAAiB,CAAA;AAAA,IAC7D,CAAC,qBAAA,EAAuB,KAAA,CAAM,MAAA,CAAO,WAAW,CAAA;AAAA,IAChD,CAAC,gCAAA,EAAkC,KAAA,CAAM,MAAA,CAAO,qBAAqB;AAAA,GACvE;AACA,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,GAAG,CAAA,IAAK,QAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,gBAAgB,GAAG,CAAA;AACnC,IAAA,IAAI,OAAA,KAAY,IAAA,EAAM,IAAA,CAAK,IAAI,CAAA,GAAI,OAAA;AAAA,EACrC;AAEA,EAAA,IAAI,MAAM,KAAA,EAAO;AACf,IAAA,IAAA,CAAK,aAAa,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,EAAA;AAClC,IAAA,IAAA,CAAK,aAAa,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,EAAA;AAClC,IAAA,IAAA,CAAK,aAAa,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,EAAA;AAAA,EACpC;AAEA,EAAA,OAAO,IAAA;AACT,CAAA;AC1DO,IAAM,YAAA,GAAeE,oBAAwC,IAAI,CAAA;AC8BjE,IAAM,gBAAgB,CAAC,EAAE,QAAQC,qBAAA,EAAS,QAAA,EAAU,UAAS,KAAa;AAC/E,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,IAAA,MAAM,OAAO,QAAA,CAAS,eAAA;AAStB,IAAA,MAAM,aAAA,GAAgB,IAAA,CAAK,YAAA,CAAa,YAAY,CAAA;AACpD,IAAA,MAAM,IAAA,GAAO,eAAe,KAAK,CAAA;AACjC,IAAA,MAAM,iBAAyC,EAAC;AAChD,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG;AACpC,MAAA,cAAA,CAAe,IAAI,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,iBAAiB,IAAI,CAAA;AAAA,IACzD;AAEA,IAAA,IAAA,CAAK,YAAA,CAAa,YAAA,EAAc,KAAA,CAAM,EAAE,CAAA;AACxC,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AAChD,MAAA,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,IAAA,EAAM,KAAK,CAAA;AAAA,IACpC;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,KAAA,MAAW,CAAC,IAAA,EAAM,aAAa,KAAK,MAAA,CAAO,OAAA,CAAQ,cAAc,CAAA,EAAG;AAIlE,QAAA,IAAI,KAAK,KAAA,CAAM,gBAAA,CAAiB,IAAI,CAAA,KAAM,IAAA,CAAK,IAAI,CAAA,EAAG;AACtD,QAAA,IAAI,kBAAkB,EAAA,EAAI;AACxB,UAAA,IAAA,CAAK,KAAA,CAAM,eAAe,IAAI,CAAA;AAAA,QAChC,CAAA,MAAO;AACL,UAAA,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,IAAA,EAAM,aAAa,CAAA;AAAA,QAC5C;AAAA,MACF;AACA,MAAA,IAAI,IAAA,CAAK,YAAA,CAAa,YAAY,CAAA,KAAM,MAAM,EAAA,EAAI;AAChD,QAAA,IAAI,kBAAkB,IAAA,EAAM;AAC1B,UAAA,IAAA,CAAK,gBAAgB,YAAY,CAAA;AAAA,QACnC,CAAA,MAAO;AACL,UAAA,IAAA,CAAK,YAAA,CAAa,cAAc,aAAa,CAAA;AAAA,QAC/C;AAAA,MACF;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,uBACEH,eAAAA,CAAC,YAAA,EAAA,EAAa,OAAO,EAAE,KAAA,EAAO,UAAS,EACrC,QAAA,EAAA;AAAA,oBAAAD,eAAC,cAAA,EAAA,EAAe,CAAA;AAAA,IACf;AAAA,GAAA,EACH,CAAA;AAEJ,CAAA;ACvEO,IAAM,eAAA,GAAkB,CAAC,EAAA,EAAkB,OAAA,GAAmB,EAAC,KAAoB;AACxF,EAAA,MAAM,EAAE,KAAA,GAAQG,qBAAAA,EAAS,GAAG,MAAK,GAAI,OAAA;AACrC,EAAA,OAAOE,eAAO,EAAA,EAAI;AAAA,IAChB,GAAG,IAAA;AAAA,IACH,OAAA,EAAS,CAAC,EAAE,QAAA,uBAAeL,cAAAA,CAAC,aAAA,EAAA,EAAc,KAAA,EAAe,QAAA,EAAS;AAAA,GACnE,CAAA;AACH","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"]}
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 * Map every brand-overridable slot to the matching CSS variable on `:root`.\n *\n * Brand colours are already stored as `R G B` channel strings (validated by\n * the brand schema), matching the shape Tailwind's\n * `rgb(var(--color-*) / <alpha-value>)` template consumes — they're written\n * straight into the CSS variable without conversion.\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).\n */\nexport const brandToCssVars = (brand: Brand): Record<string, string> => {\n const vars: Record<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\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/dist/testing.js CHANGED
@@ -17,35 +17,18 @@ var AIGradientsSvg = () => /* @__PURE__ */ jsx("svg", { width: "0", height: "0",
17
17
  ] }) });
18
18
 
19
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
20
  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
- }
21
+ const vars = {
22
+ "--color-background": brand.colors.background,
23
+ "--color-foreground": brand.colors.foreground,
24
+ "--color-muted": brand.colors.muted,
25
+ "--color-muted-foreground": brand.colors.mutedForeground,
26
+ "--color-border": brand.colors.border,
27
+ "--color-primary": brand.colors.primary,
28
+ "--color-primary-foreground": brand.colors.primaryForeground,
29
+ "--color-destructive": brand.colors.destructive,
30
+ "--color-destructive-foreground": brand.colors.destructiveForeground
31
+ };
49
32
  if (brand.radii) {
50
33
  vars["--radius-sm"] = brand.radii.sm;
51
34
  vars["--radius-md"] = brand.radii.md;
@@ -1 +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"]}
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 * Map every brand-overridable slot to the matching CSS variable on `:root`.\n *\n * Brand colours are already stored as `R G B` channel strings (validated by\n * the brand schema), matching the shape Tailwind's\n * `rgb(var(--color-*) / <alpha-value>)` template consumes — they're written\n * straight into the CSS variable without conversion.\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).\n */\nexport const brandToCssVars = (brand: Brand): Record<string, string> => {\n const vars: Record<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\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": "3.0.0",
3
+ "version": "3.1.1",
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",
@@ -118,6 +118,8 @@
118
118
  "test": "vitest run --config vite.config.ts --passWithNoTests --reporter=dot",
119
119
  "test:stories": "vitest run --config vitest.config.ts",
120
120
  "test:stories:coverage": "vitest run --config vitest.config.ts --coverage",
121
+ "test:visual": "playwright test -c playwright.visual.config.ts",
122
+ "test:visual:update": "playwright test -c playwright.visual.config.ts --update-snapshots",
121
123
  "lint": "yarn lint:format && yarn lint:oxlint && yarn lint:design",
122
124
  "lint:fix": "oxlint --fix . && oxfmt",
123
125
  "knip:check": "yarn --cwd ../.. exec knip --workspace packages/design",
@@ -125,6 +127,9 @@
125
127
  "lint:format": "oxfmt --check",
126
128
  "lint:oxlint": "oxlint . --quiet",
127
129
  "lint:design": "designmd lint DESIGN.md",
130
+ "ci-checks": "yarn check:api",
131
+ "generate:api": "tsx scripts/generate-component-api.ts",
132
+ "check:api": "tsx scripts/generate-component-api.ts --check",
128
133
  "type-check": "tsc"
129
134
  },
130
135
  "engines": {
@@ -136,6 +141,8 @@
136
141
  "@keboola/oxlint-config": "workspace:^",
137
142
  "@keboola/postcss-config": "workspace:^",
138
143
  "@keboola/tsconfig": "workspace:^",
144
+ "@playwright/test": "1.60.0",
145
+ "@storybook/addon-a11y": "10.2.19",
139
146
  "@storybook/addon-docs": "^10.2.19",
140
147
  "@storybook/addon-links": "^10.2.19",
141
148
  "@storybook/addon-vitest": "10.2.19",
@@ -152,9 +159,11 @@
152
159
  "oxfmt": "^0.44.0",
153
160
  "oxlint": "^1.56.0",
154
161
  "playwright": "1.60.0",
162
+ "react-docgen-typescript": "^2.2.2",
155
163
  "storybook": "^10.2.19",
156
164
  "tailwindcss": "^3.4.17",
157
165
  "tsup": "^8.5.1",
166
+ "tsx": "^4.19.4",
158
167
  "typescript": "^6.0.3",
159
168
  "vite": "^8.0.16",
160
169
  "vitest": "^4.1.8"
@@ -181,10 +190,10 @@
181
190
  "@codemirror/merge": "^6.12.1",
182
191
  "@codemirror/state": "^6.6.0",
183
192
  "@codemirror/view": "^6.41.0",
184
- "@keboola/brand-registry": "^0.1.2",
193
+ "@keboola/brand-registry": "^1.0.0",
185
194
  "@keboola/codemirror-lang-sfsql": "^1.2.0",
186
195
  "@keboola/codemirror-lang-sql": "^0.2.0",
187
- "@keboola/tailwind-config": "^0.1.1",
196
+ "@keboola/tailwind-config": "^0.2.0",
188
197
  "@lexical/code": "^0.42.0",
189
198
  "@lexical/link": "^0.42.0",
190
199
  "@lexical/list": "^0.42.0",
@@ -222,6 +231,7 @@
222
231
  "clsx": "^2.1.1",
223
232
  "cmdk": "^1.1.1",
224
233
  "diff": "^9.0.0",
234
+ "dompurify": "^3.4.11",
225
235
  "input-otp": "^1.4.2",
226
236
  "lexical": "^0.42.0",
227
237
  "lucide-react": "^1.17.0",
package/src/styles.css CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  :root {
12
12
  /* Surface */
13
+ --color-surface: 255 255 255; /* #ffffff — raised container chrome (Modal/Card/Sheet) */
13
14
  --color-background: 249 250 251; /* #f9fafb */
14
15
  --color-foreground: 30 41 59; /* #1e293b */
15
16
 
@@ -51,48 +52,48 @@
51
52
  --color-sidebar-ring: 31 143 255; /* #1f8fff */
52
53
  }
53
54
 
54
- /* Dark-mode seam — placeholder values; finalize palette in a follow-up PR. */
55
- /* TODO finalize dark palette (UT-3936 follow-up) */
55
+ /* Dark mode (Graphite) — Keboola-blue accent, not reachable until the theme toggle ships. */
56
56
  [data-theme='dark'] {
57
57
  /* Surface */
58
- --color-background: 30 41 59; /* #1e293b */
59
- --color-foreground: 241 245 249; /* #f1f5f9 */
58
+ --color-surface: 34 34 40; /* #222228 — raised container chrome (Modal/Card/Sheet) */
59
+ --color-background: 19 19 23; /* #131317 */
60
+ --color-foreground: 229 230 236; /* #e5e6ec */
60
61
 
61
- /* Primary — Keboola blue (kept consistent across themes; tune in UT-3936 follow-up). */
62
+ /* Primary — Keboola blue (kept consistent across themes). */
62
63
  --color-primary: 31 143 255; /* #1f8fff */
63
64
  --color-primary-foreground: 255 255 255; /* #ffffff */
64
65
 
65
66
  /* Popover */
66
- --color-popover: 30 41 59; /* #1e293b */
67
- --color-popover-foreground: 241 245 249; /* #f1f5f9 */
67
+ --color-popover: 38 38 45; /* #26262d */
68
+ --color-popover-foreground: 229 230 236; /* #e5e6ec */
68
69
 
69
70
  /* Accent */
70
71
  --color-accent: 30 58 95; /* #1e3a5f */
71
72
  --color-accent-foreground: 147 197 253; /* #93c5fd */
72
73
 
73
74
  /* Border / Input */
74
- --color-border: 51 65 85; /* #334155 */
75
- --color-input: 71 85 105; /* #475569 */
75
+ --color-border: 52 53 62; /* #34353e */
76
+ --color-input: 60 61 71; /* #3c3d47 */
76
77
 
77
78
  /* Destructive */
78
- --color-destructive: 239 68 68; /* #ef4444 */
79
+ --color-destructive: 248 113 113; /* #f87171 */
79
80
  --color-destructive-foreground: 255 255 255; /* #ffffff */
80
81
 
81
82
  /* Success */
82
- --color-success: 34 197 94; /* #22c55e */
83
+ --color-success: 62 207 127; /* #3ecf7f */
83
84
 
84
85
  /* Muted */
85
- --color-muted: 30 41 59; /* #1e293b */
86
- --color-muted-foreground: 148 163 184; /* #94a3b8 */
86
+ --color-muted: 38 38 45; /* #26262d */
87
+ --color-muted-foreground: 141 144 153; /* #8d9099 */
87
88
 
88
89
  /* Sidebar */
89
- --color-sidebar: 15 23 42; /* #0f172a */
90
- --color-sidebar-foreground: 241 245 249; /* #f1f5f9 */
90
+ --color-sidebar: 14 14 18; /* #0e0e12 */
91
+ --color-sidebar-foreground: 229 230 236; /* #e5e6ec */
91
92
  --color-sidebar-primary: 31 143 255; /* #1f8fff */
92
93
  --color-sidebar-primary-foreground: 255 255 255; /* #ffffff */
93
- --color-sidebar-accent: 30 58 95; /* #1e3a5f */
94
- --color-sidebar-accent-foreground: 241 245 249; /* #f1f5f9 */
95
- --color-sidebar-border: 51 65 85; /* #334155 */
94
+ --color-sidebar-accent: 38 38 45; /* #26262d */
95
+ --color-sidebar-accent-foreground: 229 230 236; /* #e5e6ec */
96
+ --color-sidebar-border: 48 49 57; /* #303139 */
96
97
  --color-sidebar-ring: 31 143 255; /* #1f8fff */
97
98
  }
98
99