Skip to content

Commit 308df6c

Browse files
authored
Media options: improvements (#10888)
2 parents 5e63c96 + d6ccb3f commit 308df6c

6 files changed

Lines changed: 177 additions & 107 deletions

File tree

apps/client/src/translations/en/translation.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,6 @@
18771877
"images_section_title": "Images",
18781878
"download_images_automatically": "Download images automatically",
18791879
"download_images_description": "Download referenced online images from pasted HTML so they are available offline.",
1880-
"enable_image_compression": "Image compression",
18811880
"automatic_image_compression": "Automatically compress images",
18821881
"enable_image_compression_description": "Compress and resize images when they are uploaded or pasted.",
18831882
"max_image_dimensions": "Max image dimensions",

apps/client/src/widgets/react/Card.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,24 @@
2323
}
2424

2525
.tn-card-heading {
26+
display: flex;
27+
align-items: center;
2628
margin-bottom: 10px;
2729
font-size: .75rem;
2830
font-weight: 600;
2931
letter-spacing: .4pt;
3032
text-transform: uppercase;
3133
}
3234

35+
/* Kept at the far end of the heading line, where the options page's own sections keep theirs, and
36+
sized against the heading rather than against a full-height toolbar. */
37+
.tn-card-heading-actions {
38+
--icon-button-size: 28px;
39+
display: flex;
40+
gap: 4px;
41+
margin-inline-start: auto;
42+
}
43+
3344
.tn-card-heading:has(+ .tn-card-description) {
3445
margin-bottom: 4px;
3546
}

apps/client/src/widgets/react/Card.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,23 @@ export interface CardProps {
3333
heading?: string;
3434
/** Sentence introducing the card, shown between the heading and the first section. */
3535
description?: ComponentChildren;
36+
/**
37+
* Controls for the card as a whole, kept at the far end of its heading — a help mark, or a
38+
* button that adds to what the card holds.
39+
*
40+
* Taken as children rather than named one by one, so that the card needs no import of its own
41+
* for them: `Card` is in the login and setup bundles, which have no business pulling in the app
42+
* a help mark would reach for.
43+
*/
44+
actions?: ComponentChildren;
3645
}
3746

