@burdenoff/fe-libs 2026.1021.5 → 2026.1021.7

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 (40) hide show
  1. package/README.md +35 -0
  2. package/dist/chrome/AccountSwitcher.js +7 -7
  3. package/dist/chrome/AutoBreadcrumbs.js +3 -3
  4. package/dist/chrome/Breadcrumbs.js +24 -24
  5. package/dist/chrome/ContextSwitcher.js +7 -7
  6. package/dist/chrome/DynamicFooter.js +3 -3
  7. package/dist/chrome/HeaderThemeSwitcher.js +14 -14
  8. package/dist/chrome/LanguagePickerSheet.js +7 -7
  9. package/dist/chrome/NativeTopBar.js +4 -4
  10. package/dist/chrome/NavCustomizeDialog.js +12 -12
  11. package/dist/chrome/ProductPickerSheet.js +7 -7
  12. package/dist/chrome/SideNavigation.js +17 -17
  13. package/dist/chrome/Sidebar.js +34 -34
  14. package/dist/chrome/Tabs.js +20 -20
  15. package/dist/chrome/ThemePreferences.js +188 -188
  16. package/dist/shared/assistant/ui/AssistantChatArea.js +4 -4
  17. package/dist/shared/components/ContextConversationWidget.js +7 -7
  18. package/dist/shared/components/QuotaUpgradeBridge.js +13 -13
  19. package/dist/shared/pages/AiPage.js +6 -6
  20. package/dist/shared/providers/shell/ProfileI18nBridge.js +11 -11
  21. package/dist/shared/utils/canvasTokens.d.ts +65 -0
  22. package/dist/shared/utils/canvasTokens.js +159 -0
  23. package/dist/shared/utils/index.d.ts +2 -0
  24. package/dist/shared-hooks-shell.js +7 -7
  25. package/dist/shared-providers-shell.js +6 -6
  26. package/dist/shared-utils.js +19 -18
  27. package/dist/shared.js +135 -134
  28. package/dist/ui/branded-splash.d.ts +25 -2
  29. package/dist/ui/branded-splash.js +48 -31
  30. package/dist/ui/cta-overflow-menu.js +17 -17
  31. package/dist/ui/guidance.js +19 -19
  32. package/dist/ui/index.d.ts +1 -1
  33. package/dist/utils/index.js +1 -0
  34. package/package.json +6 -2
  35. package/shared/config/eslint.config.d.mts +21 -0
  36. package/shared/config/eslint.config.mjs +80 -18
  37. package/src/shared/styles/app-shell.css +3 -0
  38. package/src/shared/styles/canvas-theme.css +82 -0
  39. package/src/shared/tokens/build/semantic.css +83 -0
  40. package/src/shared/tokens/build/tailwind.preset.js +65 -0
@@ -1,38 +1,38 @@
1
- import { cn as e } from "../shared-utils.js";
2
- import { Button as t } from "./button.js";
3
- import { DropdownMenu as n, DropdownMenuContent as r, DropdownMenuItem as i, DropdownMenuTrigger as a } from "./dropdown-menu.js";
4
- import { useShellLabel as o } from "../shared/providers/shell/useShellLabel.js";
1
+ import { useShellLabel as e } from "../shared/providers/shell/useShellLabel.js";
2
+ import { cn as t } from "../shared-utils.js";
3
+ import { Button as n } from "./button.js";
4
+ import { DropdownMenu as r, DropdownMenuContent as i, DropdownMenuItem as a, DropdownMenuTrigger as o } from "./dropdown-menu.js";
5
5
  import { MoreHorizontal as s } from "lucide-react";
6
6
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
7
7
  //#region src/ui/cta-overflow-menu.tsx
