diff --git a/DESIGN.md b/DESIGN.md index 5649209..188d7f0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -67,6 +67,22 @@ Spacing follows Tailwind's 4px scale. Existing frontend stacks use `gap-1`, `gap - **Motion**: no section-level motion. - **Layout**: vertical stack. +### Locker inventory surface + +- **Purpose**: The authenticated locker is a Polaris operations surface: a dense, readable inventory rather than a literal game replica. The grid is the focal region and selection context is kept adjacent to it. +- **Palette**: Use the existing semantic neutral tokens: `bg-background` for the page, `bg-card`/`bg-muted` for framed regions, `border-border` for structure, `text-foreground` and `text-muted-foreground` for hierarchy, and `bg-primary`/`ring-ring` for active selection and primary actions. Amber utility accents are reserved for existing equipment/skin meaning; they do not become a second surface palette. +- **Typography**: Keep Geist for interface copy and Geist Mono with `tabular-nums` for coordinates, counts, dimensions, and other inventory metadata. Compact labels use the existing `text-xs` level, never ad-hoc display sizes. +- **Primitives**: + - **`locker-shell`**: one responsive page region with a compact control band and a single-column fallback below `xl`. + - **`inventory-frame`**: a strong rectangular border around the existing two-dimensional grid. The grid may scroll only inside this bounded region when its footprint exceeds the available inline size; the page itself must not gain a horizontal scrollbar. + - **`selection-rail`**: selected-item context and actions sit beside the inventory at `xl` and later, then become an inline section below the grid at narrower widths. It remains readable when no item is selected. + - **`metadata-strip`**: a compact cluster for occupancy, dimensions, and drag/drop guidance, using 4px spacing and semantic muted text. + - **`wardrobe-slot-board`**: a body-ordered rectangular board: head/face spans the top, torso and pack occupy the middle, and weapons/attachments plus sidearm finish the lower row. The primary weapon owns an indented attachment subgroup for optic, muzzle, and bipod slots. Each slot is a labeled semantic button with distinct filled and empty states; the board is driven by `WARDROBE_SLOT_LAYOUT` and never relies on a character image as its control surface. +- **States**: default, hover, focus-visible, selected, search-highlight, valid drop, invalid drop, attachment drop, empty, error, disabled, and reduced-motion. Selected items use a visible semantic ring; placement feedback stays inside the grid cells; empty and error states retain the existing user/manager-specific copy. +- **Accessibility**: Tabs remain native shadcn tab semantics. Every icon-only button has an accessible label, every meaningful thumbnail keeps descriptive alt text, draggable items retain dnd-kit keyboard attributes, and all actions keep visible `focus-visible` rings. The responsive rail must not reorder focus away from the selected item context. +- **Motion**: Preserve the existing dnd-kit activation threshold and absolute-position/ResizeObserver mechanics. Interaction polish is limited to existing shadcn color transitions, transform/opacity drag feedback, and the existing search highlight; `prefers-reduced-motion` disables highlight animation and nonessential transitions. No decorative looping motion is introduced. + - **Responsive behavior**: At 375px the shell becomes one readable column with no viewport-level horizontal overflow. Only the `inventory-frame` may expose bounded two-dimensional scrolling when required by the configured grid; the selection rail follows the grid inline, while the tab/search control cluster wraps before it can overflow. The inventory and selection rail use the side-by-side layout only at `xl` and above so the grid remains dominant and rail labels remain readable. The wardrobe slot board keeps its body order at `lg` and collapses regions to one readable column below that threshold. + ### Mission card - **Structure**: linked `Item` with `ItemHeader`, `ItemMedia`, `ItemTitle`, `ItemContent`, and `ItemDescription` primitives. diff --git a/package.json b/package.json index 0dca57a..5f0daff 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", "generate:version-info": "bun run src/scripts/generateVersionInfo.ts", + "import:arma": "bun run src/tools/import/importArmaLibrary.ts", "version:bump": "bun run src/scripts/bumpVersion.ts", "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", diff --git a/src/app/(frontend)/account/actions.ts b/src/app/(frontend)/account/actions.ts index 5f9d519..4817b65 100644 --- a/src/app/(frontend)/account/actions.ts +++ b/src/app/(frontend)/account/actions.ts @@ -205,6 +205,7 @@ export async function updatePreferences(input: { mutedTypes: string[]; showCallsign: boolean; showVersionOverlay: boolean; + showBackgroundTexture: boolean; leadOrActual: "lead" | "actual"; discordEnabled: boolean; discordMutedTypes: string[]; @@ -221,7 +222,7 @@ export async function updatePreferences(input: { (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), ); - await payload.update({ + await payload.update({ collection: "users", id: userId, data: { @@ -230,6 +231,7 @@ export async function updatePreferences(input: { display: { showCallsign: input.showCallsign, showVersionOverlay: input.showVersionOverlay, + showBackgroundTexture: input.showBackgroundTexture, leadOrActual: input.leadOrActual, xpDisplayMini: input.xpDisplayMini, }, diff --git a/src/app/(frontend)/account/page.tsx b/src/app/(frontend)/account/page.tsx index 064e0db..75ebb67 100644 --- a/src/app/(frontend)/account/page.tsx +++ b/src/app/(frontend)/account/page.tsx @@ -81,24 +81,25 @@ export default async function AccountPage() { - - - Preferences - Control in-app notifications and display options. - - - - - + + + Preferences + Control in-app notifications and display options. + + + + + ); diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 8b9bf24..11f907c 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -139,7 +139,13 @@ export default async function RootLayout(props: { children: React.ReactNode }) { preferences={user.preferences} className="border-r" /> - + {impersonating && } {children} diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css index b451078..a41ebf8 100644 --- a/src/app/(frontend)/styles.css +++ b/src/app/(frontend)/styles.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap"); @import "tailwindcss"; @import "tw-animate-css"; @@ -13,80 +13,79 @@ */ @theme inline { + --radius-sm: calc(var(--radius) - 4px); - --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); - --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); - --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); - --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); - --color-background: var(--background); + --color-foreground: var(--foreground); - --color-foreground: var(--foreground); + --color-card: var(--card); - --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); - --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); - --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); - --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); - --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); - --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); - --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); - --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); - --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); - --color-destructive: var(--destructive); + --color-border: var(--border); - --color-border: var(--border); + --color-input: var(--input); - --color-input: var(--input); + --color-ring: var(--ring); - --color-ring: var(--ring); + --color-chart-1: var(--chart-1); - --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); - --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); - --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); - --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); - --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); - --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); - --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-ring: var(--sidebar-ring); + --font-sans: var(--font-sans); - --font-sans: var(--font-sans); - - --font-mono: var(--font-mono); + --font-mono: var(--font-mono); } /* @@ -94,74 +93,73 @@ */ :root { + --radius: 0.625rem; - --radius: 0.625rem; + --background: oklch(1 0 0); - --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); - --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); - --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); - --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); - --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); - --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); - --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); - --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); - --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); - --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); - --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); - --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); - --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); - --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); - --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); - --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); - --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); - --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); - --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); - --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); - --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); - --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); - --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); - --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); - --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); - --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); - --sidebar-ring: oklch(0.708 0 0); + --font-sans: "Geist", sans-serif; - --font-sans: 'Geist', sans-serif; - - --font-mono: 'Geist Mono', monospace; + --font-mono: "Geist Mono", monospace; } /* @@ -169,68 +167,67 @@ */ .dark { + --background: oklch(0.145 0 0); - --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); - --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); - --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); - --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); - --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); - --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); - --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); - --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); - --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); - --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); - --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); - --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); - --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); - --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); - --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); - --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); - --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); - --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); - --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); - --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); - --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); - --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); - --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); - --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-border: oklch(1 0 0 / 10%); - - --sidebar-ring: oklch(0.556 0 0); + --sidebar-ring: oklch(0.556 0 0); } /* @@ -238,13 +235,13 @@ */ @layer base { - * { - @apply border-border outline-ring/50; - } + * { + @apply border-border outline-ring/50; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } } /* @@ -253,127 +250,166 @@ /* Top-down ORBAT-style tree connectors */ .org-tree-child { - position: relative; - padding-top: 1rem; + position: relative; + padding-top: 1rem; } .org-tree-child::before { - content: ""; - position: absolute; - top: 0; - left: 50%; - height: 1rem; - border-left: 2px solid var(--color-border); + content: ""; + position: absolute; + top: 0; + left: 50%; + height: 1rem; + border-left: 2px solid var(--color-border); } .org-tree-child::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - border-top: 2px solid var(--color-border); + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + border-top: 2px solid var(--color-border); } .org-tree-child:first-child::after { - left: 50%; + left: 50%; } .org-tree-child:last-child::after { - right: 50%; + right: 50%; } .org-tree-child:only-child::after { - display: none; + display: none; } /* Locker search-result highlight */ @keyframes locker-search-pulse { - 0%, - 100% { - box-shadow: 0 0 0 0 rgb(251 191 36 / 0.7); - } - 50% { - box-shadow: 0 0 0 6px rgb(251 191 36 / 0); - } + 0%, + 100% { + box-shadow: 0 0 0 0 rgb(251 191 36 / 0.7); + } + 50% { + box-shadow: 0 0 0 6px rgb(251 191 36 / 0); + } } @keyframes xp-gain { - 0% { - opacity: 0; - transform: translateX(0.35rem); - } - 15% { - opacity: 1; - } - 75% { - opacity: 1; - } - 100% { - opacity: 0; - transform: translateX(-1.1rem); - } + 0% { + opacity: 0; + transform: translateX(0.35rem); + } + 15% { + opacity: 1; + } + 75% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateX(-1.1rem); + } } .locker-search-highlight { - animation: locker-search-pulse 1.1s ease-in-out infinite; - box-shadow: 0 0 0 3px rgb(251 191 36 / 0.9); + animation: locker-search-pulse 1.1s ease-in-out infinite; + box-shadow: 0 0 0 3px rgb(251 191 36 / 0.9); +} + +.app-background-texture { + background-image: repeating-linear-gradient( + 135deg, + rgb(255 255 255 / 0.018) 0 1px, + transparent 1px 7px + ); +} + +.inventory-frame { + background-color: rgb(7 10 12 / 0.78); + background-image: + linear-gradient(rgb(255 255 255 / 0.018) 1px, transparent 1px), + linear-gradient(90deg, rgb(255 255 255 / 0.018) 1px, transparent 1px), + radial-gradient(circle at 50% 0%, rgb(120 150 145 / 0.09), transparent 55%); + /*background-size: + 48px 48px, + 48px 48px, + 100% 100%;*/ + box-shadow: + inset 0 1px rgb(255 255 255 / 0.04), + 0 12px 35px rgb(0 0 0 / 0.18); +} + +@media (prefers-reduced-motion: reduce) { + .locker-search-highlight { + animation: none; + } } @property --medal-border-angle { - syntax: ""; - inherits: false; - initial-value: 0deg; + syntax: ""; + inherits: false; + initial-value: 0deg; } @keyframes border-spin-animate { - from { - --medal-border-angle: 0deg; - } - to { - --medal-border-angle: 360deg; - } + from { + --medal-border-angle: 0deg; + } + to { + --medal-border-angle: 360deg; + } } .medal-card { - --medal-border-angle: 0deg; - background: rgb(193 161 79 / 0.28); - position: relative; - isolation: isolate; + --medal-border-angle: 0deg; + background: rgb(193 161 79 / 0.28); + position: relative; + isolation: isolate; } .medal-card::before { - content: ''; - position: absolute; - inset: -0.125rem; - z-index: 0; - border-radius: inherit; - border: 2px solid transparent; - background: linear-gradient(var(--background), var(--background)) padding-box, repeating-conic-gradient(from var(--medal-border-angle), transparent 0deg 150deg, peru 159deg, rgb(253 224 71) 168deg, white 178deg, transparent 178deg) border-box; - animation: border-spin-animate 10s linear infinite; - pointer-events: none; + content: ""; + position: absolute; + inset: -0.125rem; + z-index: 0; + border-radius: inherit; + border: 2px solid transparent; + background: + linear-gradient(var(--background), var(--background)) padding-box, + repeating-conic-gradient( + from var(--medal-border-angle), + transparent 0deg 150deg, + peru 159deg, + rgb(253 224 71) 168deg, + white 178deg, + transparent 178deg + ) + border-box; + animation: border-spin-animate 10s linear infinite; + pointer-events: none; } @keyframes border-spin-animate { - from { - --medal-border-angle: 0deg; - } - to { - --medal-border-angle: 360deg; - } + from { + --medal-border-angle: 0deg; + } + to { + --medal-border-angle: 360deg; + } } .medal-card * { - position: relative; - z-index: 1; + position: relative; + z-index: 1; } .medal-card::after { - content: ''; - background: radial-gradient(circle, transparent, rgb(253 224 71 / 0.1)); - position: absolute; - inset: 0; - animation: pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite; + content: ""; + background: radial-gradient(circle, transparent, rgb(253 224 71 / 0.1)); + position: absolute; + inset: 0; + animation: pulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .award-xp-high { @@ -400,30 +436,30 @@ /* Scrollbars — styled to match the dark frontend theme */ * { - scrollbar-width: thin; - scrollbar-color: color-mix(in oklch, var(--foreground) 20%, transparent) transparent; + scrollbar-width: thin; + scrollbar-color: color-mix(in oklch, var(--foreground) 20%, transparent) transparent; } *::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 10px; + height: 10px; } *::-webkit-scrollbar-track { - background: transparent; + background: transparent; } *::-webkit-scrollbar-thumb { - background-color: color-mix(in oklch, var(--foreground) 20%, transparent); - border-radius: 9999px; - border: 2px solid transparent; - background-clip: padding-box; + background-color: color-mix(in oklch, var(--foreground) 20%, transparent); + border-radius: 9999px; + border: 2px solid transparent; + background-clip: padding-box; } *::-webkit-scrollbar-thumb:hover { - background-color: color-mix(in oklch, var(--foreground) 32%, transparent); + background-color: color-mix(in oklch, var(--foreground) 32%, transparent); } *::-webkit-scrollbar-corner { - background: transparent; + background: transparent; } diff --git a/src/collections/users/Users.ts b/src/collections/users/Users.ts index 8a8f9ba..53db483 100644 --- a/src/collections/users/Users.ts +++ b/src/collections/users/Users.ts @@ -269,41 +269,47 @@ export const Users: CollectionConfig = { }, ], }, - { - name: "display", - label: "Display", - type: "group", - fields: [ - { - name: "showCallsign", - label: "Show callsign (username) instead of display name in the sidebar", - type: "checkbox", - defaultValue: false, - }, - { - name: "leadOrActual", - label: 'Use "Lead" or "Actual" when reviewing the unit roster', - type: "select", - options: [ - { label: "Lead", value: "lead" }, - { label: "Actual", value: "actual" }, - ], - defaultValue: "lead", - }, - { - name: "showVersionOverlay", - label: "Show version overlay at top of page", - type: "checkbox", - defaultValue: true, - }, - { - name: "xpDisplayMini", - label: "Use mini mode for XP display", - type: "checkbox", - defaultValue: false, - }, - ], - }, + { + name: "display", + label: "Display", + type: "group", + fields: [ + { + name: "showCallsign", + label: "Show callsign (username) instead of display name in the sidebar", + type: "checkbox", + defaultValue: false, + }, + { + name: "leadOrActual", + label: 'Use "Lead" or "Actual" when reviewing the unit roster', + type: "select", + options: [ + { label: "Lead", value: "lead" }, + { label: "Actual", value: "actual" }, + ], + defaultValue: "lead", + }, + { + name: "showVersionOverlay", + label: "Show version overlay at top of page", + type: "checkbox", + defaultValue: true, + }, + { + name: "xpDisplayMini", + label: "Use mini mode for XP display", + type: "checkbox", + defaultValue: false, + }, + { + name: "showBackgroundTexture", + label: "Show diagonal background texture", + type: "checkbox", + defaultValue: true, + }, + ], + }, { name: "discord", label: "Discord", diff --git a/src/components/frontend/account/PreferencesForm.tsx b/src/components/frontend/account/PreferencesForm.tsx index 505512e..e5233aa 100644 --- a/src/components/frontend/account/PreferencesForm.tsx +++ b/src/components/frontend/account/PreferencesForm.tsx @@ -17,6 +17,7 @@ interface PreferencesFormProps { readonly mutedTypes: readonly string[]; readonly showCallsign: boolean; readonly showVersionOverlay: boolean; + readonly showBackgroundTexture: boolean; readonly leadOrActual: "lead" | "actual"; readonly discordEnabled: boolean; readonly discordMutedTypes: readonly string[]; @@ -28,6 +29,7 @@ export function PreferencesForm({ mutedTypes: initialMutedTypes, showCallsign: initialShowCallsign, showVersionOverlay: initialShowVersionOverlay, + showBackgroundTexture: initialShowBackgroundTexture, leadOrActual: initialLeadOrActual, discordEnabled: initialDiscordEnabled, discordMutedTypes: initialDiscordMutedTypes, @@ -38,6 +40,7 @@ export function PreferencesForm({ const [mutedTypes, setMutedTypes] = useState(() => [...initialMutedTypes]); const [showCallsign, setShowCallsign] = useState(initialShowCallsign); const [showVersionOverlay, setShowVersionOverlay] = useState(initialShowVersionOverlay); + const [showBackgroundTexture, setShowBackgroundTexture] = useState(initialShowBackgroundTexture); const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual); const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled); const [discordMutedTypes, setDiscordMutedTypes] = useState(() => [ @@ -72,6 +75,7 @@ export function PreferencesForm({ mutedTypes, showCallsign, showVersionOverlay, + showBackgroundTexture, leadOrActual, discordEnabled, discordMutedTypes, @@ -104,10 +108,7 @@ export function PreferencesForm({ Choose what appears in the app shell and roster views. - -