| author | |
| committer | |
| log | 362f12792254335bfe2a015787d7cbbaa214654e |
| tree | e1871e0fd30bf60cd335184098a130b2f1455113 |
| parent | 9cf9fc9c5c0f48d42f407f86cf83d0f04fbdf5a8 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 195 insertions(+), 29 deletions(-)
apps/desktop/src/settings/DesktopClientSettings.test.ts+1| ... | @@ -18,6 +18,7 @@ const clientSettings: ClientSettings = { | ... | @@ -18,6 +18,7 @@ const clientSettings: ClientSettings = { |
| 18 | confirmThreadDelete: false, | 18 | confirmThreadDelete: false, |
| 19 | dismissedProviderUpdateNotificationKeys: [], | 19 | dismissedProviderUpdateNotificationKeys: [], |
| 20 | diffIgnoreWhitespace: true, | 20 | diffIgnoreWhitespace: true, |
| 21 | environmentIdentificationMode: "artwork", | ||
| 21 | favorites: [], | 22 | favorites: [], |
| 22 | glassOpacity: 80, | 23 | glassOpacity: 80, |
| 23 | providerModelPreferences: {}, | 24 | providerModelPreferences: {}, |
apps/web/src/components/AppSidebarLayout.tsx+5-2| ... | @@ -14,7 +14,7 @@ import { getLocalStorageItem } from "../hooks/useLocalStorage"; | ... | @@ -14,7 +14,7 @@ import { getLocalStorageItem } from "../hooks/useLocalStorage"; |
| 14 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; | 14 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 15 | import { cn, isMacPlatform } from "../lib/utils"; | 15 | import { cn, isMacPlatform } from "../lib/utils"; |
| 16 | import { primaryServerKeybindingsAtom } from "../state/server"; | 16 | import { primaryServerKeybindingsAtom } from "../state/server"; |
| 17 | import { useSidebarV2Enabled } from "../hooks/useSettings"; | 17 | import { useEnvironmentIdentificationMode, useSidebarV2Enabled } from "../hooks/useSettings"; |
| 18 | import ThreadSidebar from "./Sidebar"; | 18 | import ThreadSidebar from "./Sidebar"; |
| 19 | import ThreadSidebarV2 from "./SidebarV2"; | 19 | import ThreadSidebarV2 from "./SidebarV2"; |
| 20 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; | 20 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; |
| ... | @@ -62,7 +62,10 @@ function SidebarControl() { | ... | @@ -62,7 +62,10 @@ function SidebarControl() { |
| 62 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | 62 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 63 | const { toggleSidebar } = useSidebar(); | 63 | const { toggleSidebar } = useSidebar(); |
| 64 | const isSidebarVisible = useSidebarVisibility(); | 64 | const isSidebarVisible = useSidebarVisibility(); |
| 65 | const stageBackdropVariant = useSidebarStageBackdropVariant(); | 65 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); |
| 66 | const stageBackdropVariant = useSidebarStageBackdropVariant( | ||
| 67 | environmentIdentificationMode === "artwork", | ||
| 68 | ); | ||
| 66 | const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); | 69 | const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); |
| 67 | 70 | ||
| 68 | useEffect(() => { | 71 | useEffect(() => { |
apps/web/src/components/SidebarStageBackdrop.test.tsx+20-1| ... | @@ -1,9 +1,28 @@ | ... | @@ -1,9 +1,28 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | 2 | import { renderToStaticMarkup } from "react-dom/server"; |
| 3 | 3 | ||
| 4 | import { StageBackdropArt, StageBackdropButtonArt } from "./SidebarStageBackdrop"; | 4 | import { |
| 5 | resolveEnvironmentIdentificationPillLabel, | ||
| 6 | resolveSidebarStageBackdropVariant, | ||
| 7 | StageBackdropArt, | ||
| 8 | StageBackdropButtonArt, | ||
| 9 | } from "./SidebarStageBackdrop"; | ||
| 5 | 10 | ||
| 6 | describe("SidebarStageBackdrop", () => { | 11 | describe("SidebarStageBackdrop", () => { |
| 12 | it("resolves stage artwork only when enabled", () => { | ||
| 13 | expect(resolveSidebarStageBackdropVariant("Dev")).toBe("dev"); | ||
| 14 | expect(resolveSidebarStageBackdropVariant("Nightly")).toBe("nightly"); | ||
| 15 | expect(resolveSidebarStageBackdropVariant("Dev", false)).toBeNull(); | ||
| 16 | expect(resolveSidebarStageBackdropVariant("Alpha")).toBeNull(); | ||
| 17 | }); | ||
| 18 | |||
| 19 | it("resolves supported environment pill labels", () => { | ||
| 20 | expect(resolveEnvironmentIdentificationPillLabel("Dev")).toBe("Dev"); | ||
| 21 | expect(resolveEnvironmentIdentificationPillLabel("nightly")).toBe("Nightly"); | ||
| 22 | expect(resolveEnvironmentIdentificationPillLabel("Latest")).toBeNull(); | ||
| 23 | expect(resolveEnvironmentIdentificationPillLabel("Alpha")).toBeNull(); | ||
| 24 | }); | ||
| 25 | |||
| 7 | it.each(["nightly", "dev"] as const)( | 26 | it.each(["nightly", "dev"] as const)( |
| 8 | "uses unique SVG definition ids when %s artwork is rendered more than once", | 27 | "uses unique SVG definition ids when %s artwork is rendered more than once", |
| 9 | (variant) => { | 28 | (variant) => { |
apps/web/src/components/SidebarStageBackdrop.tsx+21-7| ... | @@ -6,6 +6,7 @@ import { resolveServerBackedAppStageLabel } from "../branding.logic"; | ... | @@ -6,6 +6,7 @@ import { resolveServerBackedAppStageLabel } from "../branding.logic"; |
| 6 | import { primaryServerConfigAtom } from "../state/server"; | 6 | import { primaryServerConfigAtom } from "../state/server"; |
| 7 | 7 | ||
| 8 | export type SidebarStageBackdropVariant = "nightly" | "dev"; | 8 | export type SidebarStageBackdropVariant = "nightly" | "dev"; |
| 9 | export type EnvironmentIdentificationPillLabel = "Dev" | "Nightly"; | ||
| 9 | 10 | ||
| 10 | // A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals | 11 | // A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals |
| 11 | // more horizontal canvas instead of zooming the scene. | 12 | // more horizontal canvas instead of zooming the scene. |
| ... | @@ -13,23 +14,36 @@ const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; | ... | @@ -13,23 +14,36 @@ const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; |
| 13 | 14 | ||
| 14 | export function resolveSidebarStageBackdropVariant( | 15 | export function resolveSidebarStageBackdropVariant( |
| 15 | stageLabel: string, | 16 | stageLabel: string, |
| 17 | enabled = true, | ||
| 16 | ): SidebarStageBackdropVariant | null { | 18 | ): SidebarStageBackdropVariant | null { |
| 19 | if (!enabled) return null; | ||
| 17 | const normalized = stageLabel.trim().toLowerCase(); | 20 | const normalized = stageLabel.trim().toLowerCase(); |
| 18 | if (normalized === "nightly") return "nightly"; | 21 | if (normalized === "nightly") return "nightly"; |
| 19 | if (normalized === "dev") return "dev"; | 22 | if (normalized === "dev") return "dev"; |
| 20 | return null; | 23 | return null; |
| 21 | } | 24 | } |
| 22 | 25 | ||
| 23 | export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null { | 26 | export function resolveEnvironmentIdentificationPillLabel( |
| 27 | stageLabel: string, | ||
| 28 | ): EnvironmentIdentificationPillLabel | null { | ||
| 29 | const normalized = stageLabel.trim().toLowerCase(); | ||
| 30 | if (normalized === "dev") return "Dev"; | ||
| 31 | if (normalized === "nightly") return "Nightly"; | ||
| 32 | return null; | ||
| 33 | } | ||
| 34 | |||
| 35 | export function useEnvironmentStageLabel(): string { | ||
| 24 | const primaryServerVersion = | 36 | const primaryServerVersion = |
| 25 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; | 37 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; |
| 26 | 38 | ||
| 27 | return resolveSidebarStageBackdropVariant( | 39 | return resolveServerBackedAppStageLabel({ |
| 28 | resolveServerBackedAppStageLabel({ | 40 | primaryServerVersion, |
| 29 | primaryServerVersion, | 41 | fallbackStageLabel: APP_STAGE_LABEL, |
| 30 | fallbackStageLabel: APP_STAGE_LABEL, | 42 | }); |
| 31 | }), | 43 | } |
| 32 | ); | 44 | |
| 45 | export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null { | ||
| 46 | return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled); | ||
| 33 | } | 47 | } |
| 34 | 48 | ||
| 35 | /** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ | 49 | /** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ |
apps/web/src/components/chat/ComposerPrimaryActions.tsx+5-1| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import { memo, type PointerEventHandler } from "react"; | 1 | import { memo, type PointerEventHandler } from "react"; |
| 2 | import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react"; | 2 | import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react"; |
| 3 | import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; | ||
| 3 | import { cn } from "~/lib/utils"; | 4 | import { cn } from "~/lib/utils"; |
| 4 | import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; | 5 | import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; |
| 5 | import { Button } from "../ui/button"; | 6 | import { Button } from "../ui/button"; |
| ... | @@ -72,7 +73,10 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ | ... | @@ -72,7 +73,10 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 72 | const pointerFocusProps = preserveComposerFocusOnPointerDown | 73 | const pointerFocusProps = preserveComposerFocusOnPointerDown |
| 73 | ? { onPointerDown: preventPointerFocus } | 74 | ? { onPointerDown: preventPointerFocus } |
| 74 | : undefined; | 75 | : undefined; |
| 75 | const stageBackdropVariant = useSidebarStageBackdropVariant(); | 76 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); |
| 77 | const stageBackdropVariant = useSidebarStageBackdropVariant( | ||
| 78 | environmentIdentificationMode === "artwork", | ||
| 79 | ); | ||
| 76 | 80 | ||
| 77 | if (pendingAction) { | 81 | if (pendingAction) { |
| 78 | return ( | 82 | return ( |
apps/web/src/components/settings/SettingsPanels.tsx+63| ... | @@ -21,7 +21,9 @@ import { | ... | @@ -21,7 +21,9 @@ import { |
| 21 | squashAtomCommandFailure, | 21 | squashAtomCommandFailure, |
| 22 | } from "@t3tools/client-runtime/state/runtime"; | 22 | } from "@t3tools/client-runtime/state/runtime"; |
| 23 | import { | 23 | import { |
| 24 | DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, | ||
| 24 | DEFAULT_UNIFIED_SETTINGS, | 25 | DEFAULT_UNIFIED_SETTINGS, |
| 26 | type EnvironmentIdentificationMode, | ||
| 25 | MAX_GLASS_OPACITY, | 27 | MAX_GLASS_OPACITY, |
| 26 | MIN_GLASS_OPACITY, | 28 | MIN_GLASS_OPACITY, |
| 27 | } from "@t3tools/contracts/settings"; | 29 | } from "@t3tools/contracts/settings"; |
| ... | @@ -40,6 +42,10 @@ import { | ... | @@ -40,6 +42,10 @@ import { |
| 40 | } from "../../components/desktopUpdate.logic"; | 42 | } from "../../components/desktopUpdate.logic"; |
| 41 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; | 43 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; |
| 42 | import { TraitsPicker } from "../chat/TraitsPicker"; | 44 | import { TraitsPicker } from "../chat/TraitsPicker"; |
| 45 | import { | ||
| 46 | resolveEnvironmentIdentificationPillLabel, | ||
| 47 | useEnvironmentStageLabel, | ||
| 48 | } from "../SidebarStageBackdrop"; | ||
| 43 | import { isElectron } from "../../env"; | 49 | import { isElectron } from "../../env"; |
| 44 | import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; | 50 | import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; |
| 45 | import { useTheme } from "../../hooks/useTheme"; | 51 | import { useTheme } from "../../hooks/useTheme"; |
| ... | @@ -114,6 +120,12 @@ const THEME_OPTIONS = [ | ... | @@ -114,6 +120,12 @@ const THEME_OPTIONS = [ |
| 114 | }, | 120 | }, |
| 115 | ] as const; | 121 | ] as const; |
| 116 | 122 | ||
| 123 | const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { | ||
| 124 | artwork: "Artwork", | ||
| 125 | pill: "Version pill", | ||
| 126 | none: "None", | ||
| 127 | }; | ||
| 128 | |||
| 117 | const TIMESTAMP_FORMAT_LABELS = { | 129 | const TIMESTAMP_FORMAT_LABELS = { |
| 118 | locale: "System default", | 130 | locale: "System default", |
| 119 | "12-hour": "12-hour", | 131 | "12-hour": "12-hour", |
| ... | @@ -401,6 +413,10 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -401,6 +413,10 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 401 | () => [ | 413 | () => [ |
| 402 | ...(theme !== "system" ? ["Theme"] : []), | 414 | ...(theme !== "system" ? ["Theme"] : []), |
| 403 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), | 415 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), |
| 416 | ...(settings.environmentIdentificationMode !== | ||
| 417 | DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode | ||
| 418 | ? ["Environment identification"] | ||
| 419 | : []), | ||
| 404 | ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat | 420 | ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat |
| 405 | ? ["Time format"] | 421 | ? ["Time format"] |
| 406 | : []), | 422 | : []), |
| ... | @@ -456,6 +472,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -456,6 +472,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 456 | settings.defaultThreadEnvMode, | 472 | settings.defaultThreadEnvMode, |
| 457 | settings.newWorktreesStartFromOrigin, | 473 | settings.newWorktreesStartFromOrigin, |
| 458 | settings.diffIgnoreWhitespace, | 474 | settings.diffIgnoreWhitespace, |
| 475 | settings.environmentIdentificationMode, | ||
| 459 | settings.glassOpacity, | 476 | settings.glassOpacity, |
| 460 | settings.automaticGitFetchInterval, | 477 | settings.automaticGitFetchInterval, |
| 461 | settings.enableAssistantStreaming, | 478 | settings.enableAssistantStreaming, |
| ... | @@ -483,6 +500,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -483,6 +500,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 483 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, | 500 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 484 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, | 501 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 485 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, | 502 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| 503 | environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, | ||
| 486 | glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, | 504 | glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, |
| 487 | sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, | 505 | sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, |
| 488 | sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, | 506 | sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, |
| ... | @@ -510,6 +528,9 @@ export function GeneralSettingsPanel() { | ... | @@ -510,6 +528,9 @@ export function GeneralSettingsPanel() { |
| 510 | const { theme, setTheme } = useTheme(); | 528 | const { theme, setTheme } = useTheme(); |
| 511 | const settings = usePrimarySettings(); | 529 | const settings = usePrimarySettings(); |
| 512 | const updateSettings = useUpdatePrimarySettings(); | 530 | const updateSettings = useUpdatePrimarySettings(); |
| 531 | const environmentStageLabel = useEnvironmentStageLabel(); | ||
| 532 | const showEnvironmentIdentification = | ||
| 533 | resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null; | ||
| 513 | const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>( | 534 | const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>( |
| 514 | readLastEnabledProjectGroupingMode(), | 535 | readLastEnabledProjectGroupingMode(), |
| 515 | ); | 536 | ); |
| ... | @@ -634,6 +655,48 @@ export function GeneralSettingsPanel() { | ... | @@ -634,6 +655,48 @@ export function GeneralSettingsPanel() { |
| 634 | } | 655 | } |
| 635 | /> | 656 | /> |
| 636 | 657 | ||
| 658 | {showEnvironmentIdentification ? ( | ||
| 659 | <SettingsRow | ||
| 660 | title="Environment identification" | ||
| 661 | description="Choose how Dev and Nightly environments are identified." | ||
| 662 | resetAction={ | ||
| 663 | settings.environmentIdentificationMode !== DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE ? ( | ||
| 664 | <SettingResetButton | ||
| 665 | label="environment identification" | ||
| 666 | onClick={() => | ||
| 667 | updateSettings({ | ||
| 668 | environmentIdentificationMode: DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, | ||
| 669 | }) | ||
| 670 | } | ||
| 671 | /> | ||
| 672 | ) : null | ||
| 673 | } | ||
| 674 | control={ | ||
| 675 | <Select | ||
| 676 | value={settings.environmentIdentificationMode} | ||
| 677 | onValueChange={(value) => { | ||
| 678 | if (value === "artwork" || value === "pill" || value === "none") { | ||
| 679 | updateSettings({ environmentIdentificationMode: value }); | ||
| 680 | } | ||
| 681 | }} | ||
| 682 | > | ||
| 683 | <SelectTrigger className="w-full sm:w-40" aria-label="Environment identification"> | ||
| 684 | <SelectValue> | ||
| 685 | {ENVIRONMENT_IDENTIFICATION_LABELS[settings.environmentIdentificationMode]} | ||
| 686 | </SelectValue> | ||
| 687 | </SelectTrigger> | ||
| 688 | <SelectPopup align="end" alignItemWithTrigger={false}> | ||
| 689 | {Object.entries(ENVIRONMENT_IDENTIFICATION_LABELS).map(([value, label]) => ( | ||
| 690 | <SelectItem hideIndicator key={value} value={value}> | ||
| 691 | {label} | ||
| 692 | </SelectItem> | ||
| 693 | ))} | ||
| 694 | </SelectPopup> | ||
| 695 | </Select> | ||
| 696 | } | ||
| 697 | /> | ||
| 698 | ) : null} | ||
| 699 | |||
| 637 | <SettingsRow | 700 | <SettingsRow |
| 638 | title="Project Grouping" | 701 | title="Project Grouping" |
| 639 | description="Combine matching repositories across environments." | 702 | description="Combine matching repositories across environments." |
apps/web/src/components/sidebar/SidebarChrome.tsx+28-17| ... | @@ -1,13 +1,16 @@ | ... | @@ -1,13 +1,16 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | ||
| 2 | import { SettingsIcon } from "lucide-react"; | 1 | import { SettingsIcon } from "lucide-react"; |
| 3 | import { memo, useCallback } from "react"; | 2 | import { memo, useCallback } from "react"; |
| 4 | import { Link, useNavigate } from "@tanstack/react-router"; | 3 | import { Link, useNavigate } from "@tanstack/react-router"; |
| 5 | 4 | ||
| 6 | import { APP_STAGE_LABEL } from "../../branding"; | 5 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; |
| 7 | import { cn } from "../../lib/utils"; | 6 | import { cn } from "../../lib/utils"; |
| 8 | import { primaryServerConfigAtom } from "../../state/server"; | 7 | import { |
| 9 | import { resolveSidebarStageBadgeLabel } from "../Sidebar.logic"; | 8 | resolveEnvironmentIdentificationPillLabel, |
| 10 | import { SidebarStageBackdrop, resolveSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; | 9 | resolveSidebarStageBackdropVariant, |
| 10 | SidebarStageBackdrop, | ||
| 11 | useEnvironmentStageLabel, | ||
| 12 | } from "../SidebarStageBackdrop"; | ||
| 13 | import { Badge } from "../ui/badge"; | ||
| 11 | import { | 14 | import { |
| 12 | SidebarFooter, | 15 | SidebarFooter, |
| 13 | SidebarHeader, | 16 | SidebarHeader, |
| ... | @@ -25,8 +28,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ | ... | @@ -25,8 +28,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 25 | }: { | 28 | }: { |
| 26 | isElectron: boolean; | 29 | isElectron: boolean; |
| 27 | }) { | 30 | }) { |
| 28 | const stageLabel = useSidebarStageLabel(); | 31 | const stageLabel = useEnvironmentStageLabel(); |
| 29 | const backdropVariant = resolveSidebarStageBackdropVariant(stageLabel); | 32 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); |
| 33 | const backdropVariant = resolveSidebarStageBackdropVariant( | ||
| 34 | stageLabel, | ||
| 35 | environmentIdentificationMode === "artwork", | ||
| 36 | ); | ||
| 37 | const pillLabel = | ||
| 38 | environmentIdentificationMode === "pill" | ||
| 39 | ? resolveEnvironmentIdentificationPillLabel(stageLabel) | ||
| 40 | : null; | ||
| 30 | 41 | ||
| 31 | return ( | 42 | return ( |
| 32 | <SidebarHeader | 43 | <SidebarHeader |
| ... | @@ -44,6 +55,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ | ... | @@ -44,6 +55,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 44 | )} | 55 | )} |
| 45 | /> | 56 | /> |
| 46 | <SidebarBrand onBackdrop={backdropVariant !== null} /> | 57 | <SidebarBrand onBackdrop={backdropVariant !== null} /> |
| 58 | {pillLabel ? ( | ||
| 59 | <Badge | ||
| 60 | className="relative z-10 ml-1 rounded-full px-1.5 text-muted-foreground" | ||
| 61 | data-environment-identification="pill" | ||
| 62 | size="sm" | ||
| 63 | variant="secondary" | ||
| 64 | > | ||
| 65 | {pillLabel} | ||
| 66 | </Badge> | ||
| 67 | ) : null} | ||
| 47 | </SidebarHeader> | 68 | </SidebarHeader> |
| 48 | ); | 69 | ); |
| 49 | }); | 70 | }); |
| ... | @@ -71,16 +92,6 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { | ... | @@ -71,16 +92,6 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { |
| 71 | ); | 92 | ); |
| 72 | } | 93 | } |
| 73 | 94 | ||
| 74 | function useSidebarStageLabel() { | ||
| 75 | const primaryServerVersion = | ||
| 76 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; | ||
| 77 | |||
| 78 | return resolveSidebarStageBadgeLabel({ | ||
| 79 | primaryServerVersion, | ||
| 80 | fallbackStageLabel: APP_STAGE_LABEL, | ||
| 81 | }); | ||
| 82 | } | ||
| 83 | |||
| 84 | function T3Wordmark() { | 95 | function T3Wordmark() { |
| 85 | return ( | 96 | return ( |
| 86 | <svg | 97 | <svg |
apps/web/src/hooks/useSettings.test.ts+12-1| ... | @@ -6,7 +6,18 @@ import { | ... | @@ -6,7 +6,18 @@ import { |
| 6 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; | 6 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; |
| 7 | import { describe, expect, it } from "vite-plus/test"; | 7 | import { describe, expect, it } from "vite-plus/test"; |
| 8 | 8 | ||
| 9 | import { mergeEnvironmentSettings } from "./useSettings"; | 9 | import { mergeEnvironmentSettings, resolveEnvironmentIdentificationMode } from "./useSettings"; |
| 10 | |||
| 11 | describe("resolveEnvironmentIdentificationMode", () => { | ||
| 12 | it("keeps identification hidden until client settings hydrate", () => { | ||
| 13 | expect(resolveEnvironmentIdentificationMode({ mode: "artwork", settingsHydrated: false })).toBe( | ||
| 14 | "none", | ||
| 15 | ); | ||
| 16 | expect(resolveEnvironmentIdentificationMode({ mode: "pill", settingsHydrated: true })).toBe( | ||
| 17 | "pill", | ||
| 18 | ); | ||
| 19 | }); | ||
| 20 | }); | ||
| 10 | 21 | ||
| 11 | describe("mergeEnvironmentSettings", () => { | 22 | describe("mergeEnvironmentSettings", () => { |
| 12 | it("combines the selected environment's server settings with client preferences", () => { | 23 | it("combines the selected environment's server settings with client preferences", () => { |
apps/web/src/hooks/useSettings.ts+15| ... | @@ -21,6 +21,7 @@ import { | ... | @@ -21,6 +21,7 @@ import { |
| 21 | type ClientSettingsPatch, | 21 | type ClientSettingsPatch, |
| 22 | type ClientSettings, | 22 | type ClientSettings, |
| 23 | DEFAULT_CLIENT_SETTINGS, | 23 | DEFAULT_CLIENT_SETTINGS, |
| 24 | type EnvironmentIdentificationMode, | ||
| 24 | type UnifiedSettings, | 25 | type UnifiedSettings, |
| 25 | } from "@t3tools/contracts/settings"; | 26 | } from "@t3tools/contracts/settings"; |
| 26 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | 27 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| ... | @@ -222,6 +223,20 @@ export function useClientSettings<T = ClientSettings>( | ... | @@ -222,6 +223,20 @@ export function useClientSettings<T = ClientSettings>( |
| 222 | return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); | 223 | return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); |
| 223 | } | 224 | } |
| 224 | 225 | ||
| 226 | export function resolveEnvironmentIdentificationMode(input: { | ||
| 227 | mode: EnvironmentIdentificationMode; | ||
| 228 | settingsHydrated: boolean; | ||
| 229 | }): EnvironmentIdentificationMode { | ||
| 230 | // Avoid briefly rendering the default artwork before a persisted pill/none choice loads. | ||
| 231 | return input.settingsHydrated ? input.mode : "none"; | ||
| 232 | } | ||
| 233 | |||
| 234 | export function useEnvironmentIdentificationMode(): EnvironmentIdentificationMode { | ||
| 235 | const settingsHydrated = useClientSettingsHydrated(); | ||
| 236 | const mode = useClientSettingsValue().environmentIdentificationMode; | ||
| 237 | return resolveEnvironmentIdentificationMode({ mode, settingsHydrated }); | ||
| 238 | } | ||
| 239 | |||
| 225 | /** | 240 | /** |
| 226 | * Resolved sidebar v2 state: an explicit choice in Settings → Beta if the user | 241 | * Resolved sidebar v2 state: an explicit choice in Settings → Beta if the user |
| 227 | * has made one, otherwise the default for this build stage (on for nightly and | 242 | * has made one, otherwise the default for this build stage (on for nightly and |
packages/contracts/src/settings.test.ts+18| ... | @@ -49,6 +49,24 @@ describe("ClientSettings glass opacity", () => { | ... | @@ -49,6 +49,24 @@ describe("ClientSettings glass opacity", () => { |
| 49 | }); | 49 | }); |
| 50 | }); | 50 | }); |
| 51 | 51 | ||
| 52 | describe("ClientSettings environment identification", () => { | ||
| 53 | it("defaults to artwork and accepts each presentation mode", () => { | ||
| 54 | expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); | ||
| 55 | |||
| 56 | for (const mode of ["artwork", "pill", "none"] as const) { | ||
| 57 | expect( | ||
| 58 | decodeClientSettingsPatch({ environmentIdentificationMode: mode }) | ||
| 59 | .environmentIdentificationMode, | ||
| 60 | ).toBe(mode); | ||
| 61 | } | ||
| 62 | }); | ||
| 63 | |||
| 64 | it("rejects unsupported presentation modes", () => { | ||
| 65 | expect(() => decodeClientSettings({ environmentIdentificationMode: "badge" })).toThrow(); | ||
| 66 | expect(() => decodeClientSettingsPatch({ environmentIdentificationMode: "badge" })).toThrow(); | ||
| 67 | }); | ||
| 68 | }); | ||
| 69 | |||
| 52 | describe("ClientSettings sidebar v2", () => { | 70 | describe("ClientSettings sidebar v2", () => { |
| 53 | it("defaults the beta off with a three-day auto-settle threshold", () => { | 71 | it("defaults the beta off with a three-day auto-settle threshold", () => { |
| 54 | const settings = decodeClientSettings({}); | 72 | const settings = decodeClientSettings({}); |
packages/contracts/src/settings.ts+7| ... | @@ -58,6 +58,9 @@ export const GlassOpacity = Schema.Int.check( | ... | @@ -58,6 +58,9 @@ export const GlassOpacity = Schema.Int.check( |
| 58 | ); | 58 | ); |
| 59 | export type GlassOpacity = typeof GlassOpacity.Type; | 59 | export type GlassOpacity = typeof GlassOpacity.Type; |
| 60 | export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; | 60 | export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; |
| 61 | export const EnvironmentIdentificationMode = Schema.Literals(["artwork", "pill", "none"]); | ||
| 62 | export type EnvironmentIdentificationMode = typeof EnvironmentIdentificationMode.Type; | ||
| 63 | export const DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE: EnvironmentIdentificationMode = "artwork"; | ||
| 61 | 64 | ||
| 62 | export const ClientSettingsSchema = Schema.Struct({ | 65 | export const ClientSettingsSchema = Schema.Struct({ |
| 63 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | 66 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| ... | @@ -67,6 +70,9 @@ export const ClientSettingsSchema = Schema.Struct({ | ... | @@ -67,6 +70,9 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 67 | Schema.withDecodingDefault(Effect.succeed([])), | 70 | Schema.withDecodingDefault(Effect.succeed([])), |
| 68 | ), | 71 | ), |
| 69 | diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | 72 | diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 73 | environmentIdentificationMode: EnvironmentIdentificationMode.pipe( | ||
| 74 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE)), | ||
| 75 | ), | ||
| 70 | glassOpacity: GlassOpacity.pipe( | 76 | glassOpacity: GlassOpacity.pipe( |
| 71 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), | 77 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), |
| 72 | ), | 78 | ), |
| ... | @@ -611,6 +617,7 @@ export const ClientSettingsPatch = Schema.Struct({ | ... | @@ -611,6 +617,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 611 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), | 617 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), |
| 612 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), | 618 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), |
| 613 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), | 619 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), |
| 620 | environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), | ||
| 614 | glassOpacity: Schema.optionalKey(GlassOpacity), | 621 | glassOpacity: Schema.optionalKey(GlassOpacity), |
| 615 | favorites: Schema.optionalKey( | 622 | favorites: Schema.optionalKey( |
| 616 | Schema.Array( | 623 | Schema.Array( |