Frontend: Refactor lightbox theme colors and chip-selector tokens #4966

- Centralize sidebar/PhotoSwipe caption link styling via new `.text-html`
  class in `text.css`; drops the previous `info`-token overload.
- Swap lightbox `surface-variant` ↔ `on-surface-variant` so Vuetify
  tooltips render dark natively (remove `.v-tooltip` override). Lift
  `on-surface` to `#f9fafb`, `primary` to `#F2F3F3`, `highlight` to
  `#424041`.
- Add cross-theme `add` color token to `ui.js` (dark `#3a6e73`, light
  `#cce3de`); register `add` and `remove` in Vuetify color variations.
- Rename chip-selector classes from color-named to action-named
  (`chip--add` / `chip--remove` / `chip--default` plus `--mixed` partial
  variants); double selectors (`.chip.chip--*`) to win over the UA
  `[role="button"] { color: inherit }` reset.
- Tune `remove` saturation in `ui.js`, `default`, and `lavender` themes.
- Bind face-marker chrome in `meta.css` to theme tokens: default rect +
  removing rect → `on-surface` / `remove`; Cancel pill → `button`;
  Remove pill → `remove`; pill text → `on-surface`.
- Remove the hardcoded `menuBgColor` literal in `lightbox.vue` and the
  redundant `.meta-caption a:hover` rule in `lightbox.css`.
- Update chip-selector vitest for renamed classes; Prettier auto-format
  in `views.css` and `vuetify.css`.
This commit is contained in:
Michael Mayer 2026-05-19 06:41:36 +00:00
parent 7ed0fc44d3
commit f928844a40
12 changed files with 143 additions and 135 deletions

View file

@ -156,13 +156,13 @@ export default {
}
if (item.action === "add") {
classes.push(item.mixed ? `${baseClass}--green-light` : `${baseClass}--green`);
classes.push(item.mixed ? `${baseClass}--add-mixed` : `${baseClass}--add`);
} else if (item.action === "remove") {
classes.push(item.mixed ? `${baseClass}--red-light` : `${baseClass}--red`);
classes.push(item.mixed ? `${baseClass}--remove-mixed` : `${baseClass}--remove`);
} else if (item.mixed) {
classes.push(`${baseClass}--gray-light`);
classes.push(`${baseClass}--default-mixed`);
} else {
classes.push(`${baseClass}--gray`);
classes.push(`${baseClass}--default`);
}
return classes;

View file