8
8
  function u({ primary: u, actions: d, align: f = "end", className: p }) {
9
- let m = o();
9
+ let m = e();
10
10
  return /* @__PURE__ */ l("div", {
11
- className: e("flex items-center gap-2", p),
12
- children: [u ? /* @__PURE__ */ l(t, {
11
+ className: t("flex items-center gap-2", p),
12
+ children: [u ? /* @__PURE__ */ l(n, {
13
13
  type: "button",
14
14
  variant: "default",
15
15
  onClick: u.onSelect,
16
16
  disabled: u.disabled,
17
17
  children: [u.icon, u.label]
18
- }) : null, d.length > 0 ? /* @__PURE__ */ l(n, { children: [/* @__PURE__ */ c(a, {
18
+ }) : null, d.length > 0 ? /* @__PURE__ */ l(r, { children: [/* @__PURE__ */ c(o, {
19
19
  asChild: !0,
20
- children: /* @__PURE__ */ c(t, {
20
+ children: /* @__PURE__ */ c(n, {
21
21
  type: "button",
22
22
  variant: "outline",
23
23
  size: "icon",
24
24
  "aria-label": m("common.moreActions", "More actions"),
25
25
  children: /* @__PURE__ */ c(s, {})
26
26
  })
27
- }), /* @__PURE__ */ c(r, {
27
+ }), /* @__PURE__ */ c(i, {
28
28
  align: f,
29
- children: d.map((t, n) => /* @__PURE__ */ l(i, {
30
- "data-tour": t.dataTour,
31
- disabled: t.disabled,
32
- onSelect: t.onSelect,
33
- className: e("gap-2", t.intent === "destructive" && "text-status-error-text focus:text-status-error-text focus:bg-status-error-bg-subtle"),
34
- children: [t.icon, t.label]
35
- }, `${t.label}-${n}`))
29
+ children: d.map((e, n) => /* @__PURE__ */ l(a, {
30
+ "data-tour": e.dataTour,
31
+ disabled: e.disabled,
32
+ onSelect: e.onSelect,
33
+ className: t("gap-2", e.intent === "destructive" && "text-status-error-text focus:text-status-error-text focus:bg-status-error-bg-subtle"),
34
+ children: [e.icon, e.label]
35
+ }, `${e.label}-${n}`))
36
36
  })] }) : null]
37
37
  });
38
38
  }
@@ -1,5 +1,5 @@
1
- import { cn as e } from "../shared-utils.js";
2
- import { useShellLabel as t } from "../shared/providers/shell/useShellLabel.js";
1
+ import { useShellLabel as e } from "../shared/providers/shell/useShellLabel.js";
2
+ import { cn as t } from "../shared-utils.js";
3
3
  import { useEffect as n, useState as r } from "react";
4
4
  import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
5
5
  //#region src/ui/guidance.tsx
@@ -17,13 +17,13 @@ var s = ({ className: e }) => /* @__PURE__ */ o("svg", {
17
17
  opacity: "0.6"
18
18
  })]
19
19
  });
20
- function c({ children: t, icon: n, learnMoreHref: r, learnMoreLabel: s = "Learn more", className: c }) {
20
+ function c({ children: e, icon: n, learnMoreHref: r, learnMoreLabel: s = "Learn more", className: c }) {
21
21
  return /* @__PURE__ */ o("p", {
22
- className: e("flex items-start gap-2 text-sm leading-relaxed text-text-secondary", c),
22
+ className: t("flex items-start gap-2 text-sm leading-relaxed text-text-secondary", c),
23
23
  children: [n && /* @__PURE__ */ a("span", {
24
24
  className: "mt-0.5 shrink-0 text-text-muted",
25
25
  children: n
26
- }), /* @__PURE__ */ o("span", { children: [t, r && /* @__PURE__ */ o(i, { children: [" ", /* @__PURE__ */ a("a", {
26
+ }), /* @__PURE__ */ o("span", { children: [e, r && /* @__PURE__ */ o(i, { children: [" ", /* @__PURE__ */ a("a", {
27
27
  href: r,
28
28
  className: "font-medium text-text-link underline-offset-2 hover:underline",
29
29
  target: r.startsWith("http") ? "_blank" : void 0,
@@ -35,7 +35,7 @@ function c({ children: t, icon: n, learnMoreHref: r, learnMoreLabel: s = "Learn
35
35
  c.displayName = "PagePurpose";
36
36
  var l = "boff-nextsteps-";
37
37
  function u({ steps: i, title: c, storageKey: u, className: d }) {
38
- let f = t(), p = c ?? f("guidance.title", "Suggested next steps"), m = l + u, [h, g] = r(!1);
38
+ let f = e(), p = c ?? f("guidance.title", "Suggested next steps"), m = l + u, [h, g] = r(!1);
39
39
  n(() => {
40
40
  try {
41
41
  g(localStorage.getItem(m) === "1");
@@ -51,13 +51,13 @@ function u({ steps: i, title: c, storageKey: u, className: d }) {
51
51
  if (h) return /* @__PURE__ */ o("button", {
52
52
  type: "button",
53
53
  onClick: () => _(!1),
54
- className: e("inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-[var(--color-surface-glass)] px-3 py-1.5 text-xs font-medium text-text-secondary shadow-elevation-1 backdrop-blur-[var(--blur-glass)] transition-[box-shadow,border-color] hover:border-border-strong hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", d),
54
+ className: t("inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-[var(--color-surface-glass)] px-3 py-1.5 text-xs font-medium text-text-secondary shadow-elevation-1 backdrop-blur-[var(--blur-glass)] transition-[box-shadow,border-color] hover:border-border-strong hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", d),
55
55
  children: [/* @__PURE__ */ a(s, { className: "size-3.5 text-[var(--color-action-primary-bg)]" }), "Show suggestions"]
56
56
  });
57
57
  let v = i.filter((e) => !e.done).length;
58
58
  return /* @__PURE__ */ o("section", {
59
59
  "aria-label": p,
60
- className: e("relative overflow-hidden rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)]", d),
60
+ className: t("relative overflow-hidden rounded-card border border-border-seam bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)]", d),
61
61
  children: [/* @__PURE__ */ o("div", {
62
62
  className: "flex items-center justify-between gap-3 px-4 pt-3.5",
63
63
  children: [/* @__PURE__ */ o("h3", {
@@ -90,15 +90,15 @@ function u({ steps: i, title: c, storageKey: u, className: d }) {
90
90
  })]
91
91
  }), /* @__PURE__ */ a("ul", {
92
92
  className: "flex flex-col gap-1 p-2.5 pt-2",
93
- children: i.map((t) => /* @__PURE__ */ a("li", { children: /* @__PURE__ */ o(t.href ? "a" : "button", {
94
- ...t.href ? { href: t.href } : {
93
+ children: i.map((e) => /* @__PURE__ */ a("li", { children: /* @__PURE__ */ o(e.href ? "a" : "button", {
94
+ ...e.href ? { href: e.href } : {
95
95
  type: "button",
96
- onClick: t.onClick
96
+ onClick: e.onClick
97
97
  },
98
- className: e("group flex w-full items-start gap-3 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[var(--color-surface-glass)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", t.done && "opacity-60"),
98
+ className: t("group flex w-full items-start gap-3 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[var(--color-surface-glass)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", e.done && "opacity-60"),
99
99
  children: [/* @__PURE__ */ a("span", {
100
- className: e("mt-0.5 grid size-7 shrink-0 place-items-center rounded-full border text-[var(--color-action-primary-bg)]", t.done ? "border-border-subtle bg-bg-muted text-text-muted" : "border-[color-mix(in_oklab,var(--color-action-primary-bg)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-action-primary-bg)_12%,transparent)]"),
101
- children: t.done ? /* @__PURE__ */ a("svg", {
100
+ className: t("mt-0.5 grid size-7 shrink-0 place-items-center rounded-full border text-[var(--color-action-primary-bg)]", e.done ? "border-border-subtle bg-bg-muted text-text-muted" : "border-[color-mix(in_oklab,var(--color-action-primary-bg)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-action-primary-bg)_12%,transparent)]"),
101
+ children: e.done ? /* @__PURE__ */ a("svg", {
102
102
  viewBox: "0 0 24 24",
103
103
  className: "size-4",
104
104
  fill: "none",
@@ -110,18 +110,18 @@ function u({ steps: i, title: c, storageKey: u, className: d }) {
110
110
  strokeLinecap: "round",
111
111
  strokeLinejoin: "round"
112
112
  })
113
- }) : t.icon ?? /* @__PURE__ */ a(s, { className: "size-4" })
113
+ }) : e.icon ?? /* @__PURE__ */ a(s, { className: "size-4" })
114
114
  }), /* @__PURE__ */ o("span", {
115
115
  className: "min-w-0 flex-1",
116
116
  children: [/* @__PURE__ */ a("span", {
117
117
  className: "block text-sm font-medium text-text-primary",
118
- children: t.label
119
- }), t.description && /* @__PURE__ */ a("span", {
118
+ children: e.label
119
+ }), e.description && /* @__PURE__ */ a("span", {
120
120
  className: "block text-xs text-text-secondary",
121
- children: t.description
121
+ children: e.description
122
122
  })]
123
123
  })]
124
- }) }, t.id))
124
+ }) }, e.id))
125
125
  })]
126
126
  });
127
127
  }
@@ -36,7 +36,7 @@ export { GlassCard, glassCardVariants, type GlassCardProps } from './glass-card'
36
36
  export { EmphasisPanel, type EmphasisPanelProps } from './emphasis-panel';
37
37
  export { ShowMore, type ShowMoreProps } from './show-more';
38
38
  export { CTAOverflowMenu, type CTAOverflowMenuProps, type CTAAction, type CTAPrimaryAction, } from './cta-overflow-menu';
39
- export { BrandedSplash, type BrandedSplashProps } from './branded-splash';
39
+ export { BrandedSplash, type BrandedSplashPhase, type BrandedSplashProps } from './branded-splash';
40
40
  export { ModernLanding, type ModernLandingProps, type ModernLandingFeature, } from './modern-landing';
41
41
  export { AuroraBackground, type AuroraBackgroundProps } from './aurora-background';
42
42
  export { Illustration, type IllustrationProps, IllustratedEmptyState, type IllustratedEmptyStateProps, } from './illustration';
@@ -1,5 +1,6 @@
1
1
  import "../shared/utils/apollo.js";
2
2
  import "../shared/utils/tokens.js";
3
+ import "../shared/utils/canvasTokens.js";
3
4
  import "../shared/utils/format.js";
4
5
  import "../shared/utils/lazyWithRetry.js";
5
6
  import "../shared/utils/gatewayFetchRewrite.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/fe-libs",
3
- "version": "2026.1021.5",
3
+ "version": "2026.1021.7",
4
4
  "description": "Burdenoff frontend primitives and domain libraries",
5
5
  "type": "module",
6
6
  "bin": {
@@ -232,6 +232,10 @@
232
232
  "style": "./src/shared/styles/native.css",
233
233
  "default": "./src/shared/styles/native.css"
234
234
  },
235
+ "./shared/styles/canvas-theme.css": {
236
+ "style": "./src/shared/styles/canvas-theme.css",
237
+ "default": "./src/shared/styles/canvas-theme.css"
238
+ },
235
239
  "./shared/utils/lazyWithRetry": {
236
240
  "types": "./dist/shared/utils/lazyWithRetry.d.ts",
237
241
  "import": "./dist/shared/utils/lazyWithRetry.js"
@@ -311,7 +315,7 @@
311
315
  "test:package-livekit-optional": "node scripts/validate-livekit-optional.mjs",
312
316
  "verify:package": "node scripts/verify-package-contents.mjs",
313
317
  "test:handsfree-browser": "node scripts/handsfree-browser-run.mjs",
314
- "test": "bun test scripts/graphql-schema-drift/__tests__ scripts/a11y-audit/__tests__ src/credentials src/connections src/storage src/shared/assistant src/shared/components src/shared/graphql/links/sse-link.test.ts src/shared/graphql/fetch.test.ts src/shared/graphql/prepareWorkspaceRequest.test.ts src/shared/graphql/client.test.ts src/shared/utils/apollo.test.ts src/shared/graphql/tourWorkspaceAuth.test.ts src/shared/config/gatewayUrls.test.ts src/shared/config/authBridgeUrls.test.ts src/shared/config/shellRoutes.test.ts src/shared/config/gatewayFallback.test.ts scripts/__tests__ src/shared/config/products.test.ts src/shared/config/productSlug.test.ts src/shared/hooks/shell/useProducts.test.ts src/shared/providers/shell/brandAttribute.test.ts src/chrome/ProductSwitcher.test.ts src/shared/utils/gatewayFetchRewrite.test.ts src/shared/utils/authCookie.test.ts src/shared/utils/redirectAllowlist.test.ts src/shared/providers/shell/ProfileStorageManager.test.ts src/shared/providers/AppShellProvider.test.ts src/shared/providers/shell/interpolate.test.ts src/shared/providers/shell/resolveTranslation.test.ts src/shared/providers/shell/I18nProvider.render.test.tsx src/shared/providers/shell/I18nProvider.client.test.tsx src/shared/providers/shell/translationStore.test.ts src/shared/providers/shell/translationLayers.test.ts src/shared/providers/shell/directionMode.test.ts src/shared/providers/shell/authProvider/ssoRelay.test.ts src/shared/providers/shell/authProvider/useAuthActions.signOut.test.tsx src/shared/providers/shell/personaSurfaceRemote.test.ts src/shared/providers/shell/workspaceSwitch.client.test.tsx src/shared/events/sinks/RybbitSink.test.ts src/shell-native src/shared/native src/shared/navigation src/shared/tours src/chrome/__tests__ src/ui/__tests__",
318
+ "test": "bun test scripts/graphql-schema-drift/__tests__ scripts/a11y-audit/__tests__ src/credentials src/connections src/storage src/shared/assistant src/shared/components src/shared/graphql/links/sse-link.test.ts src/shared/graphql/fetch.test.ts src/shared/graphql/prepareWorkspaceRequest.test.ts src/shared/graphql/client.test.ts src/shared/utils/apollo.test.ts src/shared/utils/canvasTokens.test.ts src/shared/graphql/tourWorkspaceAuth.test.ts src/shared/config/gatewayUrls.test.ts src/shared/config/authBridgeUrls.test.ts src/shared/config/shellRoutes.test.ts src/shared/config/gatewayFallback.test.ts scripts/__tests__ src/shared/config/products.test.ts src/shared/config/productSlug.test.ts src/shared/hooks/shell/useProducts.test.ts src/shared/providers/shell/brandAttribute.test.ts src/chrome/ProductSwitcher.test.ts src/shared/utils/gatewayFetchRewrite.test.ts src/shared/utils/authCookie.test.ts src/shared/utils/redirectAllowlist.test.ts src/shared/providers/shell/ProfileStorageManager.test.ts src/shared/providers/AppShellProvider.test.ts src/shared/providers/shell/interpolate.test.ts src/shared/providers/shell/resolveTranslation.test.ts src/shared/providers/shell/I18nProvider.render.test.tsx src/shared/providers/shell/I18nProvider.client.test.tsx src/shared/providers/shell/translationStore.test.ts src/shared/providers/shell/translationLayers.test.ts src/shared/providers/shell/directionMode.test.ts src/shared/providers/shell/authProvider/ssoRelay.test.ts src/shared/providers/shell/authProvider/useAuthActions.signOut.test.tsx src/shared/providers/shell/personaSurfaceRemote.test.ts src/shared/providers/shell/workspaceSwitch.client.test.tsx src/shared/events/sinks/RybbitSink.test.ts src/shell-native src/shared/native src/shared/navigation src/shared/tours src/chrome/__tests__ src/ui/__tests__",
315
319
  "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build && bun run verify:package'",
316
320
  "switch:status": "bun scripts/deps-mode.ts status",
317
321
  "switch:remote": "bun scripts/deps-mode.ts remote",
@@ -3,8 +3,29 @@ import type { Linter } from 'eslint';
3
3
  export interface EslintConfigOptions {
4
4
  ignores?: string[];
5
5
  additionalRules?: Record<string, unknown>;
6
+ /**
7
+ * Severity of the raw-colour / palette-class guard (hex, rgb()/hsl()/oklch()
8
+ * literals and Tailwind palette classes such as `bg-red-500` or `text-white`
9
+ * in string literals and template strings). Defaults to `'error'`. `'warn'`
10
+ * is a visible, temporary opt-out for a repo that has not swept its
11
+ * raw-colour debt yet.
12
+ */
13
+ rawColors?: 'error' | 'warn' | 'off';
6
14
  }
7
15
 
16
+ /** esquery regex source matching a hex colour or a CSS colour function with literal channels. */
17
+ export const RAW_COLOR_PATTERN: string;
18
+
19
+ /** esquery regex source matching a Tailwind palette class or a black/white colour class. */
20
+ export const PALETTE_CLASS_PATTERN: string;
21
+
22
+ /**
23
+ * The `no-restricted-syntax` entries behind the raw-colour guard, for repos
24
+ * with a standalone ESLint config:
25
+ * `'no-restricted-syntax': ['error', ...rawColorRestrictedSyntax()]`.
26
+ */
27
+ export function rawColorRestrictedSyntax(): { selector: string; message: string }[];
28
+
8
29
  export function createEslintConfig(options?: EslintConfigOptions): Linter.FlatConfig[];
9
30
 
10
31
  export default createEslintConfig;
@@ -4,15 +4,83 @@ import reactHooks from 'eslint-plugin-react-hooks';
4
4
  import react from 'eslint-plugin-react';
5
5
  import tseslint from 'typescript-eslint';
6
6
 
7
+ /*
8
+ * ── Raw-colour guard ────────────────────────────────────────────────────────
9
+ * UI code uses the semantic tokens in fe-libs/shared/tokens (bg-*, text-*,
10
+ * border-*, action-*, status-*, chart-*, node-*, edge-*, canvas-*), never a raw
11
+ * colour. The guard reads string literals and template-string parts only, so
12
+ * a comment that quotes a PR number ("#268") or a bad class is never flagged.
13
+ *
14
+ * The patterns are esquery regexes: they must not contain "/" and use no flags.
15
+ */
16
+
17
+ /**
18
+ * A hex colour (#rgb, #rgba, #rrggbb, #rrggbbaa) not glued to a word on the
19
+ * left (so `&#123;` and `page#abc` pass) or the right (so `#1234567` passes),
20
+ * or a CSS colour function with literal channels. `rgb(var(--x))` builds on a
21
+ * token and is allowed.
22
+ */
23
+ export const RAW_COLOR_PATTERN =
24
+ '(?:^|[^&\\w])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\\w-])|\\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\\((?!\\s*var\\()';
25
+
26
+ /**
27
+ * A Tailwind palette class (`bg-red-500`, `hover:text-slate-50`,
28
+ * `border-t-gray-200`, `bg-blue-500/20`) or a named black/white one
29
+ * (`text-white`, `bg-black/50`) on any colour utility. Token classes such as
30
+ * `text-text-primary`, `bg-status-error-bg` or `stroke-edge-default` do not match.
31
+ */
32
+ export const PALETTE_CLASS_PATTERN =
33
+ '(?:^|[\\s:!])-?(?:bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|inset-ring|outline|fill|stroke|divide|from|via|to|decoration|caret|accent|shadow|inset-shadow|drop-shadow|placeholder)-(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|[1-9]00|950)|black|white)(?![\\w-])';
34
+
35
+ const RAW_COLOR_MESSAGE =
36
+ 'Use semantic tokens from @burdenoff/fe-libs/shared/tokens/tokens.css (bg-*, text-*, border-*, action-*, status-*, chart-*, node-*, edge-*, canvas-*) — not raw colours.';
37
+ const PALETTE_CLASS_MESSAGE =
38
+ 'Use semantic token classes (bg-bg-surface, text-text-primary, bg-status-error-bg, text-chart-3, bg-node-trigger, stroke-edge-default; scrims bg-bg-overlay; text on an accent fill text-action-primary-text) — not Tailwind palette classes.';
39
+
40
+ /**
41
+ * The `no-restricted-syntax` entries that ban raw colours and palette classes
42
+ * in string literals and template strings. Exported so a repo with its own
43
+ * standalone ESLint config can apply the same guard:
44
+ *
45
+ * 'no-restricted-syntax': ['error', ...rawColorRestrictedSyntax()]
46
+ *
47
+ * @returns {{ selector: string, message: string }[]}
48
+ */
49
+ export function rawColorRestrictedSyntax() {
50
+ return [
51
+ { selector: `Literal[value=/${RAW_COLOR_PATTERN}/]`, message: RAW_COLOR_MESSAGE },
52
+ { selector: `TemplateElement[value.raw=/${RAW_COLOR_PATTERN}/]`, message: RAW_COLOR_MESSAGE },
53
+ { selector: `Literal[value=/${PALETTE_CLASS_PATTERN}/]`, message: PALETTE_CLASS_MESSAGE },
54
+ {
55
+ selector: `TemplateElement[value.raw=/${PALETTE_CLASS_PATTERN}/]`,
56
+ message: PALETTE_CLASS_MESSAGE,
57
+ },
58
+ ];
59
+ }
60
+
61
+ /** Test files may assert on literal colours; the guard is for shipped UI. */
62
+ const TEST_FILES = ['**/*.{test,spec}.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}'];
63
+
64
+ const RAW_COLOR_SEVERITIES = ['error', 'warn', 'off'];
65
+
7
66
  /**
8
67
  * Creates a shared ESLint configuration for Burdenoff frontend projects.
9
68
  * @param {Object} options - Configuration options
10
- * @param {string[]} options.ignores - Additional ignore patterns
11
- * @param {Record<string, unknown>} options.additionalRules - Additional ESLint rules
69
+ * @param {string[]} [options.ignores] - Additional ignore patterns
70
+ * @param {Record<string, unknown>} [options.additionalRules] - Additional ESLint rules
71
+ * @param {'error' | 'warn' | 'off'} [options.rawColors] - Severity of the raw-colour /
72
+ * palette-class guard. Defaults to 'error'. 'warn' is for a repo that still carries
73
+ * raw-colour debt it has not swept yet; it is a visible, temporary opt-out.
12
74
  * @returns {import('eslint').Linter.FlatConfig[]} ESLint flat config array
13
75
  */
14
76
  export function createEslintConfig(options = {}) {
15
- const { ignores = [], additionalRules = {} } = options;
77
+ const { ignores = [], additionalRules = {}, rawColors = 'error' } = options;
78
+ if (!RAW_COLOR_SEVERITIES.includes(rawColors)) {
79
+ throw new Error(
80
+ `createEslintConfig: rawColors must be one of ${RAW_COLOR_SEVERITIES.join(', ')}, got ${String(rawColors)}`
81
+ );
82
+ }
83
+ const colourRule = rawColors === 'off' ? 'off' : [rawColors, ...rawColorRestrictedSyntax()];
16
84
 
17
85
  return tseslint.config(
18
86
  { ignores: ['**/dist', '**/node_modules', '**/*.stories.tsx', ...ignores] },
@@ -63,21 +131,8 @@ export function createEslintConfig(options = {}) {
63
131
  ],
64
132
  },
65
133
  ],
66
- 'no-restricted-syntax': [
67
- 'warn',
68
- {
69
- selector:
70
- "Literal[value=/(?:#[0-9a-fA-F]{3,8}\\b)|(?:rgb[a]?\\()|(?:hsl[a]?\\()/]",
71
- message:
72
- 'Use semantic tokens from fe-libs/shared/tokens/tokens.css (bg-*, text-*, action-*, status-*) — not raw colours.',
73
- },
74
- {
75
- selector:
76
- "TemplateElement[value.raw=/(?:#[0-9a-fA-F]{3,8}\\b)|(?:rgb[a]?\\()|(?:hsl[a]?\\()/]",
77
- message:
78
- 'Use semantic tokens from fe-libs/shared/tokens/tokens.css — not raw colours in template strings.',
79
- },
80
- ],
134
+ // Semantic tokens only — no hex/rgb/hsl and no Tailwind palette class.
135
+ 'no-restricted-syntax': colourRule,
81
136
  ...additionalRules,
82
137
  },
83
138
  settings: {
@@ -85,6 +140,13 @@ export function createEslintConfig(options = {}) {
85
140
  version: 'detect',
86
141
  },
87
142
  },
143
+ },
144
+ {
145
+ // Tests keep any no-restricted-syntax the repo set itself, minus the colour guard.
146
+ files: TEST_FILES,
147
+ rules: {
148
+ 'no-restricted-syntax': additionalRules['no-restricted-syntax'] ?? 'off',
149
+ },
88
150
  }
89
151
  );
90
152
  }
@@ -13,6 +13,9 @@
13
13
  /* Installed-app (Capacitor / PWA / Electron) look & feel rules. */
14
14
  @import './native.css';
15
15
 
16
+ /* Canvas / node / edge / data-type utilities (bg-node-*, stroke-edge-*, …). */
17
+ @import './canvas-theme.css';
18
+
16
19
  /* ============================================
17
20
  * @theme — Tailwind v4 theme configuration
18
21
  * Maps fe-libs design tokens to Tailwind utilities
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Canvas / node / edge / data-type token utilities (Tailwind v4).
3
+ *
4
+ * Maps the G19 builder tokens from tokens.css to utilities such as
5
+ * `bg-node-trigger`, `bg-node-logic-subtle`, `border-node-border-selected`,
6
+ * `ring-node-state-running`, `stroke-edge-default`, `fill-canvas-grid` and
7
+ * `text-datatype-string`. Imported by app-shell.css; a shell with its own
8
+ * @theme imports it directly:
9
+ *
10
+ * @import "tailwindcss";
11
+ * @import '@burdenoff/fe-libs/shared/tokens/tokens.css';
12
+ * @import '@burdenoff/fe-libs/shared/styles/canvas-theme.css';
13
+ *
14
+ * For values outside Tailwind (React Flow props, SVG attributes, inline
15
+ * styles) use `var(--color-…)` directly, or the helpers in
16
+ * @burdenoff/fe-libs/shared/utils (nodeCategoryColor, edgeColor, …).
17
+ */
18
+ @theme {
19
+ --color-canvas-bg: var(--color-canvas-bg);
20
+ --color-canvas-grid: var(--color-canvas-grid);
21
+ --color-canvas-selection: var(--color-canvas-selection);
22
+ --color-canvas-minimap-bg: var(--color-canvas-minimap-bg);
23
+ --color-canvas-minimap-node: var(--color-canvas-minimap-node);
24
+ --color-canvas-minimap-mask: var(--color-canvas-minimap-mask);
25
+ --color-canvas-control-bg: var(--color-canvas-control-bg);
26
+ --color-node-bg: var(--color-node-bg);
27
+ --color-node-bg-hover: var(--color-node-bg-hover);
28
+ --color-node-border: var(--color-node-border);
29
+ --color-node-border-hover: var(--color-node-border-hover);
30
+ --color-node-border-selected: var(--color-node-border-selected);
31
+ --color-node-text: var(--color-node-text);
32
+ --color-node-text-muted: var(--color-node-text-muted);
33
+ --color-node-handle: var(--color-node-handle);
34
+ --color-node-handle-connected: var(--color-node-handle-connected);
35
+ --color-node-trigger: var(--color-node-trigger);
36
+ --color-node-trigger-subtle: var(--color-node-trigger-subtle);
37
+ --color-node-action: var(--color-node-action);
38
+ --color-node-action-subtle: var(--color-node-action-subtle);
39
+ --color-node-logic: var(--color-node-logic);
40
+ --color-node-logic-subtle: var(--color-node-logic-subtle);
41
+ --color-node-loop: var(--color-node-loop);
42
+ --color-node-loop-subtle: var(--color-node-loop-subtle);
43
+ --color-node-data: var(--color-node-data);
44
+ --color-node-data-subtle: var(--color-node-data-subtle);
45
+ --color-node-integration: var(--color-node-integration);
46
+ --color-node-integration-subtle: var(--color-node-integration-subtle);
47
+ --color-node-ai: var(--color-node-ai);
48
+ --color-node-ai-subtle: var(--color-node-ai-subtle);
49
+ --color-node-human: var(--color-node-human);
50
+ --color-node-human-subtle: var(--color-node-human-subtle);
51
+ --color-node-utility: var(--color-node-utility);
52
+ --color-node-utility-subtle: var(--color-node-utility-subtle);
53
+ --color-node-end: var(--color-node-end);
54
+ --color-node-end-subtle: var(--color-node-end-subtle);
55
+ --color-node-default: var(--color-node-default);
56
+ --color-node-default-subtle: var(--color-node-default-subtle);
57
+ --color-node-state-idle: var(--color-node-state-idle);
58
+ --color-node-state-queued: var(--color-node-state-queued);
59
+ --color-node-state-running: var(--color-node-state-running);
60
+ --color-node-state-success: var(--color-node-state-success);
61
+ --color-node-state-warning: var(--color-node-state-warning);
62
+ --color-node-state-error: var(--color-node-state-error);
63
+ --color-node-state-skipped: var(--color-node-state-skipped);
64
+ --color-edge-default: var(--color-edge-default);
65
+ --color-edge-hover: var(--color-edge-hover);
66
+ --color-edge-selected: var(--color-edge-selected);
67
+ --color-edge-active: var(--color-edge-active);
68
+ --color-edge-success: var(--color-edge-success);
69
+ --color-edge-error: var(--color-edge-error);
70
+ --color-edge-compatible: var(--color-edge-compatible);
71
+ --color-edge-incompatible: var(--color-edge-incompatible);
72
+ --color-edge-label-bg: var(--color-edge-label-bg);
73
+ --color-edge-label-text: var(--color-edge-label-text);
74
+ --color-datatype-string: var(--color-datatype-string);
75
+ --color-datatype-number: var(--color-datatype-number);
76
+ --color-datatype-boolean: var(--color-datatype-boolean);
77
+ --color-datatype-object: var(--color-datatype-object);
78
+ --color-datatype-array: var(--color-datatype-array);
79
+ --color-datatype-date: var(--color-datatype-date);
80
+ --color-datatype-binary: var(--color-datatype-binary);
81
+ --color-datatype-any: var(--color-datatype-any);
82
+ }
@@ -506,6 +506,89 @@
506
506
  --color-focus-ring: var(--color-action-primary-bg);
507
507
  }
508
508
 
509
+ /* G19: canvas, node-category, edge and data-type tokens (workflow builders,
510
+ * graphs, mappers). Every value ALIASES an existing chart-* / status-* /
511
+ * surface token — no new palette — so light, dark, every brand and the
512
+ * expressive / high-contrast themes come for free.
513
+ *
514
+ * Declared on each element that can carry a theme (not only :root) so a
515
+ * `.dark` subtree re-resolves the aliases against ITS chart/status values;
516
+ * a var() alias declared only on :root would inherit the light value. */
517
+ :root,
518
+ [data-theme],
519
+ [data-brand],
520
+ .dark {
521
+ /* Canvas */
522
+ --color-canvas-bg: var(--color-bg-canvas);
523
+ --color-canvas-grid: var(--color-chart-grid);
524
+ --color-canvas-selection: var(--color-bg-accentSubtle);
525
+ --color-canvas-minimap-bg: var(--color-bg-surface);
526
+ --color-canvas-minimap-node: var(--color-chart-axis);
527
+ --color-canvas-minimap-mask: var(--color-bg-overlay);
528
+ --color-canvas-control-bg: var(--color-bg-elevated);
529
+ /* Node surface */
530
+ --color-node-bg: var(--color-bg-surface);
531
+ --color-node-bg-hover: var(--color-bg-elevated);
532
+ --color-node-border: var(--color-border-default);
533
+ --color-node-border-hover: var(--color-border-strong);
534
+ --color-node-border-selected: var(--color-action-primary-bg);
535
+ --color-node-text: var(--color-text-primary);
536
+ --color-node-text-muted: var(--color-text-muted);
537
+ --color-node-handle: var(--color-border-strong);
538
+ --color-node-handle-connected: var(--color-action-primary-bg);
539
+ /* Node categories (solid accent + subtle tint for headers / icon wells) */
540
+ --color-node-trigger: var(--color-chart-7); /* starts a run: webhook, schedule, API, event */
541
+ --color-node-trigger-subtle: color-mix(in oklab, var(--color-node-trigger) 14%, transparent);
542
+ --color-node-action: var(--color-chart-1); /* does work: an app or service step */
543
+ --color-node-action-subtle: color-mix(in oklab, var(--color-node-action) 14%, transparent);
544
+ --color-node-logic: var(--color-chart-3); /* branches: if, switch, router */
545
+ --color-node-logic-subtle: color-mix(in oklab, var(--color-node-logic) 14%, transparent);
546
+ --color-node-loop: var(--color-chart-4); /* iterates: loop, batch, map */
547
+ --color-node-loop-subtle: color-mix(in oklab, var(--color-node-loop) 14%, transparent);
548
+ --color-node-data: var(--color-chart-2); /* shapes data: set, filter, aggregate, transform, code */
549
+ --color-node-data-subtle: color-mix(in oklab, var(--color-node-data) 14%, transparent);
550
+ --color-node-integration: var(--color-chart-12); /* talks to the outside: HTTP, database, connector */
551
+ --color-node-integration-subtle: color-mix(in oklab, var(--color-node-integration) 14%, transparent);
552
+ --color-node-ai: var(--color-chart-8); /* model, agent, embedding, assistant */
553
+ --color-node-ai-subtle: color-mix(in oklab, var(--color-node-ai) 14%, transparent);
554
+ --color-node-human: var(--color-chart-6); /* waits on a person: approval, form, review */
555
+ --color-node-human-subtle: color-mix(in oklab, var(--color-node-human) 14%, transparent);
556
+ --color-node-utility: var(--color-chart-axis); /* wait, no-op, note, merge */
557
+ --color-node-utility-subtle: color-mix(in oklab, var(--color-node-utility) 14%, transparent);
558
+ --color-node-end: var(--color-chart-5); /* stops a run: end, error, return */
559
+ --color-node-end-subtle: color-mix(in oklab, var(--color-node-end) 14%, transparent);
560
+ --color-node-default: var(--color-chart-1); /* a node with no known category */
561
+ --color-node-default-subtle: color-mix(in oklab, var(--color-node-default) 14%, transparent);
562
+ /* Node execution state (ring / badge) */
563
+ --color-node-state-idle: var(--color-border-default);
564
+ --color-node-state-queued: var(--color-chart-axis);
565
+ --color-node-state-running: var(--color-status-info-bg);
566
+ --color-node-state-success: var(--color-status-success-bg);
567
+ --color-node-state-warning: var(--color-status-warning-bg);
568
+ --color-node-state-error: var(--color-status-error-bg);
569
+ --color-node-state-skipped: var(--color-chart-axis);
570
+ /* Edges */
571
+ --color-edge-default: var(--color-chart-axis);
572
+ --color-edge-hover: var(--color-text-secondary);
573
+ --color-edge-selected: var(--color-action-primary-bg);
574
+ --color-edge-active: var(--color-status-info-bg);
575
+ --color-edge-success: var(--color-status-success-bg);
576
+ --color-edge-error: var(--color-status-error-bg);
577
+ --color-edge-compatible: var(--color-status-info-bg);
578
+ --color-edge-incompatible: var(--color-status-error-bg);
579
+ --color-edge-label-bg: var(--color-bg-elevated);
580
+ --color-edge-label-text: var(--color-text-secondary);
581
+ /* Port / mapper data types */
582
+ --color-datatype-string: var(--color-chart-3);
583
+ --color-datatype-number: var(--color-chart-10);
584
+ --color-datatype-boolean: var(--color-chart-2);
585
+ --color-datatype-object: var(--color-chart-12);
586
+ --color-datatype-array: var(--color-chart-4);
587
+ --color-datatype-date: var(--color-chart-6);
588
+ --color-datatype-binary: var(--color-chart-11);
589
+ --color-datatype-any: var(--color-chart-axis);
590
+ }
591
+
509
592
  /* Large Typography Scale - Auto-generated */
510
593
  [data-typography="large"] {
511
594
  --font-size-body-sm: 16px;
@@ -141,6 +141,71 @@ export default {
141
141
  'chart-axis': 'var(--color-chart-axis)',
142
142
  'chart-positive': 'var(--color-chart-positive)',
143
143
  'chart-negative': 'var(--color-chart-negative)',
144
+
145
+ // G19 — canvas / node / edge / data-type tokens
146
+ 'canvas-bg': 'var(--color-canvas-bg)',
147
+ 'canvas-grid': 'var(--color-canvas-grid)',
148
+ 'canvas-selection': 'var(--color-canvas-selection)',
149
+ 'canvas-minimap-bg': 'var(--color-canvas-minimap-bg)',
150
+ 'canvas-minimap-node': 'var(--color-canvas-minimap-node)',
151
+ 'canvas-minimap-mask': 'var(--color-canvas-minimap-mask)',
152
+ 'canvas-control-bg': 'var(--color-canvas-control-bg)',
153
+ 'node-bg': 'var(--color-node-bg)',
154
+ 'node-bg-hover': 'var(--color-node-bg-hover)',
155
+ 'node-border': 'var(--color-node-border)',
156
+ 'node-border-hover': 'var(--color-node-border-hover)',
157
+ 'node-border-selected': 'var(--color-node-border-selected)',
158
+ 'node-text': 'var(--color-node-text)',
159
+ 'node-text-muted': 'var(--color-node-text-muted)',
160
+ 'node-handle': 'var(--color-node-handle)',
161
+ 'node-handle-connected': 'var(--color-node-handle-connected)',
162
+ 'node-trigger': 'var(--color-node-trigger)',
163
+ 'node-trigger-subtle': 'var(--color-node-trigger-subtle)',
164
+ 'node-action': 'var(--color-node-action)',
165
+ 'node-action-subtle': 'var(--color-node-action-subtle)',
166
+ 'node-logic': 'var(--color-node-logic)',
167
+ 'node-logic-subtle': 'var(--color-node-logic-subtle)',
168
+ 'node-loop': 'var(--color-node-loop)',
169
+ 'node-loop-subtle': 'var(--color-node-loop-subtle)',
170
+ 'node-data': 'var(--color-node-data)',
171
+ 'node-data-subtle': 'var(--color-node-data-subtle)',
172
+ 'node-integration': 'var(--color-node-integration)',
173
+ 'node-integration-subtle': 'var(--color-node-integration-subtle)',
174
+ 'node-ai': 'var(--color-node-ai)',
175
+ 'node-ai-subtle': 'var(--color-node-ai-subtle)',
176
+ 'node-human': 'var(--color-node-human)',
177
+ 'node-human-subtle': 'var(--color-node-human-subtle)',
178
+ 'node-utility': 'var(--color-node-utility)',
179
+ 'node-utility-subtle': 'var(--color-node-utility-subtle)',
180
+ 'node-end': 'var(--color-node-end)',
181
+ 'node-end-subtle': 'var(--color-node-end-subtle)',
182
+ 'node-default': 'var(--color-node-default)',
183
+ 'node-default-subtle': 'var(--color-node-default-subtle)',
184
+ 'node-state-idle': 'var(--color-node-state-idle)',
185
+ 'node-state-queued': 'var(--color-node-state-queued)',
186
+ 'node-state-running': 'var(--color-node-state-running)',
187
+ 'node-state-success': 'var(--color-node-state-success)',
188
+ 'node-state-warning': 'var(--color-node-state-warning)',
189
+ 'node-state-error': 'var(--color-node-state-error)',
190
+ 'node-state-skipped': 'var(--color-node-state-skipped)',
191
+ 'edge-default': 'var(--color-edge-default)',
192
+ 'edge-hover': 'var(--color-edge-hover)',
193
+ 'edge-selected': 'var(--color-edge-selected)',
194
+ 'edge-active': 'var(--color-edge-active)',
195
+ 'edge-success': 'var(--color-edge-success)',
196
+ 'edge-error': 'var(--color-edge-error)',
197
+ 'edge-compatible': 'var(--color-edge-compatible)',
198
+ 'edge-incompatible': 'var(--color-edge-incompatible)',
199
+ 'edge-label-bg': 'var(--color-edge-label-bg)',
200
+ 'edge-label-text': 'var(--color-edge-label-text)',
201
+ 'datatype-string': 'var(--color-datatype-string)',
202
+ 'datatype-number': 'var(--color-datatype-number)',
203
+ 'datatype-boolean': 'var(--color-datatype-boolean)',
204
+ 'datatype-object': 'var(--color-datatype-object)',
205
+ 'datatype-array': 'var(--color-datatype-array)',
206
+ 'datatype-date': 'var(--color-datatype-date)',
207
+ 'datatype-binary': 'var(--color-datatype-binary)',
208
+ 'datatype-any': 'var(--color-datatype-any)',
144
209
  },
145
210
  spacing: {
146
211
  'page-padding': 'var(--space-pagePadding)',