mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(theme): keep inverted text opaque over background images
This commit is contained in:
@@ -4498,7 +4498,7 @@ onUnmounted(() => {
|
||||
@animationend="finishSqlLibraryFlyAnimation(sqlLibraryFlyAnimation.id)"
|
||||
/>
|
||||
<Transition name="toast">
|
||||
<div v-if="toastVisible" class="fixed bottom-6 inset-x-0 mx-auto z-99999 w-max max-w-[90vw] sm:max-w-3xl px-4 py-2 rounded-lg bg-foreground text-background text-sm shadow-lg select-text whitespace-pre-wrap break-words">
|
||||
<div v-if="toastVisible" class="fixed bottom-6 inset-x-0 mx-auto z-99999 w-max max-w-[90vw] sm:max-w-3xl px-4 py-2 rounded-lg bg-foreground text-background-solid text-sm shadow-lg select-text whitespace-pre-wrap break-words">
|
||||
<span>{{ toastMessage }}</span>
|
||||
<button v-if="toastAction" type="button" class="ml-3 shrink-0 rounded border border-background/40 bg-background/10 px-2 py-0.5 text-xs font-medium hover:bg-background/20" @click="toastAction.onClick()">
|
||||
{{ toastAction.label }}
|
||||
|
||||
@@ -6901,7 +6901,7 @@ function openExternalUrl(url: string) {
|
||||
<span>{{ t(option.labelKey) }}</span>
|
||||
<Badge v-if="option.recommended" class="h-4 rounded-full px-1.5 text-[10px] leading-none">{{ t("connection.sqliteWorkerPlacementDefault") }}</Badge>
|
||||
</div>
|
||||
<p class="text-[11px] leading-relaxed text-background/80">{{ t(option.hintKey) }}</p>
|
||||
<p class="text-[11px] leading-relaxed text-background-solid/80">{{ t(option.hintKey) }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -5985,11 +5985,11 @@ async function openExternalUrl(url: string) {
|
||||
<button v-if="isGenerating" class="h-7 w-7 shrink-0 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center" :title="t('ai.stopGenerating')" @click="cancelStream">
|
||||
<Square class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button v-else-if="hasActiveRunForCurrentConversation" class="h-7 shrink-0 items-center gap-1 rounded-full bg-foreground px-2.5 text-[11px] font-medium text-background disabled:opacity-30 flex" :disabled="!canSubmitPrompt" :title="t('ai.queueSendHint')" @click="onSendClick">
|
||||
<button v-else-if="hasActiveRunForCurrentConversation" class="h-7 shrink-0 items-center gap-1 rounded-full bg-foreground px-2.5 text-[11px] font-medium text-background-solid disabled:opacity-30 flex" :disabled="!canSubmitPrompt" :title="t('ai.queueSendHint')" @click="onSendClick">
|
||||
<Hourglass class="h-3.5 w-3.5" />
|
||||
<span>{{ t("ai.queueSend") }}</span>
|
||||
</button>
|
||||
<button v-else class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background flex items-center justify-center disabled:opacity-30" :disabled="!canSubmitPrompt" @click="send">
|
||||
<button v-else class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background-solid flex items-center justify-center disabled:opacity-30" :disabled="!canSubmitPrompt" @click="send">
|
||||
<ArrowUp class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ const emit = defineEmits<{
|
||||
</span>
|
||||
<template #content>
|
||||
<div class="dbx-column-info-tooltip grid min-w-56 grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-1 px-3 py-2">
|
||||
<span class="text-background/70">{{ columnNameLabel }}</span>
|
||||
<span class="text-background-solid/70">{{ columnNameLabel }}</span>
|
||||
<span class="flex min-w-0 items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-mono">{{ name }}</span>
|
||||
<button data-column-header-copy-name type="button" class="flex h-5 w-5 shrink-0 items-center justify-center rounded hover:bg-background/10" :title="copyColumnNameLabel" @click.stop="emit('copyName')">
|
||||
@@ -96,19 +96,19 @@ const emit = defineEmits<{
|
||||
</button>
|
||||
</span>
|
||||
<template v-if="tooltipColumnType ?? columnType">
|
||||
<span class="text-background/70">{{ columnTypeLabel }}</span>
|
||||
<span class="text-background-solid/70">{{ columnTypeLabel }}</span>
|
||||
<span :class="typeClass">{{ tooltipColumnType ?? columnType }}</span>
|
||||
</template>
|
||||
<template v-if="tooltipColumnComment ?? columnComment">
|
||||
<span class="text-background/70">{{ columnCommentLabel }}</span>
|
||||
<span class="text-background-solid/70">{{ columnCommentLabel }}</span>
|
||||
<span>{{ tooltipColumnComment ?? columnComment }}</span>
|
||||
</template>
|
||||
<template v-if="columnNullability">
|
||||
<span class="text-background/70">{{ nullableLabel }}</span>
|
||||
<span class="text-background-solid/70">{{ nullableLabel }}</span>
|
||||
<span>{{ columnNullability === "nullable" ? yesLabel : noLabel }}</span>
|
||||
</template>
|
||||
<template v-if="columnIndexKind && columnIndexKind !== 'none'">
|
||||
<span class="text-background/70">{{ columnIndexLabel }}</span>
|
||||
<span class="text-background-solid/70">{{ columnIndexLabel }}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<KeyRound v-if="columnIndexKind === 'primary'" class="h-3 w-3" :class="columnIndexColorClass(columnIndexKind)" />
|
||||
<Hash v-else class="h-3 w-3" :class="columnIndexColorClass(columnIndexKind)" />
|
||||
|
||||
@@ -702,7 +702,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
|
||||
<Teleport to="body">
|
||||
<div v-if="historyPreview" class="pointer-events-none fixed z-[140] rounded-md bg-foreground shadow-xl" :style="previewStyle">
|
||||
<span class="absolute h-3 w-3 rotate-45 bg-foreground" :class="historyPreview.side === 'left' ? '-left-1.5' : '-right-1.5'" :style="previewArrowStyle" />
|
||||
<div class="max-h-[min(320px,calc(100vh-16px))] overflow-auto rounded-md px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words text-background">{{ historyPreview.value }}</div>
|
||||
<div class="max-h-[min(320px,calc(100vh-16px))] overflow-auto rounded-md px-3 py-2 font-mono text-xs leading-relaxed whitespace-pre-wrap break-words text-background-solid">{{ historyPreview.value }}</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
@@ -3069,7 +3069,7 @@ useUpdateBlocker(() =>
|
||||
:key="format"
|
||||
type="button"
|
||||
class="shrink-0 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors"
|
||||
:class="configType === format ? 'border-foreground/80 bg-foreground text-background' : 'border-transparent text-muted-foreground hover:border-border hover:bg-background hover:text-foreground'"
|
||||
:class="configType === format ? 'border-foreground/80 bg-foreground text-background-solid' : 'border-transparent text-muted-foreground hover:border-border hover:bg-background hover:text-foreground'"
|
||||
:disabled="readOnly"
|
||||
:aria-pressed="configType === format"
|
||||
@click="setConfigFormat(format)"
|
||||
|
||||
@@ -79,7 +79,7 @@ const arrowClass = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const tooltipSurfaceClass = computed(() => (props.surface === "popover" ? "bg-popover text-popover-foreground" : "bg-foreground text-background"));
|
||||
const tooltipSurfaceClass = computed(() => (props.surface === "popover" ? "bg-popover text-popover-foreground" : "bg-foreground text-background-solid"));
|
||||
|
||||
const arrowSurfaceClass = computed(() => (props.surface === "popover" ? "bg-popover border-border" : "bg-foreground border-foreground"));
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm bg-foreground text-background z-50 w-fit max-w-xs origin-(--reka-tooltip-content-transform-origin)',
|
||||
'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm bg-foreground text-background-solid z-50 w-fit max-w-xs origin-(--reka-tooltip-content-transform-origin)',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -86,6 +86,45 @@ describe("useBackgroundImage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an opaque companion for inverted text while the surfaces turn translucent", async () => {
|
||||
const theme = useTheme();
|
||||
const originalMode = theme.themeMode.value;
|
||||
const originalPalette = theme.themePalette.value;
|
||||
const originalColors = { ...theme.customUiColors.value };
|
||||
theme.setThemeMode("light");
|
||||
theme.setCustomUiColors({ ...originalColors, background: "#123456" });
|
||||
theme.setThemePalette("custom");
|
||||
const createObjectURL = vi.fn(() => "blob:bg-solid");
|
||||
Object.defineProperty(URL, "createObjectURL", { value: createObjectURL, configurable: true, writable: true });
|
||||
readBackgroundImageMock.mockResolvedValue("aGVsbG8=");
|
||||
try {
|
||||
const settings = settingsWith({ filePath: "/data/background-image.png", opacity: 0.3 });
|
||||
const bg = useBackgroundImage(settings);
|
||||
await vi.waitFor(() => expect(bg.active.value).toBe(true));
|
||||
await vi.waitFor(() => {
|
||||
// The surface itself goes translucent so the wallpaper shows through...
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(18 52 86 / 0.3)");
|
||||
// ...but the color used as text on `bg-foreground` widgets stays opaque,
|
||||
// otherwise tooltip/toast glyphs wash out (#8678).
|
||||
expect(document.documentElement.style.getPropertyValue("--background-solid")).toBe("rgb(18 52 86)");
|
||||
});
|
||||
// No other surface grows a companion: only the inverted-text vars do.
|
||||
expect(document.documentElement.style.getPropertyValue("--sidebar-solid")).toBe("");
|
||||
|
||||
settings.editorSettings.backgroundImage = { ...defaultBackgroundImageSettings() };
|
||||
await vi.waitFor(() => expect(bg.active.value).toBe(false));
|
||||
await vi.waitFor(() => {
|
||||
expect(document.documentElement.style.getPropertyValue("--background-solid")).toBe("");
|
||||
});
|
||||
// Inactive: the custom palette's own inline value is restored untouched.
|
||||
expect(document.documentElement.style.getPropertyValue("--background")).toBe("rgb(18 52 86)");
|
||||
} finally {
|
||||
theme.setCustomUiColors(originalColors);
|
||||
theme.setThemeMode(originalMode);
|
||||
theme.setThemePalette(originalPalette);
|
||||
}
|
||||
});
|
||||
|
||||
it("re-emits custom palette inline surface colors when the wallpaper is inactive", async () => {
|
||||
const theme = useTheme();
|
||||
const originalMode = theme.themeMode.value;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, ref, watch, watchPostEffect, type ComputedRef, type Ref } from "vue";
|
||||
import { BACKGROUND_IMAGE_SURFACE_VARS, backgroundImageStyle, backgroundImageSurfaceAlpha, surfaceColorWithAlpha, type BackgroundImageSettings } from "@/lib/app/appBackgroundImage";
|
||||
import { BACKGROUND_IMAGE_INVERTED_TEXT_VARS, BACKGROUND_IMAGE_SURFACE_VARS, backgroundImageSolidVarName, backgroundImageStyle, backgroundImageSurfaceAlpha, surfaceColorWithAlpha, type BackgroundImageSettings } from "@/lib/app/appBackgroundImage";
|
||||
import { APP_CUSTOM_UI_COLOR_DEFS, appCustomUiColorValue, deriveCustomUiColors } from "@/lib/app/appTheme";
|
||||
import { readBackgroundImage } from "@/lib/backend/api";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
@@ -112,7 +112,7 @@ export function useBackgroundImage(settingsStore: SettingsStoreLike): Background
|
||||
const alpha = backgroundImageSurfaceAlpha(backgroundSettings.value);
|
||||
const isActive = active.value;
|
||||
const customBase = customSurfaceBaseColors();
|
||||
for (const varName of BACKGROUND_IMAGE_SURFACE_VARS) {
|
||||
for (const varName of [...BACKGROUND_IMAGE_SURFACE_VARS, ...BACKGROUND_IMAGE_INVERTED_TEXT_VARS.map(backgroundImageSolidVarName)]) {
|
||||
doc.style.removeProperty(varName);
|
||||
}
|
||||
doc.classList.toggle(BACKGROUND_IMAGE_ACTIVE_CLASS, isActive);
|
||||
@@ -125,10 +125,16 @@ export function useBackgroundImage(settingsStore: SettingsStoreLike): Background
|
||||
}
|
||||
const computedStyle = getComputedStyle(doc);
|
||||
for (const varName of BACKGROUND_IMAGE_SURFACE_VARS) {
|
||||
const base = customBase.get(varName) ?? computedStyle.getPropertyValue(varName);
|
||||
const base = (customBase.get(varName) ?? computedStyle.getPropertyValue(varName)).trim();
|
||||
const tinted = surfaceColorWithAlpha(base, alpha);
|
||||
if (tinted) doc.style.setProperty(varName, tinted);
|
||||
}
|
||||
// Inverted-text surfaces (`bg-foreground text-background`) keep the opaque
|
||||
// palette color: their glyphs must not inherit the wallpaper alpha.
|
||||
for (const varName of BACKGROUND_IMAGE_INVERTED_TEXT_VARS) {
|
||||
const base = (customBase.get(varName) ?? computedStyle.getPropertyValue(varName)).trim();
|
||||
if (base) doc.style.setProperty(backgroundImageSolidVarName(varName), base);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BACKGROUND_IMAGE_FILE_EXTENSIONS,
|
||||
BACKGROUND_IMAGE_INVERTED_TEXT_VARS,
|
||||
BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES,
|
||||
BACKGROUND_IMAGE_SURFACE_VARS,
|
||||
backgroundImageFileExtension,
|
||||
backgroundImageSolidVarName,
|
||||
backgroundImageStyle,
|
||||
backgroundImageSurfaceAlpha,
|
||||
defaultBackgroundImageSettings,
|
||||
@@ -149,6 +151,16 @@ describe("surfaceColorWithAlpha", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("inverted-text surface vars", () => {
|
||||
it("only marks surface vars that turn translucent, and names their opaque companion", () => {
|
||||
for (const varName of BACKGROUND_IMAGE_INVERTED_TEXT_VARS) {
|
||||
expect(BACKGROUND_IMAGE_SURFACE_VARS).toContain(varName);
|
||||
expect(backgroundImageSolidVarName(varName)).toBe(`${varName}-solid`);
|
||||
}
|
||||
expect(BACKGROUND_IMAGE_INVERTED_TEXT_VARS).toContain("--background");
|
||||
});
|
||||
});
|
||||
|
||||
describe("BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES", () => {
|
||||
it("is 20 MB", () => {
|
||||
expect(BACKGROUND_IMAGE_STORAGE_LIMIT_BYTES).toBe(20 * 1024 * 1024);
|
||||
|
||||
@@ -113,6 +113,25 @@ export function backgroundImageStyle(settings: Pick<BackgroundImageSettings, "di
|
||||
*/
|
||||
export const BACKGROUND_IMAGE_SURFACE_VARS = ["--background", "--sidebar", "--muted", "--secondary", "--accent", "--dbx-chrome", "--dbx-chrome-muted", "--dbx-content", "--dbx-editor-toolbar", "--dbx-gutter", "--dbx-sidebar-header"] as const;
|
||||
|
||||
/**
|
||||
* Surface variables that are also consumed as *foreground* colors, and
|
||||
* therefore need an opaque companion while the surface itself turns
|
||||
* translucent.
|
||||
*
|
||||
* `--background` backs `text-background`, the inverted text painted on
|
||||
* `bg-foreground` widgets (tooltips, toasts, grid chips). Tinting it for the
|
||||
* wallpaper also tints those glyphs, so a toolbar tooltip ends up with washed
|
||||
* out gray text on its own opaque bubble — reported as "the popup background
|
||||
* and the text color merged" in #8678. Consumers of the inverted text read
|
||||
* `text-background-solid` (tokens.css) instead.
|
||||
*/
|
||||
export const BACKGROUND_IMAGE_INVERTED_TEXT_VARS = ["--background"] as const;
|
||||
|
||||
/** Opaque companion variable published for an inverted-text surface var. */
|
||||
export function backgroundImageSolidVarName(varName: string): string {
|
||||
return `${varName}-solid`;
|
||||
}
|
||||
|
||||
/** Alpha for the surface variables derived from the surface-opacity setting. */
|
||||
export function backgroundImageSurfaceAlpha(settings: Pick<BackgroundImageSettings, "opacity">): number {
|
||||
return clampNumber(settings.opacity, BACKGROUND_IMAGE_MIN_OPACITY, BACKGROUND_IMAGE_MAX_OPACITY, BACKGROUND_IMAGE_DEFAULT_OPACITY);
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
/* Opaque background for inverted text (`bg-foreground text-background-solid`).
|
||||
useBackgroundImage publishes `--background-solid` while a wallpaper is active,
|
||||
so tooltips/toasts keep readable glyphs when the surfaces turn translucent. */
|
||||
--color-background-solid: var(--background-solid, var(--background));
|
||||
--radius-sm: var(--dbx-radius-sm);
|
||||
--radius-md: var(--dbx-radius-md);
|
||||
--radius-lg: var(--dbx-radius-lg);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
Reference in New Issue
Block a user