@ -188,7 +188,6 @@ export default {
sidebarVisible: shouldShowSidebar(),
hideCaption: shouldHideCaption() || shouldShowSidebar(),
menuElement: null,
menuBgColor: "#252525",
menuVisible: false,
lightbox: null, // Current PhotoSwipe lightbox instance.
captionPlugin: null, // Current PhotoSwipe caption plugin instance.

View file

@ -63,7 +63,7 @@
@blur="onInlineFieldBlur"
></v-textarea>
<!-- eslint-disable-next-line vue/no-v-html -- captionHtml is encode-then-sanitized via $util.sanitizeHtml($util.encodeHTML(raw)); see captionHtml() computed -->
<div v-else-if="model.Caption" class="text-body-2 meta-caption meta-scrollable" v-html="captionHtml"></div>
<div v-else-if="model.Caption" class="text-body-2 meta-caption meta-scrollable text-html" v-html="captionHtml"></div>
<div v-else class="meta-add-prompt" @click.stop="startEditing('caption')">{{ $gettext("Add a Caption") }}</div>
<template v-if="isEditable" #append>
<p-lightbox-sidebar-toolbar
@ -429,7 +429,7 @@
@blur="onInlineFieldBlur"
></v-textarea>
<!-- eslint-disable-next-line vue/no-v-html -- f.htmlValue points at a sanitized computed (e.g. notesHtml) which runs $util.sanitizeHtml($util.encodeHTML(raw)). -->
<div v-else-if="f.display === 'html' && fieldHtml(f)" class="text-body-2 meta-scrollable" :class="`meta-${f.key}`" v-html="fieldHtml(f)"></div>
<div v-else-if="f.display === 'html' && fieldHtml(f)" class="text-body-2 meta-scrollable text-html" :class="`meta-${f.key}`" v-html="fieldHtml(f)"></div>
<div v-else-if="f.display !== 'html' && f.read(photo)" class="text-body-2 meta-scrollable" :class="`meta-${f.key}`">{{ f.read(photo) }}</div>
<div v-else class="meta-add-prompt" @click.stop="startEditing(f.key)">{{ f.placeholder ? f.placeholder : f.label }}</div>
<template v-if="isEditable" #append>

View file

@ -1,112 +1,117 @@
/* Chip Selector Styles */
.chip-selector {
display: flex;
flex-direction: column;
gap: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.chip-selector__title {
font-weight: 500;
font-size: 14px;
color: rgba(var(--v-theme-on-surface), 0.87);
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
color: rgba(var(--v-theme-on-surface), 0.87);
margin-bottom: 8px;
}
.chip-selector__chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 40px;
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 40px;
align-items: flex-start;
}
.chip-selector__input-container {
margin-top: 8px;
margin-top: 8px;
}
.chip-selector__input {
width: 100%;
width: 100%;
}
/* Chip Styles */
.chip {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 16px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
user-select: none;
min-height: 32px;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 16px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
user-select: none;
min-height: 32px;
transition: all 0.2s ease;
}
.chip__content {
display: flex;
align-items: center;
gap: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.chip__icon {
font-size: 16px;
opacity: 0.9;
font-size: 16px;
opacity: 0.9;
}
/* Chip States */
.chip--gray {
background-color: rgb(var(--v-theme-secondary-light, var(--v-theme-surface-bright, var(--v-theme-surface))));
color: rgb(var(--v-theme-on-surface));
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.12);
/* Chip States class names mirror the `item.action` value the chip-selector
emits (`add` / `remove` / default), and `--mixed` is the partial-selection
variant (action applies to only some of the selected items). Selectors are
doubled (`.chip.chip--*`) so the `color` rules out-rank the UA-level
`[role="button"] { color: inherit }` reset, which would otherwise strip the
on-token foreground from a solid chip rendered as a button. */
.chip.chip--default {
background-color: rgb(var(--v-theme-secondary-light, var(--v-theme-surface-bright, var(--v-theme-surface))));
color: rgb(var(--v-theme-on-surface));
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.12);
}
.chip--gray-light {
background-color: rgba(var(--v-theme-secondary-light, var(--v-theme-surface-bright, var(--v-theme-surface))), 0.55);
color: rgb(var(--v-theme-on-surface));
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.12);
.chip.chip--default-mixed {
background-color: rgba(var(--v-theme-secondary-light, var(--v-theme-surface-bright, var(--v-theme-surface))), 0.55);
color: rgb(var(--v-theme-on-surface));
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.12);
}
.chip--green {
background-color: rgb(var(--v-theme-download));
color: rgb(var(--v-theme-on-download));
.chip.chip--add {
background-color: rgba(var(--v-theme-add), 0.8);
color: rgb(var(--v-theme-on-add));
}
.chip--green-light {
background-color: rgba(var(--v-theme-download), 0.3);
color: rgb(var(--v-theme-download));
.chip.chip--add-mixed {
background-color: rgba(var(--v-theme-add-darken-1), 0.45);
color: rgb(var(--v-theme-on-add));
}
.chip--red {
background-color: rgb(var(--v-theme-error));
color: rgb(var(--v-theme-on-error));
.chip.chip--remove {
background-color: rgba(var(--v-theme-remove), 0.8);
color: rgb(var(--v-theme-on-remove));
}
.chip--red-light {
background-color: rgba(var(--v-theme-error), 0.3);
color: rgb(var(--v-theme-error));
.chip.chip--remove-mixed {
background-color: rgba(var(--v-theme-remove-darken-1), 0.45);
color: rgb(var(--v-theme-on-remove));
}
/* Empty state */
.chip-selector__empty {
padding: 16px;
text-align: center;
color: rgba(var(--v-theme-on-surface), 0.6);
font-style: italic;
padding: 16px;
text-align: center;
color: rgba(var(--v-theme-on-surface), 0.6);
font-style: italic;
}
/* Mobile optimizations */
@media (max-width: 600px) {
.chip-selector__chips {
gap: 6px;
}
.chip {
padding: 4px 8px;
font-size: 12px;
min-height: 28px;
}
.chip__icon {
font-size: 14px;
}
.chip-selector__chips {
gap: 6px;
}
.chip {
padding: 4px 8px;
font-size: 12px;
min-height: 28px;
}
.chip__icon {
font-size: 14px;
}
}

View file

@ -177,17 +177,6 @@
text-align: start;
}
.p-lightbox__container > .p-lightbox__sidebar .meta-caption a,
.p-lightbox__container > .p-lightbox__sidebar .meta-notes a {
text-decoration: underline;
word-break: break-all;
}
.p-lightbox__container > .p-lightbox__sidebar .meta-caption a:hover,
.p-lightbox__container > .p-lightbox__sidebar .meta-notes a:hover {
text-decoration: none;
}
.p-lightbox__container > .p-lightbox__sidebar .meta-chip {
display: inline-flex;
align-items: center;
@ -398,14 +387,6 @@
line-height: 1.125rem;
}
/* Flip surface/on-surface-variant for lightbox tooltips so they read as a
dark popover instead of a near-white block over the dark sidebar.
Tooltips are body-teleported, so the theme class lands on the popper itself. */
.v-tooltip.v-theme--lightbox > .v-overlay__content {
background: rgb(var(--v-theme-on-surface-variant));
color: rgb(var(--v-theme-surface-variant));
}
/* Media Content */
.pswp__content {
@ -670,20 +651,6 @@
touch-action: none;
}
.pswp__dynamic-caption a {
color: inherit;
text-decoration: underline;
white-space: normal;
overflow-wrap: normal;
word-wrap: normal;
word-break: break-word;
text-align: start;
text-overflow: ellipsis;
hyphens: auto;
pointer-events: auto;
touch-action: auto;
}
.pswp__dynamic-caption h4 {
font-weight: 600;
word-wrap: normal;

View file

@ -39,8 +39,8 @@
}
.p-meta-face-markers__rect {
fill: rgba(255, 255, 255, 0.06);
stroke: rgba(255, 255, 255, 0.9);
fill: rgba(var(--v-theme-on-surface), 0.06);
stroke: rgba(var(--v-theme-on-surface), 0.9);
stroke-width: 2px;
vector-effect: non-scaling-stroke;
pointer-events: none;
@ -62,8 +62,8 @@
the inline remove-confirm pill is anchored below it. Red stroke
plus a faint red fill makes the destructive intent obvious. */
.p-meta-face-markers__rect--removing {
stroke: rgba(244, 67, 54, 0.95);
fill: rgba(244, 67, 54, 0.16);
stroke: rgba(var(--v-theme-remove), 0.95);
fill: rgba(var(--v-theme-remove), 0.16);
stroke-width: 2.5px;
}
@ -114,7 +114,7 @@
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
color: #fff;
color: rgb(var(--v-theme-on-surface));
transition: transform 0.08s ease-out;
}
@ -127,11 +127,11 @@
}
.p-meta-face-markers__btn--cancel {
background: rgba(90, 90, 90, 0.95);
background: rgba(var(--v-theme-button), 0.95);
}
.p-meta-face-markers__btn--remove {
background: rgba(211, 47, 47, 0.95);
background: rgba(var(--v-theme-remove), 0.95);
}
/* Back button anchored top-left in LTR / top-right in RTL with its own

View file

@ -153,13 +153,38 @@ code,
.text-link,
p a.text-link {
color: rgb(var(--v-theme-primary, #ffffff));
text-decoration: none;
}
.text-html a,
.pswp__dynamic-caption a,
.p-page a,
.p-page footer .text-link,
.p-page footer .body-link {
color: rgb(var(--v-theme-on-surface, #ffffff));
}
.text-html a,
.pswp__dynamic-caption a {
text-decoration-thickness: 1px;
text-decoration: underline;
word-break: break-all;
white-space: normal;
overflow-wrap: normal;
word-wrap: normal;
text-align: start;
text-overflow: ellipsis;
hyphens: auto;
pointer-events: auto;
touch-action: auto;
}
.text-html a:hover,
.pswp__dynamic-caption a:hover,
.text-link,
p a.text-link,
.p-page a,
.p-page footer .text-link,
.p-page footer .body-link {
text-decoration: none;
}

View file

@ -297,8 +297,8 @@
}
.search-results.list-view .v-table > .v-table__wrapper > table > tbody > tr > td.result .preview,
.search-results.list-view .v-table>.v-table__wrapper>table>tbody>tr>td.result .preview,
.edit-batch.list-view .v-table>table>tbody>tr>td.result .preview {
.search-results.list-view .v-table > .v-table__wrapper > table > tbody > tr > td.result .preview,
.edit-batch.list-view .v-table > table > tbody > tr > td.result .preview {
width: 50px;
height: 50px;
}

View file

@ -132,7 +132,7 @@ body > .v-overlay-container .v-overlay__scrim {
overflow-y: auto;
}
.v-file-upload input[type=file] {
.v-file-upload input[type="file"] {
max-width: 100%;
}
@ -180,7 +180,7 @@ body > .v-overlay-container .v-overlay__scrim {
margin-bottom: 6px;
}
.v-dialog--upload .v-file-upload .v-file-upload-icon .v-icon--size-default {
.v-dialog--upload .v-file-upload .v-file-upload-icon .v-icon--size-default {
font-size: 1em;
}
@ -510,7 +510,7 @@ div.v-dialog.v-dialog--fullscreen > div.v-card {
font-size: 0.75rem;
}
body .v-table>.v-table__wrapper>table {
body .v-table > .v-table__wrapper > table {
border-spacing: 0;
}

View file

@ -4,7 +4,7 @@ import { style, colors, variables } from "ui";
/* Theme Color Variations */
export const variations = {
colors: ["primary", "highlight", "secondary", "surface", "surface-variant", "table", "navigation"],
colors: ["primary", "highlight", "secondary", "surface", "surface-variant", "table", "navigation", "add", "remove"],
lighten: 2,
darken: 1,
};
@ -41,7 +41,8 @@ let themes = {
"success": "#4db6ac", // Successful-outcome notification color (saved toast, completed-job indicator).
"warning": "#bc9714", // Caution / recoverable-concern notification color (warning banners, paused indicators).
"favorite": "#FFD600", // Favorite-star color.
"remove": "#da4e4c", // Destructive "remove from collection" action color (distinct from `error`, which is a fault state).
"add": "#00897B",
"remove": "#aa504f", // Destructive "remove from collection" action color (distinct from `error`, which is a fault state).
"restore": "#00d48a", // Restore-from-trash / undo-remove action color.
"album": "#ed9e00", // Album identity color (album icons, chips, thumbnail accents).
"on-album": "#ffffff", // Text/icon color on album-tinted backgrounds.
@ -71,18 +72,19 @@ let themes = {
name: "lightbox",
colors: {
"background": "#0c0d0d", // Page canvas; near-black so the photo dominates. Also painted onto `navigation` rows.
"on-background": "#ffffff", // Text and icon color readable against `background`.
"surface": "#151515", // Default container surface (sidebar, dropdown menus, dialog v-card); one step above `background`.
"on-surface": "#ffffff", // Body text and icon color on `surface`, `surface-bright`, `surface-light`, and `card`.
"on-surface": "#f9fafb", // Body text and icon color on `surface`, `surface-bright`, `surface-light`, and `card`.
"surface-bright": "#171717", // Lifted variant of `surface`; raised tiles, hover backgrounds (matches `card` currently).
"surface-variant": "#bdbdbd", // Light-gray tone used by Vuetify defaults for active dropdown rows, focus rings, etc.
"on-surface-variant": "#1c1c1c", // Inverse pair of `surface-variant`; also painted as the tooltip background.
"surface-variant": "#1c1c1c", // Inverse pair of `surface-variant`; also painted as the tooltip background.
"on-surface-variant": "#bdbdbd", // Light-gray tone used by Vuetify defaults for active dropdown rows, focus rings, etc.
"card": "#171717", // Dedicated card-container background; raised dialog v-cards inherit this via the VCard default.
"selected": "#3d3f40", // Active list-item background; one step above `highlight` for a distinct, neutral gray.
"table": "#242424", // VDataTable row and header background.
"button": "#232425", // Secondary button color (e.g. dialog "Cancel"); the neutral companion to `highlight`.
"highlight": "#383838", // Primary button color (Save / Confirm); softened from #3d3f40 on May 13 to keep the action calm.
"highlight": "#424041", // Primary button color (Save / Confirm); softened from #3d3f40 on May 13 to keep the action calm.
"switch": "#101112", // VSwitch track background when off.
"primary": "#ebebeb", // Brand/identity accent; desaturated near-white to keep the lightbox grayscale (focus rings).
"primary": "#F2F3F3", // Brand/identity accent; desaturated near-white to keep the lightbox grayscale (focus rings).
"secondary": "#191919", // Background for secondary panels (tab strips, expansion-panel headers, nav drawer sections).
"secondary-light": "#1e1e1e", // Lifted variant of `secondary`; backs `.meta-chip` and the chip-selector chips.
"accent": "#2D2E2E", // Small-decoration tint for hover/focus subtleties; not a primary action color.
@ -115,6 +117,8 @@ let themes = {
"theme-overlay-multiplier": 0.16, // Vuetify elevation-overlay multiplier; kept low to keep raised surfaces near-black.
"high-emphasis-opacity": 0.96, // Body-text alpha on `surface`; under 1.0 so pure-white doesn't read as harsh.
"medium-emphasis-opacity": 0.88, // Secondary-text alpha (captions, helpers); above the 0.7 dark default for legibility.
"label-opacity": 0.67, // Floating-label and helper-text alpha on inputs.
"disabled-opacity": 0.75, // Alpha applied to disabled controls.
},
},
@ -418,7 +422,9 @@ let themes = {
"info": "#7887df",
"success": "#26A69A",
"warning": "#bfa965",
"remove": "#e57373",
"add": "#94d5c4",
"remove": "#e45d6a",
"on-remove": "#F3F3F5",
"restore": "#64b5f6",
"album": "#ffab00",
"download": "#00bfa5",
@ -590,12 +596,15 @@ let themes = {
"danger": "#9f2727",
"info": "#4aa2bc",
"on-info": "#323742",
"success": "#1ac5c1",
"success": "#89d1cf",
"on-success": "#323742",
"warning": "#d88a0b",
"on-warning": "#b87d16",
"favorite": "#EBCB8B",
"remove": "#BF616A",
"add": "#b2ddd2",
"on-add": "#323742",
"remove": "#e49ca4",
"on-remove": "#323742",
"restore": "#81A1C1",
"album": "#EBCB8B",
"download": "#8FBCBB",

View file

@ -25,7 +25,8 @@ export const colorsDark = {
"success": "#4db6ac", // Successful-outcome notification color (saved toast, completed-job indicator).
"warning": "#bc9714", // Caution / recoverable-concern notification color (warning banners, paused indicators).
"favorite": "#FFD600", // Favorite-star color.
"remove": "#da4e4c", // Destructive "remove from collection" action color (distinct from `error`, which is a fault state).
"add": "#50a9b1", // User-initiated "add to collection" action color (paired with `remove`; distinct from `download` and `success`).
"remove": "#aa504f", // Destructive "remove from collection" action color (distinct from `error`, which is a fault state).
"restore": "#00d48a", // Restore-from-trash / undo-remove action color.
"album": "#ed9e00", // Album identity color (album icons, chips, thumbnail accents).
"on-album": "#ffffff", // Text/icon color on album-tinted backgrounds.
@ -91,7 +92,9 @@ export const colorsLight = {
"success": "#8FBCBB",
"warning": "#f0d8a8",
"favorite": "#EBCB8B",
"remove": "#BF616A",
"add": "#a7cac1",
"remove": "#cfa0a4",
"on-remove": "#1e1e1f",
"restore": "#81A1C1",
"album": "#EBCB8B",
"download": "#8FBCBB",

View file

@ -69,9 +69,9 @@ describe("component/file/chip-selector", () => {
describe("Chip Icons", () => {
it.each([
{ idx: 0, expectedClass: "chip--gray", expectedIcon: null },
{ idx: 1, expectedClass: "chip--green-light", expectedIcon: "mdi-plus" },
{ idx: 2, expectedClass: "chip--red", expectedIcon: "mdi-minus" },
{ idx: 0, expectedClass: "chip--default", expectedIcon: null },
{ idx: 1, expectedClass: "chip--add-mixed", expectedIcon: "mdi-plus" },
{ idx: 2, expectedClass: "chip--remove", expectedIcon: "mdi-minus" },
])("should render expected style/icon for chip at index $idx", ({ idx, expectedClass, expectedIcon }) => {
const chips = wrapper.findAll(".chip");
const chip = chips[idx];