diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json index afe74c514b..daf6d8950e 100644 --- a/design-system/packages/design-tokens/src/system.tokens.json +++ b/design-system/packages/design-tokens/src/system.tokens.json @@ -716,6 +716,18 @@ "overflowText": { "fadeExtent": { "$value": "{space.4}" } }, + "empty": { + "$description": "Shared empty-state media geometry. Callers select a semantic size and do not size artwork directly.", + "gap": { "$value": "10px" }, + "paddingBlock": { "$value": "{space.8}" }, + "paddingInline": { "$value": "{space.4}" }, + "mediaSizeSm": { "$value": "{control.icon.sizeLg}" }, + "mediaSizeMd": { "$value": "{control.height.sm}" }, + "mediaSizeLg": { "$value": "{control.height.md}" }, + "iconSizeSm": { "$value": "{control.icon.sizeLg}" }, + "iconSizeMd": { "$value": "{control.height.sm}" }, + "iconSizeLg": { "$value": "{control.height.md}" } + }, "field": { "rootGap": { "$value": "{space.2}" }, "horizontalGap": { "$value": "{space.5}" }, diff --git a/design-system/packages/design-tokens/tests/contract.test.mjs b/design-system/packages/design-tokens/tests/contract.test.mjs index 780a6b5e80..db0d0566b2 100644 --- a/design-system/packages/design-tokens/tests/contract.test.mjs +++ b/design-system/packages/design-tokens/tests/contract.test.mjs @@ -45,6 +45,18 @@ test("search height preserves the compact reference and follows other density sc assert.equal(value("space.component.inline", "compact"), "10px"); }); +test("Empty media geometry owns semantic artwork sizes", () => { + assert.equal(tokens["layout.empty.gap"], "10px"); + assert.equal(tokens["layout.empty.paddingBlock"], "32px"); + assert.equal(tokens["layout.empty.paddingInline"], "16px"); + assert.equal(tokens["layout.empty.mediaSizeSm"], "24px"); + assert.equal(tokens["layout.empty.mediaSizeMd"], "32px"); + assert.equal(tokens["layout.empty.mediaSizeLg"], "40px"); + assert.equal(tokens["layout.empty.iconSizeSm"], "24px"); + assert.equal(tokens["layout.empty.iconSizeMd"], "32px"); + assert.equal(tokens["layout.empty.iconSizeLg"], "40px"); +}); + test("Switch geometry preserves the compact reference contract", () => { assert.equal(tokens["control.switch.trackWidth"], "28px"); assert.equal(tokens["control.switch.trackHeight"], "16px"); diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md index b8ff315275..b19969bdee 100644 --- a/design-system/packages/theme-openbitfun/README.md +++ b/design-system/packages/theme-openbitfun/README.md @@ -55,6 +55,13 @@ IconButton, and other controls. Dark and high-contrast mappings retain their mode-specific feedback and outline contrast. Color entries remain editable in Design Lab's Colors catalog; Button geometry is independent of this palette. +`component.empty.media` owns the low-emphasis color for decorative Empty +artwork. It resolves to an opaque neutral in every mode because Lucide icons can +contain overlapping paths; reusing translucent disabled-content colors would +make those intersections visibly darker. The color matches muted content at 35% +over the tertiary empty-state surface, while `layout.empty.*` owns the reference +10px rhythm, 32/16px padding, and 24/32/40px media sizes. + The default Web UI appearances consume these published component values. Branded presets and imported appearances may still supply the existing action tokens: the Web UI inherits explicit old values only when the corresponding diff --git a/design-system/packages/theme-openbitfun/scripts/build.mjs b/design-system/packages/theme-openbitfun/scripts/build.mjs index 96e20fe42a..c47ab8c7ab 100644 --- a/design-system/packages/theme-openbitfun/scripts/build.mjs +++ b/design-system/packages/theme-openbitfun/scripts/build.mjs @@ -38,7 +38,14 @@ const highContrastDarkTokens = resolveTokens( mergeTokenDocuments(reference, dark, highContrastDark), ); -const PUBLIC_THEME_TOKEN_PREFIXES = ["color.", "component.button.", "effect.", "opacity.", "shadow."]; +const PUBLIC_THEME_TOKEN_PREFIXES = [ + "color.", + "component.button.", + "component.empty.", + "effect.", + "opacity.", + "shadow.", +]; const REFERENCE_COLOR_TOKEN_PATTERN = /^ref\.color\.([a-z][a-z0-9-]*)\.(\d+)$/; function createReferenceColorArtifacts(document, tokens) { @@ -106,6 +113,7 @@ const semanticThemes = { }; const requiredSemanticTokens = [ + "component.empty.media", "color.surface.canvas", "color.surface.panel", "color.surface.raised", diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json index c8103c1edb..fdd98d88bf 100644 --- a/design-system/packages/theme-openbitfun/src/dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json @@ -16,6 +16,13 @@ "primaryBackgroundHover": { "$type": "color", "$value": "{color.action.primary.hover}" }, "primaryBackgroundPressed": { "$type": "color", "$value": "{color.action.primary.pressed}" }, "primaryContentDisabled": { "$type": "color", "$value": "{color.action.neutral.contentDisabled}" } + }, + "empty": { + "media": { + "$description": "Opaque low-emphasis color for decorative empty-state artwork, matching muted content at 35% over the tertiary empty-state surface without alpha accumulation.", + "$type": "color", + "$value": "color-mix(in srgb, {color.content.muted} 35%, {color.surface.tertiary})" + } } }, "color": { diff --git a/design-system/packages/theme-openbitfun/src/light.tokens.json b/design-system/packages/theme-openbitfun/src/light.tokens.json index 715e283296..c4473b8272 100644 --- a/design-system/packages/theme-openbitfun/src/light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/light.tokens.json @@ -16,6 +16,13 @@ "primaryBackgroundHover": { "$type": "color", "$value": "rgba(0, 0, 0, 0.60)" }, "primaryBackgroundPressed": { "$type": "color", "$value": "rgba(0, 0, 0, 0.90)" }, "primaryContentDisabled": { "$type": "color", "$value": "rgba(0, 0, 0, 0.20)" } + }, + "empty": { + "media": { + "$description": "Opaque low-emphasis color for decorative empty-state artwork, matching muted content at 35% over the tertiary empty-state surface without alpha accumulation.", + "$type": "color", + "$value": "color-mix(in srgb, {color.content.muted} 35%, {color.surface.tertiary})" + } } }, "color": { diff --git a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs index 0fb2c7cd23..2d7c2bc72f 100644 --- a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs +++ b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs @@ -278,6 +278,14 @@ test("Button states have a mode-complete palette independent from shared actions } }); +test("Empty artwork uses a mode-complete opaque component color", () => { + assert.equal(themes.light["component.empty.media"], "color-mix(in srgb, #6a6a6a 35%, #f7f7f7)"); + assert.equal(themes.dark["component.empty.media"], "color-mix(in srgb, #858585 35%, #0e0e10)"); + for (const values of Object.values(themes)) { + assert.match(values["component.empty.media"], /^color-mix\(in srgb, #[0-9a-f]{6} 35%, #[0-9a-f]{6}\)$/i); + } +}); + test("default modes preserve the built-in Appearance anchor values", () => { assert.equal(themes.light["color.surface.canvas"], "#fdfdfd"); assert.equal(themes.light["color.content.primary"], "rgba(0, 0, 0, 0.80)"); @@ -381,11 +389,15 @@ test("public theme catalog contains only semantic theme tokens for every mode", assert.equal(themeTokenCatalog.length, Object.keys(themes.light).length); for (const token of themeTokenCatalog) { assert.equal( - ["color.", "component.button.", "effect.", "opacity.", "shadow."].some((prefix) => token.name.startsWith(prefix)), + ["color.", "component.button.", "component.empty.", "effect.", "opacity.", "shadow."].some((prefix) => token.name.startsWith(prefix)), true, ); assert.equal(token.name.startsWith("ref."), false); - if (token.name.startsWith("color.") || token.name.startsWith("component.button.")) { + if ( + token.name.startsWith("color.") + || token.name.startsWith("component.button.") + || token.name.startsWith("component.empty.") + ) { assert.equal(token.type, "color"); } assert.deepEqual(Object.keys(token.values), themeModes); diff --git a/design-system/packages/ui/src/components/Empty/Empty.meta.ts b/design-system/packages/ui/src/components/Empty/Empty.meta.ts index d9ab60adf1..130fb858d6 100644 --- a/design-system/packages/ui/src/components/Empty/Empty.meta.ts +++ b/design-system/packages/ui/src/components/Empty/Empty.meta.ts @@ -14,10 +14,18 @@ export const emptyMeta = { ], states: ["default", "with-title", "with-actions"], tokens: [ - "color.content.primary", - "color.content.secondary", + "component.empty.media", "color.content.muted", - "control.height.md", - "control.height.lg", + "layout.empty.gap", + "layout.empty.paddingBlock", + "layout.empty.paddingInline", + "layout.empty.mediaSizeSm", + "layout.empty.mediaSizeMd", + "layout.empty.mediaSizeLg", + "layout.empty.iconSizeSm", + "layout.empty.iconSizeMd", + "layout.empty.iconSizeLg", + "type.label.md.fontSize", + "type.body.sm.fontSize", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/Empty/Empty.module.css b/design-system/packages/ui/src/components/Empty/Empty.module.css index ce637924b9..fc3001e6ea 100644 --- a/design-system/packages/ui/src/components/Empty/Empty.module.css +++ b/design-system/packages/ui/src/components/Empty/Empty.module.css @@ -4,46 +4,55 @@ flex-direction: column; align-items: center; justify-content: center; - gap: var(--openbitfun-space-3); + gap: var(--openbitfun-layout-empty-gap); min-inline-size: 0; - padding: var(--openbitfun-space-8) var(--openbitfun-space-5); - color: var(--openbitfun-color-content-secondary); + padding: var(--openbitfun-layout-empty-padding-block) var(--openbitfun-layout-empty-padding-inline); + color: var(--openbitfun-color-content-muted); text-align: center; } .media { + --_empty-media-size: var(--openbitfun-layout-empty-media-size-md); + --_empty-icon-size: var(--openbitfun-layout-empty-icon-size-md); + display: inline-flex; align-items: center; justify-content: center; - inline-size: var(--openbitfun-control-height-lg); - block-size: var(--openbitfun-control-height-lg); - color: var(--openbitfun-color-content-muted); + inline-size: var(--_empty-media-size); + block-size: var(--_empty-media-size); + color: var(--openbitfun-component-empty-media); } .media[data-size="sm"] { - inline-size: var(--openbitfun-control-height-md); - block-size: var(--openbitfun-control-height-md); + --_empty-media-size: var(--openbitfun-layout-empty-media-size-sm); + --_empty-icon-size: var(--openbitfun-layout-empty-icon-size-sm); } .media[data-size="lg"] { - inline-size: calc(var(--openbitfun-control-height-lg) + var(--openbitfun-space-4)); - block-size: calc(var(--openbitfun-control-height-lg) + var(--openbitfun-space-4)); + --_empty-media-size: var(--openbitfun-layout-empty-media-size-lg); + --_empty-icon-size: var(--openbitfun-layout-empty-icon-size-lg); } .media > :where(svg, img), .media > [data-openbitfun-component="icon"] { display: block; + inline-size: var(--_empty-icon-size); + block-size: var(--_empty-icon-size); max-inline-size: 100%; max-block-size: 100%; } + .media > [data-openbitfun-component="icon"] { + color: inherit; + } + .title { - color: var(--openbitfun-color-content-primary); - font-family: var(--openbitfun-type-heading-card-font-family); - font-size: var(--openbitfun-type-heading-card-font-size); - font-weight: var(--openbitfun-type-heading-card-font-weight); - line-height: var(--openbitfun-type-heading-card-line-height); - letter-spacing: var(--openbitfun-type-heading-card-letter-spacing); + color: var(--openbitfun-color-content-muted); + font-family: var(--openbitfun-type-label-md-font-family); + font-size: var(--openbitfun-type-label-md-font-size); + font-weight: var(--openbitfun-type-label-md-font-weight); + line-height: var(--openbitfun-type-label-md-line-height); + letter-spacing: var(--openbitfun-type-label-md-letter-spacing); } .description { diff --git a/design-system/packages/ui/src/components/Empty/Empty.tsx b/design-system/packages/ui/src/components/Empty/Empty.tsx index 22db3ba1ef..24ebf10f8a 100644 --- a/design-system/packages/ui/src/components/Empty/Empty.tsx +++ b/design-system/packages/ui/src/components/Empty/Empty.tsx @@ -32,7 +32,7 @@ export const Empty = forwardRef(function Empty({ title, ...props }, ref) { - const media = icon ?? image ?? ; + const media = icon ?? image ?? ; const footer = actions ?? children; return ( diff --git a/design-system/packages/ui/tests/empty.test.mjs b/design-system/packages/ui/tests/empty.test.mjs index 45496ea696..65bad5530b 100644 --- a/design-system/packages/ui/tests/empty.test.mjs +++ b/design-system/packages/ui/tests/empty.test.mjs @@ -1,21 +1,48 @@ import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; -import test from "node:test"; import { Empty } from "../dist/index.js"; -test("Empty exposes independent media, copy, and action regions", () => { +test("Empty exposes media, copy, and action anatomy", () => { const markup = renderToStaticMarkup(createElement(Empty, { actions: createElement("button", null, "Create"), - description: "Create an item to get started.", - imageSize: "sm", - title: "No items", + description: "Created items will appear here.", + icon: createElement("svg", { "aria-hidden": true }), + title: "No items yet", })); assert.match(markup, /data-openbitfun-component="empty"/); assert.match(markup, /data-openbitfun-part="media"/); - assert.match(markup, /data-size="sm"/); - assert.match(markup, /data-openbitfun-part="title"/); - assert.match(markup, /data-openbitfun-part="description"/); + assert.match(markup, /data-openbitfun-part="title">No items yet/); + assert.match(markup, /data-openbitfun-part="description">Created items will appear here\./); assert.match(markup, /data-openbitfun-part="actions"/); }); + +test("Empty keeps decorative media and copy on the quiet content hierarchy", async () => { + const styles = await readFile( + new URL("../src/components/Empty/Empty.module.css", import.meta.url), + "utf8", + ); + const media = styles.match(/\.media\s*\{([^}]+)\}/)?.[1]; + const title = styles.match(/\.title\s*\{([^}]+)\}/)?.[1]; + + assert.ok(media); + assert.match(media, /color: var\(--openbitfun-component-empty-media\)/); + assert.match(styles, /gap: var\(--openbitfun-layout-empty-gap\)/); + assert.match(styles, /padding: var\(--openbitfun-layout-empty-padding-block\) var\(--openbitfun-layout-empty-padding-inline\)/); + assert.match(media, /--_empty-media-size: var\(--openbitfun-layout-empty-media-size-md\)/); + assert.match(media, /--_empty-icon-size: var\(--openbitfun-layout-empty-icon-size-md\)/); + assert.match(styles, /\.media\[data-size="sm"\][^{]*\{[^}]*--_empty-icon-size: var\(--openbitfun-layout-empty-icon-size-sm\)/); + assert.match(styles, /\.media\[data-size="lg"\][^{]*\{[^}]*--_empty-icon-size: var\(--openbitfun-layout-empty-icon-size-lg\)/); + assert.match(styles, /inline-size: var\(--_empty-icon-size\)/); + assert.match(styles, /block-size: var\(--_empty-icon-size\)/); + assert.doesNotMatch(styles, /opacity:/); + assert.ok(title); + assert.match(title, /color: var\(--openbitfun-color-content-muted\)/); + assert.match(title, /font-family: var\(--openbitfun-type-label-md-font-family\)/); + assert.match(title, /font-size: var\(--openbitfun-type-label-md-font-size\)/); + assert.match(title, /font-weight: var\(--openbitfun-type-label-md-font-weight\)/); + assert.doesNotMatch(title, /color-content-primary|type-heading-card/); +}); diff --git a/design-system/packages/ui/tests/icon-slots.test.mjs b/design-system/packages/ui/tests/icon-slots.test.mjs index 5633be4d2e..cfbc08f8fe 100644 --- a/design-system/packages/ui/tests/icon-slots.test.mjs +++ b/design-system/packages/ui/tests/icon-slots.test.mjs @@ -23,7 +23,7 @@ const slots = [ ["Select", "indicator", "inline-size", "100%"], ["StatusPill", "leading", "inline-size", "100%"], ["Avatar", "content", "inline-size", "55%"], - ["Empty", "media", "max-inline-size", "100%"], + ["Empty", "media", "inline-size", "var(--_empty-icon-size)"], ["ConfirmDialog", "icon", "inline-size", "var(--openbitfun-layout-confirm-dialog-icon-glyph-size)"], ["Listbox", "leading", "inline-size", "100%"], ["Listbox", "indicator", "inline-size", "100%"], diff --git a/design-system/packages/ui/tests/registry.test.mjs b/design-system/packages/ui/tests/registry.test.mjs index 7030ad9403..50ffe2587a 100644 --- a/design-system/packages/ui/tests/registry.test.mjs +++ b/design-system/packages/ui/tests/registry.test.mjs @@ -126,6 +126,7 @@ test("every registered component declares states and owned tokens", () => { token.startsWith("border.") || token.startsWith("color.") || (component.name === "Button" && token.startsWith("component.button.")) || + (component.name === "Empty" && token.startsWith("component.empty.")) || (component.name === "TabGroup" && [ "component.button.outlineBorder", "component.button.outlineBorderInteractive", diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index 84938d4f42..d7493560f2 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -162,7 +162,11 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { assert.equal( CANONICAL_THEME_COLOR_TOKENS.every(token => ( /^--openbitfun-[a-z0-9-]+$/.test(token.cssVariable) - && (token.name.startsWith('color.') || token.name.startsWith('component.button.')) + && ( + token.name.startsWith('color.') + || token.name.startsWith('component.button.') + || token.name.startsWith('component.empty.') + ) && typeof token.value === 'string' )), true, diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index 808ed6e379..28140c2927 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -22,7 +22,14 @@ const openbitfunThemeTokens = resolveTokens(mergeTokenDocuments( readDesignSystemTokenDocument('design-system/packages/theme-openbitfun/src/reference.tokens.json'), readDesignSystemTokenDocument('design-system/packages/theme-openbitfun/src/light.tokens.json'), )); -const publicThemePrefixes = ['color.', 'component.button.', 'effect.', 'opacity.', 'shadow.']; +const publicThemePrefixes = [ + 'color.', + 'component.button.', + 'component.empty.', + 'effect.', + 'opacity.', + 'shadow.', +]; export const PACKAGE_CSS_VAR_DEFINITION_CONTRACTS = Object.freeze([ Object.freeze({ diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index de6c5ed057..ced99d6e60 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -233,6 +233,15 @@ --openbitfun-layout-disclosure-trigger-padding-block: var(--openbitfun-space-1); --openbitfun-layout-disclosure-trigger-padding-inline: var(--openbitfun-space-2); --openbitfun-layout-disclosure-trigger-radius: var(--openbitfun-radius-base); + --openbitfun-layout-empty-gap: 10px; + --openbitfun-layout-empty-icon-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-icon-size-md: var(--openbitfun-control-height-sm); + --openbitfun-layout-empty-icon-size-sm: var(--openbitfun-control-icon-size-lg); + --openbitfun-layout-empty-media-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-media-size-md: var(--openbitfun-control-height-sm); + --openbitfun-layout-empty-media-size-sm: var(--openbitfun-control-icon-size-lg); + --openbitfun-layout-empty-padding-block: var(--openbitfun-space-8); + --openbitfun-layout-empty-padding-inline: var(--openbitfun-space-4); --openbitfun-layout-field-content-gap: var(--openbitfun-space-1); --openbitfun-layout-field-control-gap: var(--openbitfun-space-2); --openbitfun-layout-field-horizontal-gap: var(--openbitfun-space-5); @@ -673,6 +682,10 @@ --openbitfun-control-search-field-height-md: var(--openbitfun-control-height-md); --openbitfun-control-search-field-height-sm: 30px; --openbitfun-control-tab-group-item-height: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-icon-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-icon-size-md: var(--openbitfun-control-height-sm); + --openbitfun-layout-empty-media-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-media-size-md: var(--openbitfun-control-height-sm); --openbitfun-space-component-block: 6px; --openbitfun-space-component-inline: 10px; } @@ -689,6 +702,10 @@ --openbitfun-control-search-field-height-md: var(--openbitfun-control-height-md); --openbitfun-control-search-field-height-sm: var(--openbitfun-control-height-sm); --openbitfun-control-tab-group-item-height: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-icon-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-icon-size-md: var(--openbitfun-control-height-sm); + --openbitfun-layout-empty-media-size-lg: var(--openbitfun-control-height-md); + --openbitfun-layout-empty-media-size-md: var(--openbitfun-control-height-sm); --openbitfun-space-component-block: 12px; --openbitfun-space-component-inline: 16px; } @@ -843,6 +860,7 @@ --openbitfun-component-button-text-content: #059cb0; --openbitfun-component-button-text-content-disabled: rgba(5, 156, 176, 0.30); --openbitfun-component-button-text-content-hover: #059cb0; + --openbitfun-component-empty-media: color-mix(in srgb, #6a6a6a 35%, #f7f7f7); --openbitfun-effect-blur-base: blur(8px) saturate(1.05); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.02); @@ -997,6 +1015,7 @@ --openbitfun-component-button-text-content: #60a5fa; --openbitfun-component-button-text-content-disabled: rgba(96, 165, 250, 0.30); --openbitfun-component-button-text-content-hover: #3b82f6; + --openbitfun-component-empty-media: color-mix(in srgb, #858585 35%, #0e0e10); --openbitfun-effect-blur-base: blur(8px) saturate(1.1); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.05); @@ -1084,6 +1103,7 @@ --openbitfun-component-button-primary-background-hover: #262626; --openbitfun-component-button-primary-background-pressed: #000000; --openbitfun-component-button-primary-content-disabled: #343434; + --openbitfun-component-empty-media: color-mix(in srgb, #343434 35%, #f7f7f7); } } @@ -1156,5 +1176,6 @@ --openbitfun-component-button-primary-background-hover: #e6e6e6; --openbitfun-component-button-primary-background-pressed: #ffffff; --openbitfun-component-button-primary-content-disabled: #d0d0d0; + --openbitfun-component-empty-media: color-mix(in srgb, #d0d0d0 35%, #000000); } } diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css index 408b17568d..56a21f7619 100644 --- a/src/apps/desktop/src/generated/bootstrap_theme.css +++ b/src/apps/desktop/src/generated/bootstrap_theme.css @@ -131,6 +131,7 @@ --openbitfun-component-button-text-content: #60a5fa; --openbitfun-component-button-text-content-disabled: rgba(96, 165, 250, 0.30); --openbitfun-component-button-text-content-hover: #3b82f6; + --openbitfun-component-empty-media: color-mix(in srgb, #858585 35%, #0e0e10); --openbitfun-effect-blur-base: blur(8px) saturate(1.1); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.05); diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx index 884eb58f1b..de53a3bbaf 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx @@ -694,7 +694,7 @@ const AssistantDefaultsPage: React.FC = () => { const renderEmptyState = (message: string) => (
} + icon={} description={message} actions={(searchQuery || statusFilter !== 'all') ? (
) : !hasEntries && loadFailures.length === 0 ? (
- - {t('nav.sessions.noArchivedSessions')} + } + title={t('nav.sessions.noArchivedSessions')} + />
) : hasEntries ? ( <> diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx index 12229b7958..42a86be1bb 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx @@ -800,7 +800,7 @@ const SkillsScene: React.FC = () => { } + icon={} description={market.marketError} actions={} data-openbitfun-scene="skills" @@ -811,7 +811,7 @@ const SkillsScene: React.FC = () => { {!market.marketLoading && !market.marketError && !market.loadingMore && market.marketSkills.length === 0 && ( } + icon={} description={marketQuery ? t('market.empty.noMatch') : t('market.empty.noSkills')} data-testid="skill-list-empty" data-openbitfun-scene="skills" diff --git a/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx b/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx index e769091cc5..5f141f86ab 100644 --- a/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx +++ b/src/web-ui/src/infrastructure/config/components/AppearanceMarketWorkflows.tsx @@ -415,7 +415,7 @@ export function AppearanceMarketWorkflows({ workflow }: AppearanceMarketWorkflow : submissions.length === 0 ? (