refactor(print_preview): extract styles

This commit is contained in:
Elian Doran
2026-04-26 09:27:10 +03:00
parent f9a5935b85
commit 8c8e797dda
4 changed files with 81 additions and 18 deletions
+6
View File
@@ -129,6 +129,12 @@ Common UI components are available in `apps/client/src/widgets/react/` — **alw
**Do not use Bootstrap utility classes** (e.g. `form-control-sm`, `form-select-sm`, `input-group`) on these components — they manage their own styling internally. If you need to adjust sizing or layout, use props provided by the component or CSS custom properties, not Bootstrap overrides.
#### Component Styling
- **Avoid inline styles** — do not use the `style` attribute/prop on JSX elements unless absolutely necessary (e.g. a truly dynamic, computed value that cannot be expressed in CSS). Static layout, sizing, spacing, and visual properties must go in CSS.
- **Per-component CSS files**: each component should have a matching `.css` file (e.g. `my_dialog.tsx``my_dialog.css`), imported at the top of the component file.
- **CSS nesting for scoping**: since CSS modules are not available, scope styles using a root class and native CSS nesting. For example, a dialog with `className="my-dialog"` should have its styles nested under `.modal.my-dialog { … }`.
- **Reuse existing components** instead of building custom markup — prefer `FormTextBox`, `FormTextBoxWithUnit`, `FormSelect`, `Slider`, `Button`, etc. over hand-rolled `<input>`, `<select>`, or `<button>` elements.
## Development Workflow
### Running & Testing
+6
View File
@@ -80,6 +80,12 @@ Common UI components are available in `apps/client/src/widgets/react/` — **alw
**Do not use Bootstrap utility classes** (e.g. `form-control-sm`, `form-select-sm`, `input-group`) on these components — they manage their own styling internally. If you need to adjust sizing or layout, use props provided by the component or CSS custom properties, not Bootstrap overrides.
#### Component Styling
- **Avoid inline styles** — do not use the `style` attribute/prop on JSX elements unless absolutely necessary (e.g. a truly dynamic, computed value that cannot be expressed in CSS). Static layout, sizing, spacing, and visual properties must go in CSS.
- **Per-component CSS files**: each component should have a matching `.css` file (e.g. `my_dialog.tsx``my_dialog.css`), imported at the top of the component file.
- **CSS nesting for scoping**: since CSS modules are not available, scope styles using a root class and native CSS nesting. For example, a dialog with `className="my-dialog"` should have its styles nested under `.modal.my-dialog { … }`.
- **Reuse existing components** instead of building custom markup — prefer `FormTextBox`, `FormTextBoxWithUnit`, `FormSelect`, `Slider`, `Button`, etc. over hand-rolled `<input>`, `<select>`, or `<button>` elements.
#### API Architecture
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
@@ -0,0 +1,54 @@
.modal.print-preview-dialog {
.modal-body {
height: 78vh;
padding: 0;
display: flex;
}
.print-preview-settings {
padding: 16px;
min-width: 250px;
overflow-y: auto;
}
.print-preview-page-ranges {
width: 140px;
}
.print-preview-pane {
flex: 1;
position: relative;
}
.print-preview-loading-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
background-color: var(--modal-bg-color, rgba(255, 255, 255, 0.8));
.bx-loader-circle {
font-size: 2rem;
}
}
.margin-editor {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 0;
}
.margin-editor-row {
display: flex;
gap: 24px;
align-items: center;
}
.margin-spinner {
width: 130px;
}
}
@@ -1,3 +1,5 @@
import "./print_preview.css";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
@@ -280,7 +282,6 @@ export default function PrintPreviewDialog() {
size="xl"
show={shown}
onHidden={handleClose}
bodyStyle={{ height: "78vh", padding: 0, display: "flex" }}
footerAlignment="between"
footer={
<>
@@ -307,7 +308,7 @@ export default function PrintPreviewDialog() {
</>
}
>
<div style={{ padding: "16px", minWidth: "250px", overflowY: "auto" }}>
<div class="print-preview-settings">
<OptionsSection>
<OptionsRow name="destination" label={t("print_preview.destination")}>
<Dropdown
@@ -404,21 +405,20 @@ export default function PrintPreviewDialog() {
description={!pageRangesValid ? t("print_preview.page_ranges_invalid") : t("print_preview.page_ranges_hint")}
>
<FormTextBox
className={!pageRangesValid ? "is-invalid" : ""}
className={`print-preview-page-ranges ${!pageRangesValid ? "is-invalid" : ""}`}
currentValue={pageRanges}
placeholder={t("print_preview.page_ranges_placeholder")}
onChange={(value) => setPageRanges(value)}
disabled={loading}
style={{ width: "140px" }}
/>
</OptionsRow>
</OptionsSection>
</div>
<div style={{ flex: 1, position: "relative" }}>
<div class="print-preview-pane">
{loading && (
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1, backgroundColor: "var(--modal-bg-color, rgba(255,255,255,0.8))" }}>
<span class="bx bx-loader-circle bx-spin" style={{ fontSize: "2rem" }} />
<div class="print-preview-loading-overlay">
<span class="bx bx-loader-circle bx-spin" />
</div>
)}
{pdfUrl && <PdfViewer pdfUrl={pdfUrl} toolbar={false} disableSelection />}
@@ -440,31 +440,28 @@ function MarginEditor({ margins, onChange, disabled }: {
onChange: (side: keyof CustomMargins, value: number) => void;
disabled: boolean;
}) {
const spinnerStyle = { width: "130px" };
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "4px", padding: "8px 0" }}>
<MarginSpinner label={t("print_preview.margin_top")} value={margins.top} onChange={(v) => onChange("top", v)} disabled={disabled} style={spinnerStyle} />
<div style={{ display: "flex", gap: "24px", alignItems: "center" }}>
<MarginSpinner label={t("print_preview.margin_left")} value={margins.left} onChange={(v) => onChange("left", v)} disabled={disabled} style={spinnerStyle} />
<MarginSpinner label={t("print_preview.margin_right")} value={margins.right} onChange={(v) => onChange("right", v)} disabled={disabled} style={spinnerStyle} />
<div class="margin-editor">
<MarginSpinner label={t("print_preview.margin_top")} value={margins.top} onChange={(v) => onChange("top", v)} disabled={disabled} />
<div class="margin-editor-row">
<MarginSpinner label={t("print_preview.margin_left")} value={margins.left} onChange={(v) => onChange("left", v)} disabled={disabled} />
<MarginSpinner label={t("print_preview.margin_right")} value={margins.right} onChange={(v) => onChange("right", v)} disabled={disabled} />
</div>
<MarginSpinner label={t("print_preview.margin_bottom")} value={margins.bottom} onChange={(v) => onChange("bottom", v)} disabled={disabled} style={spinnerStyle} />
<MarginSpinner label={t("print_preview.margin_bottom")} value={margins.bottom} onChange={(v) => onChange("bottom", v)} disabled={disabled} />
</div>
);
}
function MarginSpinner({ label, value, onChange, disabled, style }: {
function MarginSpinner({ label, value, onChange, disabled }: {
label: string;
value: number;
onChange: (value: number) => void;
disabled: boolean;
style?: Record<string, string>;
}) {
return (
<FormTextBoxWithUnit
type="number"
style={style}
className="margin-spinner"
title={label}
aria-label={label}
currentValue={String(value)}