@open-press/core 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +14 -3
- package/src/openpress/app/WorkspaceGalleryPage.tsx +65 -39
- package/src/openpress/reader/SlidePresentationPage.tsx +7 -3
- package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +46 -43
- package/src/styles/openpress/workbench-toolbar.css +33 -0
- package/src/styles/openpress/workspace-gallery.css +130 -47
package/package.json
CHANGED
|
@@ -221,16 +221,27 @@ export function OpenPressApp() {
|
|
|
221
221
|
const presentationSlug = state.activeSlug || currentRouteFromLocation().slug;
|
|
222
222
|
const openPresentation = state.document.meta.type === "slides" && presentationSlug
|
|
223
223
|
? (pageIndex: number) => {
|
|
224
|
+
// requestFullscreen must be called synchronously within the user gesture.
|
|
225
|
+
// window.open() creates a new browsing context, so the user gesture is lost
|
|
226
|
+
// and the browser blocks fullscreen. Navigate in-place instead.
|
|
227
|
+
const root = globalThis.document?.documentElement;
|
|
228
|
+
if (root?.requestFullscreen) void root.requestFullscreen().catch(() => {});
|
|
224
229
|
const slug = normalizeSlug(presentationSlug);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
pushPressRoute(slug, "present", pageIndex);
|
|
231
|
+
setState((latest) => latest.status === "ready"
|
|
232
|
+
? { ...latest, runtimeMode: "present" }
|
|
233
|
+
: latest);
|
|
228
234
|
}
|
|
229
235
|
: undefined;
|
|
230
236
|
|
|
231
237
|
const exitPresentation = state.document.meta.type === "slides"
|
|
232
238
|
? (pageIndex: number) => {
|
|
233
239
|
if (state.status !== "ready") return;
|
|
240
|
+
// Exit fullscreen before returning to the workbench.
|
|
241
|
+
const activeDoc = globalThis.document;
|
|
242
|
+
if (activeDoc?.fullscreenElement && activeDoc?.exitFullscreen) {
|
|
243
|
+
void activeDoc.exitFullscreen().catch(() => {});
|
|
244
|
+
}
|
|
234
245
|
const slug = state.activeSlug || currentRouteFromLocation().slug;
|
|
235
246
|
if (slug) pushPressRoute(slug, "preview", pageIndex);
|
|
236
247
|
setState((latest) => latest.status === "ready"
|
|
@@ -1,49 +1,90 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type CSSProperties, type KeyboardEvent } from "react";
|
|
2
2
|
import type { HtmlPageBlock, ReaderDocument, WorkspaceManifest, WorkspaceManifestPress } from "../document-model";
|
|
3
3
|
|
|
4
|
+
type GalleryFilter = "all" | "pages" | "slides";
|
|
5
|
+
|
|
4
6
|
interface Props {
|
|
5
7
|
manifest: WorkspaceManifest;
|
|
6
|
-
// Called when the reader navigates into a specific Press. The host
|
|
7
|
-
// is responsible for routing (history.pushState, hash, etc.); the
|
|
8
|
-
// gallery just emits the chosen slug.
|
|
9
8
|
onSelectPress: (press: WorkspaceManifestPress) => void;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
// Reader landing page for multi-Press workspaces. Shows a Figma-style
|
|
13
|
-
// uniform-grid card per Press; each card lazily loads that Press's
|
|
14
|
-
// document.json and renders the first page as a thumbnail preview.
|
|
15
|
-
// Single-Press workspaces skip the gallery entirely.
|
|
16
11
|
export function WorkspaceGalleryPage({ manifest, onSelectPress }: Props) {
|
|
17
12
|
const heading = manifest.name ?? "Workspace";
|
|
18
|
-
const
|
|
13
|
+
const [filter, setFilter] = useState<GalleryFilter>("all");
|
|
14
|
+
|
|
15
|
+
const counts = {
|
|
16
|
+
all: manifest.presses.length,
|
|
17
|
+
pages: manifest.presses.filter((p) => p.type === "pages").length,
|
|
18
|
+
slides: manifest.presses.filter((p) => p.type === "slides").length,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const visiblePresses = filter === "all"
|
|
22
|
+
? manifest.presses
|
|
23
|
+
: manifest.presses.filter((p) => p.type === filter);
|
|
19
24
|
|
|
20
25
|
return (
|
|
21
26
|
<main className="openpress-workspace-gallery" aria-labelledby="workspace-gallery-heading">
|
|
22
27
|
<header className="openpress-workspace-gallery__header">
|
|
23
28
|
<div className="openpress-workspace-gallery__headline">
|
|
24
|
-
<p className="openpress-workspace-
|
|
29
|
+
<p className="openpress-workspace-gallery__brand">
|
|
30
|
+
<span className="openpress-workspace-gallery__brand-mark">open-press</span>
|
|
31
|
+
<span className="openpress-workspace-gallery__brand-sep" aria-hidden="true">/</span>
|
|
32
|
+
<span className="openpress-workspace-gallery__eyebrow">Workspace</span>
|
|
33
|
+
</p>
|
|
25
34
|
<h1 id="workspace-gallery-heading">{heading}</h1>
|
|
26
35
|
</div>
|
|
27
|
-
<p className="openpress-workspace-gallery__count">
|
|
28
|
-
<span>{pressCount}</span>
|
|
29
|
-
<small>{manifest.presses.length === 1 ? "document" : "documents"}</small>
|
|
30
|
-
</p>
|
|
31
36
|
</header>
|
|
32
37
|
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
<div className="openpress-workspace-gallery__body">
|
|
39
|
+
<nav className="openpress-workspace-gallery__sidebar" aria-label="文件類型篩選">
|
|
40
|
+
<FilterButton label="All" count={counts.all} active={filter === "all"} onClick={() => setFilter("all")} />
|
|
41
|
+
<FilterButton label="Pages" count={counts.pages} active={filter === "pages"} onClick={() => setFilter("pages")} />
|
|
42
|
+
<FilterButton label="Slides" count={counts.slides} active={filter === "slides"} onClick={() => setFilter("slides")} />
|
|
43
|
+
</nav>
|
|
44
|
+
|
|
45
|
+
<section className="openpress-workspace-gallery__main" aria-label={`${filter} 文件`}>
|
|
46
|
+
{visiblePresses.length > 0 ? (
|
|
47
|
+
<ul className="openpress-workspace-gallery__grid" role="list">
|
|
48
|
+
{visiblePresses.map((press) => (
|
|
49
|
+
<li key={press.slug || "root"} className="openpress-workspace-gallery__item">
|
|
50
|
+
<PressCard press={press} onSelect={() => onSelectPress(press)} />
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
) : (
|
|
55
|
+
<p className="openpress-workspace-gallery__empty">No {filter} documents.</p>
|
|
56
|
+
)}
|
|
57
|
+
</section>
|
|
58
|
+
</div>
|
|
40
59
|
</main>
|
|
41
60
|
);
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
function FilterButton({
|
|
64
|
+
label,
|
|
65
|
+
count,
|
|
66
|
+
active,
|
|
67
|
+
onClick,
|
|
68
|
+
}: {
|
|
69
|
+
label: string;
|
|
70
|
+
count: number;
|
|
71
|
+
active: boolean;
|
|
72
|
+
onClick: () => void;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className="openpress-workspace-gallery__filter-btn"
|
|
78
|
+
aria-pressed={active}
|
|
79
|
+
data-active={active ? "true" : "false"}
|
|
80
|
+
onClick={onClick}
|
|
81
|
+
>
|
|
82
|
+
<span className="openpress-workspace-gallery__filter-label">{label}</span>
|
|
83
|
+
<span className="openpress-workspace-gallery__filter-count">{String(count).padStart(2, "0")}</span>
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
function PressCard({ press, onSelect }: { press: WorkspaceManifestPress; onSelect: () => void }) {
|
|
48
89
|
const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
49
90
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -62,7 +103,7 @@ function PressCard({ press, onSelect }: { press: WorkspaceManifestPress; onSelec
|
|
|
62
103
|
aria-label={`Open ${press.title}`}
|
|
63
104
|
>
|
|
64
105
|
<PressThumbnail press={press} />
|
|
65
|
-
<div className="openpress-workspace-
|
|
106
|
+
<div className="openpress-workspace-gallery__card-body">
|
|
66
107
|
<div className="openpress-workspace-gallery__title">{press.title}</div>
|
|
67
108
|
<div className="openpress-workspace-gallery__meta">
|
|
68
109
|
{press.slug ? <span className="openpress-workspace-gallery__slug">{press.slug}</span> : null}
|
|
@@ -78,9 +119,6 @@ function PressCard({ press, onSelect }: { press: WorkspaceManifestPress; onSelec
|
|
|
78
119
|
function PressThumbnail({ press }: { press: WorkspaceManifestPress }) {
|
|
79
120
|
const [state, setState] = useState<ThumbnailState>({ status: "loading" });
|
|
80
121
|
|
|
81
|
-
// Lazy-load each Press's document.json so the gallery doesn't block
|
|
82
|
-
// on a network waterfall when there are many Press. Errors degrade
|
|
83
|
-
// to the geometry-only placeholder used by the loading state.
|
|
84
122
|
useEffect(() => {
|
|
85
123
|
let cancelled = false;
|
|
86
124
|
fetchFirstPage(press.documentUrl).then((page) => {
|
|
@@ -92,9 +130,6 @@ function PressThumbnail({ press }: { press: WorkspaceManifestPress }) {
|
|
|
92
130
|
return () => { cancelled = true; };
|
|
93
131
|
}, [press.documentUrl]);
|
|
94
132
|
|
|
95
|
-
// Outer card is uniform 4:3 (set in CSS). The page itself letterboxes
|
|
96
|
-
// inside via centered scale, so A4 portrait renders tall-and-narrow,
|
|
97
|
-
// social square renders centered, 16:9 slide stretches edge-to-edge.
|
|
98
133
|
return (
|
|
99
134
|
<div className="openpress-workspace-gallery__thumb" aria-hidden="true">
|
|
100
135
|
{state.status === "ready" ? (
|
|
@@ -147,10 +182,6 @@ function PageMiniature({ page, press }: { page: HtmlPageBlock; press: WorkspaceM
|
|
|
147
182
|
visibility: scale ? "visible" : "hidden",
|
|
148
183
|
};
|
|
149
184
|
|
|
150
|
-
// Match the wrapping used by PublicReaderPage so scoped CSS targeting
|
|
151
|
-
// `.openpress-html-page__html` selectors lights up identically. The
|
|
152
|
-
// outer frame owns centering; the page only scales from its top-left
|
|
153
|
-
// origin, which avoids mixed translate/scale centering drift.
|
|
154
185
|
const pageStyle: CSSProperties = {
|
|
155
186
|
"--openpress-page-width": `${pageWidthPx}px`,
|
|
156
187
|
"--openpress-page-height": `${pageHeightPx}px`,
|
|
@@ -172,7 +203,6 @@ function PageMiniature({ page, press }: { page: HtmlPageBlock; press: WorkspaceM
|
|
|
172
203
|
<div className={pageClass} style={pageStyle} data-openpress-thumb-page="true">
|
|
173
204
|
<div
|
|
174
205
|
className="openpress-html-page__html"
|
|
175
|
-
// Trusted HTML — same source as the reader's main render path.
|
|
176
206
|
dangerouslySetInnerHTML={{ __html: page.html }}
|
|
177
207
|
/>
|
|
178
208
|
</div>
|
|
@@ -198,10 +228,6 @@ async function fetchFirstPage(url: string): Promise<HtmlPageBlock | null> {
|
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
230
|
|
|
201
|
-
// Convert a CSS length string (px / mm / cm / in) into device pixels
|
|
202
|
-
// at 96 dpi. A4 pages are stored as "210mm" / "297mm" so the gallery
|
|
203
|
-
// and thumbnail scalers need this to compute their fit ratio — using
|
|
204
|
-
// the bare string would otherwise resolve to the default asset path.
|
|
205
231
|
function parsePxLength(value: string | undefined): number | null {
|
|
206
232
|
if (!value) return null;
|
|
207
233
|
const match = value.trim().match(/^([\d.]+)\s*(px|mm|cm|in)$/i);
|
|
@@ -29,9 +29,13 @@ export function SlidePresentationPage({
|
|
|
29
29
|
if (typeof window === "undefined") return 0;
|
|
30
30
|
return pageIndexFromHash(window.location.hash, normalizedPageCount) ?? 0;
|
|
31
31
|
});
|
|
32
|
-
const [uiMode, setUiMode] = useState<PresentationUiMode>(() =>
|
|
33
|
-
shouldStartImmersive()
|
|
34
|
-
|
|
32
|
+
const [uiMode, setUiMode] = useState<PresentationUiMode>(() => {
|
|
33
|
+
if (shouldStartImmersive()) return "immersive";
|
|
34
|
+
// Fullscreen may already be active if requestFullscreen() was called
|
|
35
|
+
// synchronously in the workbench click handler before in-place navigation.
|
|
36
|
+
if (typeof globalThis.document !== "undefined" && globalThis.document.fullscreenElement) return "immersive";
|
|
37
|
+
return "chrome";
|
|
38
|
+
});
|
|
35
39
|
const pageViewport = usePageViewportScale({
|
|
36
40
|
stageRef,
|
|
37
41
|
pageContainerRef: sourceContainerRef,
|
|
@@ -98,36 +98,9 @@ export function WorkbenchToolbarActions({
|
|
|
98
98
|
</button>
|
|
99
99
|
</div>
|
|
100
100
|
) : null}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
currentPageIndex={currentPageIndex}
|
|
105
|
-
pressTitle={pressTitle}
|
|
106
|
-
theme={theme}
|
|
107
|
-
onExportPdf={onExportPdf}
|
|
108
|
-
pdfDisabled={pdfDisabled}
|
|
109
|
-
pdfLabel={pdfLabel}
|
|
110
|
-
pdfStatusMessage={pdfStatusMessage}
|
|
111
|
-
pdfActionStatus={pdfActionStatus}
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
115
|
-
{isSlidePress && onOpenPresentation ? (
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
className="openpress-workbench-toolbar-action"
|
|
119
|
-
data-openpress-slide-present
|
|
120
|
-
data-openpress-toolbar-expanded="false"
|
|
121
|
-
data-openpress-toolbar-active="false"
|
|
122
|
-
aria-pressed="false"
|
|
123
|
-
title="進入放映模式"
|
|
124
|
-
aria-label="進入放映模式"
|
|
125
|
-
onClick={() => onOpenPresentation(currentPageIndex)}
|
|
126
|
-
>
|
|
127
|
-
<Play aria-hidden="true" />
|
|
128
|
-
<span className="openpress-workbench-toolbar-action__label">放映</span>
|
|
129
|
-
</button>
|
|
130
|
-
) : null}
|
|
101
|
+
|
|
102
|
+
{/* Center group: page geometry / zoom + workspace tools */}
|
|
103
|
+
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格與工具">
|
|
131
104
|
<button
|
|
132
105
|
type="button"
|
|
133
106
|
className="openpress-workbench-page-geometry"
|
|
@@ -146,25 +119,15 @@ export function WorkbenchToolbarActions({
|
|
|
146
119
|
onScaleModeChange={onScaleModeChange}
|
|
147
120
|
onPageLayoutModeChange={onPageLayoutModeChange}
|
|
148
121
|
/>
|
|
149
|
-
|
|
150
|
-
|
|
122
|
+
{workspaceMode ? (
|
|
123
|
+
<span className="openpress-workbench-toolbar__sep" aria-hidden="true" />
|
|
124
|
+
) : null}
|
|
151
125
|
{workspaceMode ? (
|
|
152
126
|
<SearchControl
|
|
153
127
|
sourceBlocksByPath={sourceBlocksByPath}
|
|
154
128
|
onSelectPage={onSelectPage}
|
|
155
129
|
/>
|
|
156
130
|
) : null}
|
|
157
|
-
{workspaceMode && editStatusMessage ? (
|
|
158
|
-
<span
|
|
159
|
-
className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
|
|
160
|
-
data-openpress-edit-status={inlineEditStatus.state}
|
|
161
|
-
role="status"
|
|
162
|
-
aria-live="polite"
|
|
163
|
-
>
|
|
164
|
-
{inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
|
|
165
|
-
<span>{editStatusMessage}</span>
|
|
166
|
-
</span>
|
|
167
|
-
) : null}
|
|
168
131
|
{workspaceMode ? (
|
|
169
132
|
<button
|
|
170
133
|
type="button"
|
|
@@ -183,6 +146,17 @@ export function WorkbenchToolbarActions({
|
|
|
183
146
|
<span className="openpress-dev-inspector-status">{inspectorSelectionLabel}</span>
|
|
184
147
|
</button>
|
|
185
148
|
) : null}
|
|
149
|
+
{workspaceMode && editStatusMessage ? (
|
|
150
|
+
<span
|
|
151
|
+
className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
|
|
152
|
+
data-openpress-edit-status={inlineEditStatus.state}
|
|
153
|
+
role="status"
|
|
154
|
+
aria-live="polite"
|
|
155
|
+
>
|
|
156
|
+
{inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
|
|
157
|
+
<span>{editStatusMessage}</span>
|
|
158
|
+
</span>
|
|
159
|
+
) : null}
|
|
186
160
|
{workspaceMode && inspectorMode ? (
|
|
187
161
|
<span
|
|
188
162
|
className="openpress-dev-inspector-status"
|
|
@@ -193,6 +167,21 @@ export function WorkbenchToolbarActions({
|
|
|
193
167
|
{inspectorCommentStatusMessage}
|
|
194
168
|
</span>
|
|
195
169
|
) : null}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Right group: export + deploy + present */}
|
|
173
|
+
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--right" aria-label="匯出與發布">
|
|
174
|
+
<ExportControl
|
|
175
|
+
pages={pages}
|
|
176
|
+
currentPageIndex={currentPageIndex}
|
|
177
|
+
pressTitle={pressTitle}
|
|
178
|
+
theme={theme}
|
|
179
|
+
onExportPdf={onExportPdf}
|
|
180
|
+
pdfDisabled={pdfDisabled}
|
|
181
|
+
pdfLabel={pdfLabel}
|
|
182
|
+
pdfStatusMessage={pdfStatusMessage}
|
|
183
|
+
pdfActionStatus={pdfActionStatus}
|
|
184
|
+
/>
|
|
196
185
|
{localDeployEnabled ? (
|
|
197
186
|
<DeploymentControl
|
|
198
187
|
info={deploymentInfo}
|
|
@@ -200,6 +189,20 @@ export function WorkbenchToolbarActions({
|
|
|
200
189
|
onDeploy={onDeploy}
|
|
201
190
|
/>
|
|
202
191
|
) : null}
|
|
192
|
+
{isSlidePress && onOpenPresentation ? (
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className="openpress-workbench-toolbar-action openpress-workbench-toolbar-action--primary"
|
|
196
|
+
data-openpress-slide-present
|
|
197
|
+
aria-pressed="false"
|
|
198
|
+
title="進入放映模式"
|
|
199
|
+
aria-label="進入放映模式"
|
|
200
|
+
onClick={() => onOpenPresentation(currentPageIndex)}
|
|
201
|
+
>
|
|
202
|
+
<Play aria-hidden="true" />
|
|
203
|
+
<span className="openpress-workbench-toolbar-action__label">放映</span>
|
|
204
|
+
</button>
|
|
205
|
+
) : null}
|
|
203
206
|
</div>
|
|
204
207
|
</>
|
|
205
208
|
);
|
|
@@ -235,6 +235,15 @@
|
|
|
235
235
|
height: 1px;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
.openpress-workbench-toolbar__sep {
|
|
239
|
+
display: block;
|
|
240
|
+
width: 1px;
|
|
241
|
+
height: 16px;
|
|
242
|
+
flex: 0 0 auto;
|
|
243
|
+
background: rgb(255 255 255 / 10%);
|
|
244
|
+
border-radius: 1px;
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
.openpress-workbench-toolbar-action {
|
|
239
248
|
position: relative;
|
|
240
249
|
display: inline-flex;
|
|
@@ -279,6 +288,30 @@
|
|
|
279
288
|
opacity: 0.62;
|
|
280
289
|
}
|
|
281
290
|
|
|
291
|
+
.openpress-workbench-toolbar-action--primary {
|
|
292
|
+
width: auto;
|
|
293
|
+
min-width: 30px;
|
|
294
|
+
max-width: min(34vw, 300px);
|
|
295
|
+
gap: 7px;
|
|
296
|
+
padding: 0 12px;
|
|
297
|
+
background: var(--openpress-accent, #df4b21);
|
|
298
|
+
border-color: transparent;
|
|
299
|
+
color: #fff;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.openpress-workbench-toolbar-action--primary .openpress-workbench-toolbar-action__label {
|
|
303
|
+
display: inline-flex;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.openpress-workbench-toolbar-action--primary:hover:not(:disabled) {
|
|
307
|
+
background: color-mix(in srgb, var(--openpress-accent, #df4b21) 82%, #fff);
|
|
308
|
+
color: #fff;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.openpress-workbench-toolbar-action--primary:active:not(:disabled) {
|
|
312
|
+
transform: translateY(1px);
|
|
313
|
+
}
|
|
314
|
+
|
|
282
315
|
.openpress-workbench-toolbar-action[data-openpress-toolbar-expanded="true"] {
|
|
283
316
|
width: auto;
|
|
284
317
|
min-width: 30px;
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
load the document directly, so this CSS is dormant until users
|
|
5
5
|
add a second <Press> to their <Workspace>.
|
|
6
6
|
|
|
7
|
-
Layout
|
|
8
|
-
|
|
9
|
-
loads its first page asynchronously and renders it scaled-down
|
|
10
|
-
inside the thumbnail slot. */
|
|
7
|
+
Layout: full-height dark page, header + two-column body
|
|
8
|
+
(narrow left sidebar for type filter, fluid right grid). */
|
|
11
9
|
|
|
12
10
|
.openpress-workspace-gallery {
|
|
13
11
|
--workspace-bg: #10110f;
|
|
@@ -20,10 +18,10 @@
|
|
|
20
18
|
--workspace-card-muted: #65635d;
|
|
21
19
|
--workspace-card-line: rgba(20, 20, 17, 0.1);
|
|
22
20
|
--workspace-card-stage: #e8e5dc;
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
--workspace-sidebar-w: 180px;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
25
24
|
min-height: 100vh;
|
|
26
|
-
max-width: none;
|
|
27
25
|
margin: 0;
|
|
28
26
|
padding: 3.6rem clamp(2rem, 4vw, 4.5rem) 6rem;
|
|
29
27
|
font-family: var(--openpress-font-body, system-ui, sans-serif);
|
|
@@ -31,11 +29,13 @@
|
|
|
31
29
|
background:
|
|
32
30
|
linear-gradient(180deg, var(--workspace-bg-soft), var(--workspace-bg) 42rem),
|
|
33
31
|
var(--workspace-bg);
|
|
32
|
+
gap: 2.25rem;
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
/* ── Header ──────────────────────────────────────────────── */
|
|
36
|
+
|
|
36
37
|
.openpress-workspace-gallery__header {
|
|
37
|
-
display:
|
|
38
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
38
|
+
display: flex;
|
|
39
39
|
align-items: end;
|
|
40
40
|
justify-content: space-between;
|
|
41
41
|
gap: 2.5rem;
|
|
@@ -48,50 +48,114 @@
|
|
|
48
48
|
gap: 0.75rem;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
.openpress-workspace-
|
|
51
|
+
.openpress-workspace-gallery__brand {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 0.5rem;
|
|
52
55
|
margin: 0;
|
|
53
|
-
color: var(--workspace-muted);
|
|
54
56
|
font-family: var(--openpress-font-mono, ui-monospace, monospace);
|
|
55
57
|
font-size: 0.68rem;
|
|
56
58
|
font-weight: 600;
|
|
57
|
-
letter-spacing: 0.
|
|
59
|
+
letter-spacing: 0.12em;
|
|
58
60
|
text-transform: uppercase;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
.openpress-workspace-gallery__brand-mark {
|
|
64
|
+
color: var(--workspace-ink);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.openpress-workspace-gallery__brand-sep {
|
|
68
|
+
color: var(--workspace-muted);
|
|
69
|
+
letter-spacing: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.openpress-workspace-gallery__eyebrow {
|
|
73
|
+
color: var(--workspace-muted);
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
.openpress-workspace-gallery__header h1 {
|
|
62
77
|
margin: 0;
|
|
63
78
|
font-family: var(--openpress-font-display, var(--openpress-font-body, system-ui));
|
|
64
|
-
font-size: clamp(
|
|
65
|
-
font-weight:
|
|
66
|
-
line-height:
|
|
67
|
-
letter-spacing: -0.
|
|
79
|
+
font-size: clamp(1.4rem, 2.6vw, 2.2rem);
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
line-height: 1.1;
|
|
82
|
+
letter-spacing: -0.02em;
|
|
68
83
|
color: var(--workspace-ink);
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
/* ── Body: sidebar + grid ────────────────────────────────── */
|
|
87
|
+
|
|
88
|
+
.openpress-workspace-gallery__body {
|
|
73
89
|
display: grid;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
grid-template-columns: var(--workspace-sidebar-w) 1fr;
|
|
91
|
+
align-items: start;
|
|
92
|
+
gap: 2.5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ── Left sidebar ────────────────────────────────────────── */
|
|
96
|
+
|
|
97
|
+
.openpress-workspace-gallery__sidebar {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 2px;
|
|
101
|
+
position: sticky;
|
|
102
|
+
top: 1.5rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.openpress-workspace-gallery__filter-btn {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: 0.6rem;
|
|
110
|
+
width: 100%;
|
|
111
|
+
padding: 0.52rem 0.75rem;
|
|
112
|
+
border: 1px solid transparent;
|
|
113
|
+
border-radius: 7px;
|
|
114
|
+
background: transparent;
|
|
115
|
+
color: var(--workspace-muted);
|
|
116
|
+
font-family: var(--openpress-font-body, system-ui, sans-serif);
|
|
117
|
+
font-size: 0.82rem;
|
|
118
|
+
font-weight: 500;
|
|
119
|
+
text-align: left;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition:
|
|
122
|
+
background 140ms ease,
|
|
123
|
+
color 140ms ease,
|
|
124
|
+
border-color 140ms ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.openpress-workspace-gallery__filter-btn:hover {
|
|
128
|
+
background: rgba(244, 241, 232, 0.06);
|
|
77
129
|
color: var(--workspace-ink);
|
|
78
|
-
font-family: var(--openpress-font-mono, ui-monospace, monospace);
|
|
79
|
-
line-height: 1;
|
|
80
130
|
}
|
|
81
131
|
|
|
82
|
-
.openpress-workspace-
|
|
132
|
+
.openpress-workspace-gallery__filter-btn[data-active="true"] {
|
|
133
|
+
background: rgba(244, 241, 232, 0.1);
|
|
134
|
+
border-color: rgba(244, 241, 232, 0.14);
|
|
83
135
|
color: var(--workspace-ink);
|
|
84
|
-
font-size: 2rem;
|
|
85
|
-
font-weight: 500;
|
|
86
|
-
letter-spacing: -0.04em;
|
|
87
136
|
}
|
|
88
137
|
|
|
89
|
-
.openpress-workspace-
|
|
138
|
+
.openpress-workspace-gallery__filter-label {
|
|
139
|
+
flex: 1 1 auto;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.openpress-workspace-gallery__filter-count {
|
|
143
|
+
flex: 0 0 auto;
|
|
144
|
+
font-family: var(--openpress-font-mono, ui-monospace, monospace);
|
|
145
|
+
font-size: 0.72rem;
|
|
146
|
+
font-weight: 500;
|
|
90
147
|
color: var(--workspace-muted);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
148
|
+
letter-spacing: 0.04em;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.openpress-workspace-gallery__filter-btn[data-active="true"] .openpress-workspace-gallery__filter-count {
|
|
152
|
+
color: var(--workspace-ink);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── Main grid ───────────────────────────────────────────── */
|
|
156
|
+
|
|
157
|
+
.openpress-workspace-gallery__main {
|
|
158
|
+
min-width: 0;
|
|
95
159
|
}
|
|
96
160
|
|
|
97
161
|
.openpress-workspace-gallery__grid {
|
|
@@ -100,8 +164,6 @@
|
|
|
100
164
|
padding: 0;
|
|
101
165
|
display: grid;
|
|
102
166
|
align-items: start;
|
|
103
|
-
/* Uniform card size — Figma-style. Outer thumb is fixed 4:3, the
|
|
104
|
-
inner page letterboxes to its own geometry. */
|
|
105
167
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
|
106
168
|
gap: 1.5rem;
|
|
107
169
|
}
|
|
@@ -110,6 +172,15 @@
|
|
|
110
172
|
display: flex;
|
|
111
173
|
}
|
|
112
174
|
|
|
175
|
+
.openpress-workspace-gallery__empty {
|
|
176
|
+
margin: 0;
|
|
177
|
+
padding: 3rem 0;
|
|
178
|
+
color: var(--workspace-muted);
|
|
179
|
+
font-size: 0.88rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── Card ────────────────────────────────────────────────── */
|
|
183
|
+
|
|
113
184
|
.openpress-workspace-gallery__card {
|
|
114
185
|
appearance: none;
|
|
115
186
|
display: grid;
|
|
@@ -138,13 +209,12 @@
|
|
|
138
209
|
outline: none;
|
|
139
210
|
}
|
|
140
211
|
|
|
212
|
+
/* ── Thumbnail ───────────────────────────────────────────── */
|
|
213
|
+
|
|
141
214
|
.openpress-workspace-gallery__thumb {
|
|
142
215
|
position: relative;
|
|
143
216
|
display: block;
|
|
144
217
|
width: 100%;
|
|
145
|
-
/* Uniform 4:3 outer slot across every card. The page itself
|
|
146
|
-
letterboxes inside via centered scale, so each Press shows at its
|
|
147
|
-
own true aspect against the gradient background. */
|
|
148
218
|
aspect-ratio: 4 / 3;
|
|
149
219
|
background:
|
|
150
220
|
linear-gradient(
|
|
@@ -220,7 +290,9 @@
|
|
|
220
290
|
50% { opacity: 0.55; }
|
|
221
291
|
}
|
|
222
292
|
|
|
223
|
-
|
|
293
|
+
/* ── Card body ───────────────────────────────────────────── */
|
|
294
|
+
|
|
295
|
+
.openpress-workspace-gallery__card-body {
|
|
224
296
|
display: grid;
|
|
225
297
|
align-content: space-between;
|
|
226
298
|
gap: 1.2rem;
|
|
@@ -262,10 +334,6 @@
|
|
|
262
334
|
white-space: nowrap;
|
|
263
335
|
}
|
|
264
336
|
|
|
265
|
-
.openpress-workspace-gallery__geom {
|
|
266
|
-
color: var(--workspace-card-muted);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
337
|
.openpress-workspace-gallery__geom {
|
|
270
338
|
display: inline-flex;
|
|
271
339
|
align-items: center;
|
|
@@ -279,16 +347,31 @@
|
|
|
279
347
|
white-space: nowrap;
|
|
280
348
|
}
|
|
281
349
|
|
|
350
|
+
/* ── Responsive ──────────────────────────────────────────── */
|
|
351
|
+
|
|
352
|
+
@media (max-width: 860px) {
|
|
353
|
+
.openpress-workspace-gallery__body {
|
|
354
|
+
grid-template-columns: 1fr;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.openpress-workspace-gallery__sidebar {
|
|
358
|
+
position: static;
|
|
359
|
+
flex-direction: row;
|
|
360
|
+
flex-wrap: wrap;
|
|
361
|
+
gap: 6px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.openpress-workspace-gallery__filter-btn {
|
|
365
|
+
width: auto;
|
|
366
|
+
flex: 0 0 auto;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
282
370
|
@media (max-width: 720px) {
|
|
283
371
|
.openpress-workspace-gallery {
|
|
284
372
|
padding: 2.25rem 1rem 4rem;
|
|
285
373
|
}
|
|
286
374
|
|
|
287
|
-
.openpress-workspace-gallery__header {
|
|
288
|
-
display: grid;
|
|
289
|
-
align-items: start;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
375
|
.openpress-workspace-gallery__grid {
|
|
293
376
|
grid-template-columns: 1fr;
|
|
294
377
|
}
|