diff --git a/frontend/src/component/input/chip-selector.vue b/frontend/src/component/input/chip-selector.vue
index 49f9582cd..fcef92eeb 100644
--- a/frontend/src/component/input/chip-selector.vue
+++ b/frontend/src/component/input/chip-selector.vue
@@ -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;
diff --git a/frontend/src/component/lightbox.vue b/frontend/src/component/lightbox.vue
index 690150164..dcf08acbd 100644
--- a/frontend/src/component/lightbox.vue
+++ b/frontend/src/component/lightbox.vue
@@ -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.
diff --git a/frontend/src/component/lightbox/sidebar.vue b/frontend/src/component/lightbox/sidebar.vue
index da2ce3a9e..edfaa9f86 100644
--- a/frontend/src/component/lightbox/sidebar.vue
+++ b/frontend/src/component/lightbox/sidebar.vue
@@ -63,7 +63,7 @@
@blur="onInlineFieldBlur"
>
-
+
{{ $gettext("Add a Caption") }}
-
+
{{ f.read(photo) }}
{{ f.placeholder ? f.placeholder : f.label }}
diff --git a/frontend/src/css/chip-selector.css b/frontend/src/css/chip-selector.css
index a8875e165..00f36594b 100644
--- a/frontend/src/css/chip-selector.css
+++ b/frontend/src/css/chip-selector.css
@@ -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;
+ }
}
diff --git a/frontend/src/css/lightbox.css b/frontend/src/css/lightbox.css
index 6ed4d7f53..3044e983b 100644
--- a/frontend/src/css/lightbox.css
+++ b/frontend/src/css/lightbox.css
@@ -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;
diff --git a/frontend/src/css/meta.css b/frontend/src/css/meta.css
index 5bf041c57..3f5eff89a 100644
--- a/frontend/src/css/meta.css
+++ b/frontend/src/css/meta.css
@@ -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
diff --git a/frontend/src/css/text.css b/frontend/src/css/text.css
index 0b6fd474b..03102be84 100644
--- a/frontend/src/css/text.css
+++ b/frontend/src/css/text.css
@@ -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;
}
diff --git a/frontend/src/css/views.css b/frontend/src/css/views.css
index c5451c360..124038563 100644
--- a/frontend/src/css/views.css
+++ b/frontend/src/css/views.css
@@ -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;
}
diff --git a/frontend/src/css/vuetify.css b/frontend/src/css/vuetify.css
index d8fc4d3a5..aac953f53 100644
--- a/frontend/src/css/vuetify.css
+++ b/frontend/src/css/vuetify.css
@@ -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;
}
diff --git a/frontend/src/options/themes.js b/frontend/src/options/themes.js
index 90360aeae..b1856e16f 100644
--- a/frontend/src/options/themes.js
+++ b/frontend/src/options/themes.js
@@ -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",
diff --git a/frontend/src/options/ui.js b/frontend/src/options/ui.js
index 4e4470ec7..fdd868c9b 100644
--- a/frontend/src/options/ui.js
+++ b/frontend/src/options/ui.js
@@ -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",
diff --git a/frontend/tests/vitest/component/file/chip-selector.test.js b/frontend/tests/vitest/component/file/chip-selector.test.js
index e205ca8a2..ef069d9d9 100644
--- a/frontend/tests/vitest/component/file/chip-selector.test.js
+++ b/frontend/tests/vitest/component/file/chip-selector.test.js
@@ -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];