3847
export function Card(props: {children: ComponentChildren} & CardProps) {
3948
return <div className={clsx("tn-card", props.className)}>
40-
{props.heading && <h5 class="tn-card-heading">{props.heading}</h5>}
49+
{(props.heading || props.actions) && <h5 class="tn-card-heading">
50+
{props.heading}
51+
{props.actions && <span className="tn-card-heading-actions">{props.actions}</span>}
52+
</h5>}
4153
{props.description && <p className="tn-card-description">{props.description}</p>}
4254
<div className="tn-card-body">
4355
{props.children}
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
/* A real card where the rest of the page is options sections. It keeps the card styling the theme
2-
already gives every other one — the wrapper is here for the page's own spacing, so the card sits
3-
where a section would rather than flush against the one above it. */
4-
.media-image-compression {
5-
/* The choice buttons and their labels sit on one line in a dialog sized to its content; a
6-
settings page is only as wide as its pane, and a narrow one has to be allowed to stack them
7-
rather than push the card wider than the page. */
8-
.image-compression-section {
9-
flex-wrap: wrap;
10-
}
1+
/* The choice buttons and their labels sit on one line in a dialog sized to its content; a settings
2+
page is only as wide as its pane, and a narrow one has to be allowed to stack them rather than
3+
push the card wider than the page. */
4+
.media-images .image-compression-section {
5+
flex-wrap: wrap;
6+
}
7+
8+
/* The width the compression sliders were given, so the two cards read as one page. */
9+
.media-ocr .slider {
10+
width: 140px;
11+
}
12+
13+
/* Wide enough for the sentence written across it, and fixed: the label beside it would otherwise
14+
take the row's slack and leave the bar too narrow to read its own progress in. */
15+
.media-batch-ocr .media-batch-ocr-progress {
16+
flex: 0 0 200px;
17+
height: 24px;
1118
}

apps/client/src/widgets/type_widgets/options/media.spec.tsx

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { act } from "preact/test-utils";
33
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44

55
/**
6-
* The image compression card: the tool's own rows, driving the options that govern every image
7-
* arriving in the database rather than a run the user is watching.
6+
* The images card: everything that happens to an image on its way in, compression included — the
7+
* tool's own rows, driving the options that govern every image arriving in the database rather than
8+
* a run the user is watching.
89
*
910
* What is worth holding here is the wiring, since the rows themselves are tested where they live —
1011
* that each row reads and writes the option it stands for, that the group hangs off the switch
@@ -73,13 +74,46 @@ function open(overrides: Record<string, string> = {}) {
7374

7475
/** The card's rows, in the order they are drawn, named by their titles. */
7576
function rowTitles(): (string | undefined)[] {
76-
return [ ...host.querySelectorAll(".media-image-compression .image-compression-section") ]
77-
.map((row) => row.querySelector(".image-compression-section-title")?.textContent ?? undefined);
77+
return rows().map(titleOf);
78+
}
79+
80+
function rows(): HTMLElement[] {
81+
return [ ...host.querySelectorAll<HTMLElement>(".media-images .tn-card-section") ];
7882
}
7983

8084
function row(title: string): HTMLElement | undefined {
81-
return [ ...host.querySelectorAll<HTMLElement>(".media-image-compression .image-compression-section") ]
82-
.find((candidate) => candidate.querySelector(".image-compression-section-title")?.textContent === title);
85+
return rows().find((candidate) => titleOf(candidate) === title);
86+
}
87+
88+
/**
89+
* What a row is called, whichever of the two kinds it is: the page's own option rows carry the
90+
* sentence inside the label, so the title is the text ahead of it, where the compression tool's
91+
* rows keep the title in an element of its own.
92+
*/
93+
function titleOf(row: Element): string | undefined {
94+
const label = row.querySelector(".tn-card-option-label");
95+
96+
return label
97+
? label.childNodes[0]?.textContent ?? undefined
98+
: row.querySelector(".image-compression-section-title")?.textContent ?? undefined;
99+
}
100+
101+
/** The sentence beneath a row's title, from whichever of the two kinds of row it is. */
102+
function describes(title: string): string | undefined {
103+
const row = rowOrFail(title);
104+
105+
return row.querySelector(".tn-card-option-description")?.textContent
106+
?? row.querySelector(".image-compression-section-description")?.textContent
107+
?? undefined;
108+
}
109+
110+
function rowOrFail(title: string): HTMLElement {
111+
const found = row(title);
112+
if (!found) {
113+
throw new Error(`No row titled "${title}".`);
114+
}
115+
116+
return found;
83117
}
84118

85119
/** Presses one of a row's choice buttons by its label. */
@@ -102,15 +136,17 @@ afterEach(() => {
102136
vi.clearAllMocks();
103137
});
104138

105-
describe("the image compression card", () => {
139+
describe("the images card", () => {
106140
it("hangs the whole group off its switch, nesting what qualifies each choice", () => {
107141
open();
108142

109-
// What an untouched install shows, in order: the switch, scaling with its bound, then one
110-
// exclusive choice per format. Recompressing a JPEG brings a quality with it; optimizing a
111-
// PNG does not, there being no quality to reducing it to a palette — so only one of the two
112-
// choices carries a nested row here.
143+
// What an untouched install shows, in order: fetching a referenced image at all, then the
144+
// switch over what is done to the ones that arrive, scaling with its bound, and one
145+
// exclusive choice per format. Recompressing a JPEG brings a quality with it; optimizing
146+
// a PNG does not, there being no quality to reducing it to a palette — so only one of the
147+
// two choices carries a nested row here.
113148
expect(rowTitles()).toEqual([
149+
"images.download_images_automatically",
114150
"images.automatic_image_compression",
115151
"space_usage.compress_resize",
116152
"space_usage.compress_max_dimensions",
@@ -120,9 +156,13 @@ describe("the image compression card", () => {
120156
]);
121157

122158
// Switched off, the settings are not merely greyed out but gone: there is nothing for them
123-
// to govern, and a bound sitting there would read as one still in force.
159+
// to govern, and a bound sitting there would read as one still in force. What the switch
160+
// does not govern stays where it was — the card is not the compression alone.
124161
open({ compressImages: "false" });
125-
expect(rowTitles()).toEqual([ "images.automatic_image_compression" ]);
162+
expect(rowTitles()).toEqual([
163+
"images.download_images_automatically",
164+
"images.automatic_image_compression"
165+
]);
126166
});
127167

128168
it("drops a quality that no longer qualifies anything", () => {
@@ -131,6 +171,7 @@ describe("the image compression card", () => {
131171
// Neither format is being re-encoded, so neither quality is in force — and resizing is
132172
// still on offer, being the one step that reaches an image whatever its encoding.
133173
expect(rowTitles()).toEqual([
174+
"images.download_images_automatically",
134175
"images.automatic_image_compression",
135176
"space_usage.compress_resize",
136177
"space_usage.compress_max_dimensions",
@@ -146,9 +187,6 @@ describe("the image compression card", () => {
146187
it("carries the sentences the settings had before they were rows", () => {
147188
open();
148189

149-
const describes = (title: string) =>
150-
row(title)?.querySelector(".image-compression-section-description")?.textContent;
151-
152190
expect(describes("images.automatic_image_compression")).toBe("images.enable_image_compression_description");
153191
expect(describes("space_usage.compress_resize")).toBe("images.max_image_dimensions_description");
154192
// Both qualities take the same advice, being the same scale read twice.

0 commit comments

Comments
 (0)