@anchrd/intel-ui 0.41.0 → 0.42.0
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/attachment-viewer/attachment-viewer.tsx +6 -5
- package/src/board/board-assignee/board-assignee.tsx +45 -0
- package/src/board/board-chip/board-chip.tsx +31 -0
- package/src/board/board-status/board-status.tsx +60 -0
- package/src/board/board-table/board-table.tsx +207 -101
- package/src/board/board-task/board-task.tsx +22 -105
- package/src/file-preview/file-preview-view.tsx +326 -12
- package/src/file-preview/pdf-file-preview.tsx +1 -1
- package/src/i18n/de.json +25 -2
- package/src/i18n/en.json +25 -2
- package/src/i18n/es.json +25 -2
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import type { BoardTask } from "@anchrd/intel-contract/board";
|
|
2
|
-
import { ARCHIVE_COLUMN_ID } from "@anchrd/intel-contract/board";
|
|
3
2
|
import { Lock, Plus, Square, SquareCheck } from "lucide-react";
|
|
4
3
|
import { useState } from "react";
|
|
5
|
-
import {
|
|
4
|
+
import { AssigneeChip } from "@/board/board-assignee/board-assignee.tsx";
|
|
5
|
+
import { BoardChip } from "@/board/board-chip/board-chip.tsx";
|
|
6
6
|
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
7
|
+
import { StatusChip } from "@/board/board-status/board-status.tsx";
|
|
7
8
|
import {
|
|
8
9
|
DropdownMenu,
|
|
9
10
|
DropdownMenuContent,
|
|
10
11
|
DropdownMenuItem,
|
|
11
|
-
DropdownMenuRadioGroup,
|
|
12
|
-
DropdownMenuRadioItem,
|
|
13
12
|
DropdownMenuTrigger,
|
|
14
13
|
} from "@/components/ui/dropdown-menu.tsx";
|
|
15
14
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
16
|
-
import { initials } from "@/user-name/user-name.ts";
|
|
17
15
|
import { type LinkKind, linksOf, progressOf, type TaskLink } from "./board-task.ts";
|
|
18
16
|
|
|
19
17
|
const linkIcons: Record<LinkKind, typeof Lock> = {
|
|
@@ -22,54 +20,6 @@ const linkIcons: Record<LinkKind, typeof Lock> = {
|
|
|
22
20
|
blocked: Lock,
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
/**
|
|
26
|
-
* The circle for whoever a card is for, and the way to take the assignment off.
|
|
27
|
-
*
|
|
28
|
-
* ⚠️ **One person, not a group.** Jack's decision 2026-08-20: the STYLE is borrowed from the access
|
|
29
|
-
* summary, the field stays `assigneeId`. A row of circles here would imply a second data model that
|
|
30
|
-
* does not exist.
|
|
31
|
-
*
|
|
32
|
-
* ⚠️ **Only ever drawn for somebody.** "Nobody yet" as a chip is a placeholder for an absence, and
|
|
33
|
-
* an absence needs no place on screen (#692).
|
|
34
|
-
*
|
|
35
|
-
* ⚠️ **The NAME lives on the button, and the circle is hidden.** Children of a `<button>` are
|
|
36
|
-
* presentational in ARIA, so a label inside one is never announced and the button's own name wins.
|
|
37
|
-
* Wrapping the circle in a control therefore takes the person out of the accessibility tree unless
|
|
38
|
-
* the control says it too. That is the trap in `packages/ui/CLAUDE.md`, walked into by the very
|
|
39
|
-
* change that documented it.
|
|
40
|
-
*/
|
|
41
|
-
function AssigneeChip({ id, clear }: { id: string; clear(): void }) {
|
|
42
|
-
const i18n = useI18n();
|
|
43
|
-
const label = useAssigneeLabel(id);
|
|
44
|
-
return (
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
aria-label={i18n.t("board.clearAssignee", { name: label })}
|
|
48
|
-
title={label}
|
|
49
|
-
onClick={clear}
|
|
50
|
-
className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
51
|
-
>
|
|
52
|
-
<AssigneeCircle label={label} />
|
|
53
|
-
</button>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function AssigneeCircle({ label }: { label: string }) {
|
|
58
|
-
return (
|
|
59
|
-
<span
|
|
60
|
-
// ⚠️ **Hidden, and it must stay hidden.** This span used to carry `role="img"` with the
|
|
61
|
-
// person's name, and that reached nobody: it sits inside a `<button>`, whose children are
|
|
62
|
-
// presentational in ARIA. Giving it a role and a label back would not add a second announcement
|
|
63
|
-
// — it would add none, while making the code look as though the name were covered here rather
|
|
64
|
-
// than on the button. See `AssigneeChip` above and `packages/ui/CLAUDE.md`.
|
|
65
|
-
aria-hidden="true"
|
|
66
|
-
className="flex size-7 items-center justify-center rounded-full bg-accent text-xs font-medium text-accent-foreground ring-2 ring-background"
|
|
67
|
-
>
|
|
68
|
-
{initials(label)}
|
|
69
|
-
</span>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
23
|
/** One entry of the link list. The icon carries the kind; nothing writes the word out. */
|
|
74
24
|
function LinkRow({ link, onOpen }: { link: TaskLink; onOpen(id: string): void }) {
|
|
75
25
|
const i18n = useI18n();
|
|
@@ -145,57 +95,37 @@ export function BoardTaskDocument({
|
|
|
145
95
|
{/* ⚠️ ONE line of chips, and no field labels. "Status:" in front of a chip that says "Backlog"
|
|
146
96
|
says nothing the chip does not — and it turns a quiet head into a form. */}
|
|
147
97
|
<div className="flex flex-wrap items-center gap-2">
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</DropdownMenuTrigger>
|
|
155
|
-
<DropdownMenuContent align="start">
|
|
156
|
-
<DropdownMenuRadioGroup
|
|
157
|
-
value={task.status}
|
|
158
|
-
// ⚠️ Where a task SITS is a move, not a field edit: the server computes a position and
|
|
159
|
-
// checks the cycles for it. Everything else on this screen is a plain change.
|
|
160
|
-
onValueChange={(next) => write({ status: next })}
|
|
161
|
-
>
|
|
162
|
-
{columns.map((entry) => (
|
|
163
|
-
<DropdownMenuRadioItem key={entry.id} value={entry.id}>
|
|
164
|
-
{entry.title}
|
|
165
|
-
</DropdownMenuRadioItem>
|
|
166
|
-
))}
|
|
167
|
-
<DropdownMenuRadioItem value={ARCHIVE_COLUMN_ID}>
|
|
168
|
-
{i18n.t("board.column.archive")}
|
|
169
|
-
</DropdownMenuRadioItem>
|
|
170
|
-
</DropdownMenuRadioGroup>
|
|
171
|
-
</DropdownMenuContent>
|
|
172
|
-
</DropdownMenu>
|
|
98
|
+
<StatusChip
|
|
99
|
+
status={task.status}
|
|
100
|
+
title={column?.title ?? task.status}
|
|
101
|
+
columns={columns}
|
|
102
|
+
write={write}
|
|
103
|
+
/>
|
|
173
104
|
|
|
174
105
|
{/* ⚠️ Two chips when both dates are set, not a range. A range reads as one fact; these
|
|
175
106
|
are two, and each is removed on its own. */}
|
|
176
107
|
{task.startDate === null ? null : (
|
|
177
|
-
<
|
|
108
|
+
<BoardChip
|
|
178
109
|
label={i18n.t("board.fromDate", { date: task.startDate.slice(0, 10) })}
|
|
179
|
-
|
|
110
|
+
action={i18n.t("board.clearStart")}
|
|
111
|
+
onClick={() => write({ startDate: null })}
|
|
180
112
|
/>
|
|
181
113
|
)}
|
|
182
114
|
{task.dueDate === null ? null : (
|
|
183
|
-
<
|
|
115
|
+
<BoardChip
|
|
184
116
|
label={i18n.t("board.untilDate", { date: task.dueDate.slice(0, 10) })}
|
|
185
|
-
|
|
117
|
+
action={i18n.t("board.clearDue")}
|
|
118
|
+
onClick={() => write({ dueDate: null })}
|
|
186
119
|
/>
|
|
187
120
|
)}
|
|
188
121
|
|
|
189
122
|
{task.labels.map((entry) => (
|
|
190
|
-
<
|
|
123
|
+
<BoardChip
|
|
191
124
|
key={entry}
|
|
192
|
-
|
|
193
|
-
|
|
125
|
+
label={entry}
|
|
126
|
+
action={i18n.t("board.removeLabel")}
|
|
194
127
|
onClick={() => write({ labels: task.labels.filter((keep) => keep !== entry) })}
|
|
195
|
-
|
|
196
|
-
>
|
|
197
|
-
{entry}
|
|
198
|
-
</button>
|
|
128
|
+
/>
|
|
199
129
|
))}
|
|
200
130
|
|
|
201
131
|
{/* ⚠️ **Last, always.** It is the only round element in the row; standing between the chips
|
|
@@ -245,19 +175,6 @@ export function BoardTaskDocument({
|
|
|
245
175
|
* card they mean, not before.
|
|
246
176
|
*/
|
|
247
177
|
|
|
248
|
-
/** A date, and the way to take it off again. */
|
|
249
|
-
function DateChip({ label, remove }: { label: string; remove(): void }) {
|
|
250
|
-
return (
|
|
251
|
-
<button
|
|
252
|
-
type="button"
|
|
253
|
-
onClick={remove}
|
|
254
|
-
className="rounded-full border px-3 py-1 text-xs tabular-nums outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
255
|
-
>
|
|
256
|
-
{label}
|
|
257
|
-
</button>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
178
|
/**
|
|
262
179
|
* What the card can still be given. The order matches the chips it produces.
|
|
263
180
|
*
|
|
@@ -332,7 +249,7 @@ function Adder({
|
|
|
332
249
|
<DropdownMenu>
|
|
333
250
|
<DropdownMenuTrigger
|
|
334
251
|
aria-label={i18n.t("board.add")}
|
|
335
|
-
className="rounded-full border px-2 py-
|
|
252
|
+
className="rounded-full border px-2 py-0.5 text-xs outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
336
253
|
>
|
|
337
254
|
<Plus aria-hidden="true" className="size-3" />
|
|
338
255
|
</DropdownMenuTrigger>
|
|
@@ -364,7 +281,7 @@ function Adder({
|
|
|
364
281
|
event.preventDefault();
|
|
365
282
|
commit();
|
|
366
283
|
}}
|
|
367
|
-
className="w-36 rounded-full border bg-background px-
|
|
284
|
+
className="w-36 rounded-full border bg-background px-2.5 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
368
285
|
/>
|
|
369
286
|
)}
|
|
370
287
|
|
|
@@ -372,7 +289,7 @@ function Adder({
|
|
|
372
289
|
<DropdownMenu open onOpenChange={(next) => !next && close()}>
|
|
373
290
|
<DropdownMenuTrigger
|
|
374
291
|
aria-label={i18n.t("board.link.blocker")}
|
|
375
|
-
className="rounded-full border px-
|
|
292
|
+
className="rounded-full border px-2.5 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
376
293
|
>
|
|
377
294
|
{i18n.t("board.link.blocker")}
|
|
378
295
|
</DropdownMenuTrigger>
|
|
@@ -1,9 +1,66 @@
|
|
|
1
|
-
import FileViewer, {
|
|
2
|
-
|
|
1
|
+
import FileViewer, {
|
|
2
|
+
type FileViewerHandle,
|
|
3
|
+
type ViewerOptions,
|
|
4
|
+
type ViewerState,
|
|
5
|
+
} from "@file-viewer/react";
|
|
6
|
+
import {
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
Minus,
|
|
10
|
+
PanelLeft,
|
|
11
|
+
Plus,
|
|
12
|
+
RotateCcw,
|
|
13
|
+
RotateCw,
|
|
14
|
+
Search,
|
|
15
|
+
X,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
19
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
3
20
|
import { useResolvedTheme } from "@/theme/theme-context.tsx";
|
|
4
21
|
|
|
5
|
-
|
|
22
|
+
// The PDF renderer exposes no stable Shadow Parts for its navigation internals. These selectors
|
|
23
|
+
// intentionally target the pinned @file-viewer/renderer-pdf 2.3.0 DOM; verify them before upgrades.
|
|
24
|
+
const viewerTheme = `
|
|
25
|
+
:host,.file-viewer-web-shell{
|
|
26
|
+
--file-viewer-render-surface-background:var(--muted);
|
|
27
|
+
--file-viewer-bg:var(--background);--file-viewer-content-bg:var(--muted);
|
|
28
|
+
--file-viewer-text:var(--foreground);--file-viewer-muted:var(--muted-foreground);
|
|
29
|
+
--file-viewer-border:var(--border);--file-viewer-toolbar-bg:var(--background);
|
|
30
|
+
--file-viewer-toolbar-border:var(--border);--file-viewer-group-bg:var(--muted);
|
|
31
|
+
--file-viewer-group-border:var(--border);--file-viewer-button-color:var(--foreground);
|
|
32
|
+
--file-viewer-button-hover-bg:var(--accent);--file-viewer-button-hover-color:var(--accent-foreground);
|
|
33
|
+
--file-viewer-button-disabled-color:var(--muted-foreground);--file-viewer-input-bg:var(--background);
|
|
34
|
+
--file-viewer-input-color:var(--foreground);--file-viewer-focus-ring:var(--ring);
|
|
35
|
+
}
|
|
36
|
+
.pdf-shell,.pdf-wrapper{background:var(--muted)!important;color:var(--foreground)!important}
|
|
37
|
+
.pdf-nav-pane,.pdf-nav-tabs,.pdf-nav-head{background:var(--background)!important;border-color:var(--border)!important}
|
|
38
|
+
.pdf-nav-head{display:none!important}.pdf-nav-tabs{display:flex!important;justify-content:flex-end;padding:6px!important}
|
|
39
|
+
.pdf-nav-tabs button{width:30px;min-width:30px;padding:0;font-size:0;color:var(--muted-foreground)!important}
|
|
40
|
+
.pdf-nav-tabs button svg{width:15px;height:15px;margin:auto;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
41
|
+
.pdf-nav-tabs button:hover,.pdf-nav-tabs button.active{background:var(--accent)!important;border-color:var(--border)!important;color:var(--accent-foreground)!important}
|
|
42
|
+
.pdf-page-list{gap:4px!important;padding:6px!important}.pdf-page-button{display:flex!important;min-height:36px!important;padding:4px!important;border-color:transparent!important;background:transparent!important;color:var(--foreground)!important}
|
|
43
|
+
.pdf-page-button:hover,.pdf-page-button--active{background:var(--accent)!important;border-color:var(--border)!important;box-shadow:none!important}
|
|
44
|
+
.pdf-page-thumb{width:28px!important;height:28px!important;border:0!important;background:transparent!important;color:var(--foreground)!important}
|
|
45
|
+
.pdf-page-label{display:none!important}.pdf-outline-button{color:var(--foreground)!important}.pdf-outline-button:hover{background:var(--accent)!important;border-color:var(--border)!important}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export function FilePreviewView({
|
|
49
|
+
file,
|
|
50
|
+
renderer,
|
|
51
|
+
pdf = false,
|
|
52
|
+
}: {
|
|
53
|
+
file: File;
|
|
54
|
+
renderer: unknown;
|
|
55
|
+
pdf?: boolean;
|
|
56
|
+
}) {
|
|
6
57
|
const theme = useResolvedTheme();
|
|
58
|
+
const i18n = useI18n();
|
|
59
|
+
const viewer = useRef<FileViewerHandle>(null);
|
|
60
|
+
const searchInput = useRef<HTMLInputElement>(null);
|
|
61
|
+
const [state, setState] = useState<ViewerState | null>(null);
|
|
62
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
63
|
+
const [query, setQuery] = useState("");
|
|
7
64
|
const options = useMemo<ViewerOptions>(
|
|
8
65
|
() => ({
|
|
9
66
|
renderers: [renderer] as unknown as NonNullable<ViewerOptions["renderers"]>,
|
|
@@ -13,16 +70,273 @@ export function FilePreviewView({ file, renderer }: { file: File; renderer: unkn
|
|
|
13
70
|
styleIsolation: "shadow",
|
|
14
71
|
theme,
|
|
15
72
|
locale: "auto",
|
|
16
|
-
toolbar:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
theme: false,
|
|
21
|
-
permissions: { download: false, print: false, exportHtml: false },
|
|
22
|
-
},
|
|
73
|
+
toolbar: false,
|
|
74
|
+
...(pdf
|
|
75
|
+
? { pdf: { toolbar: false, navigation: true, defaultNavigationVisible: false } }
|
|
76
|
+
: {}),
|
|
23
77
|
ui: { density: "compact", surfaceBackground: "transparent" },
|
|
24
78
|
}),
|
|
25
|
-
[renderer, theme],
|
|
79
|
+
[pdf, renderer, theme],
|
|
80
|
+
);
|
|
81
|
+
const viewState = state?.viewState;
|
|
82
|
+
const page = viewState?.page ?? 1;
|
|
83
|
+
const pageCount = viewState?.pageCount ?? 0;
|
|
84
|
+
const scale = state?.zoom?.scale ?? viewState?.scale ?? 1;
|
|
85
|
+
const search = state?.search;
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!state?.ready) return;
|
|
89
|
+
const root = viewer.current?.getController()?.container.shadowRoot;
|
|
90
|
+
if (!root || root.querySelector("style[data-intel-viewer-theme]")) return;
|
|
91
|
+
const style = document.createElement("style");
|
|
92
|
+
style.dataset.intelViewerTheme = "true";
|
|
93
|
+
style.textContent = viewerTheme;
|
|
94
|
+
root.append(style);
|
|
95
|
+
const labelPages = () => {
|
|
96
|
+
for (const button of root.querySelectorAll<HTMLButtonElement>(".pdf-page-button")) {
|
|
97
|
+
const label = button.querySelector(".pdf-page-label")?.textContent?.trim();
|
|
98
|
+
if (label) button.title = label;
|
|
99
|
+
}
|
|
100
|
+
for (const [index, button] of root
|
|
101
|
+
.querySelectorAll<HTMLButtonElement>(".pdf-nav-tabs button")
|
|
102
|
+
.entries()) {
|
|
103
|
+
if (button.querySelector("[data-intel-navigation-icon]")) continue;
|
|
104
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
105
|
+
svg.dataset.intelNavigationIcon = "true";
|
|
106
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
107
|
+
svg.setAttribute("aria-hidden", "true");
|
|
108
|
+
const paths =
|
|
109
|
+
index === 0
|
|
110
|
+
? ["M8 6h13M8 12h13M8 18h13", "M3 6h.01M3 12h.01M3 18h.01"]
|
|
111
|
+
: ["M4 6h7M9 12h11M13 18h7", "M4 6v12h9M9 6v6M13 12v6"];
|
|
112
|
+
for (const pathData of paths) {
|
|
113
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
114
|
+
path.setAttribute("d", pathData);
|
|
115
|
+
svg.append(path);
|
|
116
|
+
}
|
|
117
|
+
button.prepend(svg);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
labelPages();
|
|
121
|
+
const observer = new MutationObserver(labelPages);
|
|
122
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
123
|
+
return () => observer.disconnect();
|
|
124
|
+
}, [state?.ready]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (searchOpen) searchInput.current?.focus();
|
|
128
|
+
}, [searchOpen]);
|
|
129
|
+
|
|
130
|
+
const applyViewState = (next: Record<string, unknown>) =>
|
|
131
|
+
viewer.current?.applyViewState({ ...viewState, ...next });
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="relative h-full min-h-64">
|
|
135
|
+
{pdf ? (
|
|
136
|
+
<ActionSlot name="title-actions">
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
aria-label={i18n.t("attachment.viewer.navigation")}
|
|
140
|
+
aria-pressed={viewState?.navigation?.visible ?? false}
|
|
141
|
+
onClick={() =>
|
|
142
|
+
void applyViewState({
|
|
143
|
+
navigation: {
|
|
144
|
+
...viewState?.navigation,
|
|
145
|
+
visible: !(viewState?.navigation?.visible ?? false),
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
className="inline-flex size-8 items-center justify-center rounded-md bg-transparent outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-muted"
|
|
150
|
+
>
|
|
151
|
+
<PanelLeft aria-hidden="true" className="size-4" />
|
|
152
|
+
</button>
|
|
153
|
+
</ActionSlot>
|
|
154
|
+
) : null}
|
|
155
|
+
<div className="absolute inset-x-0 top-0 bottom-14 min-h-0">
|
|
156
|
+
<FileViewer
|
|
157
|
+
ref={viewer}
|
|
158
|
+
file={file}
|
|
159
|
+
className="h-full min-h-0"
|
|
160
|
+
options={options}
|
|
161
|
+
onStateChange={setState}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
{state?.ready ? (
|
|
165
|
+
<div
|
|
166
|
+
role="toolbar"
|
|
167
|
+
aria-label={i18n.t("attachment.viewer.controls")}
|
|
168
|
+
className="absolute inset-x-3 bottom-2 z-[5] mx-auto flex min-h-10 w-fit max-w-[calc(100%-1.5rem)] items-center gap-1 rounded-full border bg-background/95 p-1 shadow-lg backdrop-blur"
|
|
169
|
+
>
|
|
170
|
+
<span role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
|
171
|
+
{search?.query
|
|
172
|
+
? search.total > 0
|
|
173
|
+
? i18n.t("attachment.viewer.searchResults", {
|
|
174
|
+
current: search.currentIndex + 1,
|
|
175
|
+
total: search.total,
|
|
176
|
+
})
|
|
177
|
+
: i18n.t("attachment.viewer.searchNoResults")
|
|
178
|
+
: ""}
|
|
179
|
+
</span>
|
|
180
|
+
{pdf && pageCount > 0 && !searchOpen ? (
|
|
181
|
+
<div className="flex items-center rounded-full bg-muted p-0.5">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
aria-label={i18n.t("attachment.viewer.previousPage")}
|
|
185
|
+
disabled={page <= 1}
|
|
186
|
+
onClick={() => void applyViewState({ page: page - 1 })}
|
|
187
|
+
className="size-7 rounded-full hover:bg-accent disabled:opacity-40"
|
|
188
|
+
>
|
|
189
|
+
<ChevronLeft aria-hidden="true" className="mx-auto size-3.5" />
|
|
190
|
+
</button>
|
|
191
|
+
<span className="min-w-14 px-1 text-center text-xs tabular-nums">
|
|
192
|
+
{page} / {pageCount}
|
|
193
|
+
</span>
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
aria-label={i18n.t("attachment.viewer.nextPage")}
|
|
197
|
+
disabled={page >= pageCount}
|
|
198
|
+
onClick={() => void applyViewState({ page: page + 1 })}
|
|
199
|
+
className="size-7 rounded-full hover:bg-accent disabled:opacity-40"
|
|
200
|
+
>
|
|
201
|
+
<ChevronRight aria-hidden="true" className="mx-auto size-3.5" />
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
) : null}
|
|
205
|
+
{searchOpen ? (
|
|
206
|
+
<form
|
|
207
|
+
className="flex min-w-0 items-center gap-1"
|
|
208
|
+
onSubmit={(event) => {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
void viewer.current?.searchDocument(query);
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<input
|
|
214
|
+
ref={searchInput}
|
|
215
|
+
value={query}
|
|
216
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
217
|
+
placeholder={i18n.t("attachment.viewer.searchPlaceholder")}
|
|
218
|
+
className="h-7 w-28 min-w-0 rounded-full border bg-background px-3 text-xs outline-none focus:ring-2 focus:ring-ring sm:w-40"
|
|
219
|
+
/>
|
|
220
|
+
<button
|
|
221
|
+
type="submit"
|
|
222
|
+
aria-label={i18n.t("attachment.viewer.search")}
|
|
223
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent"
|
|
224
|
+
>
|
|
225
|
+
<Search aria-hidden="true" className="size-3.5" />
|
|
226
|
+
</button>
|
|
227
|
+
{search && search.total > 0 ? (
|
|
228
|
+
<div className="flex items-center rounded-full bg-muted p-0.5">
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
aria-label={i18n.t("attachment.viewer.previousResult")}
|
|
232
|
+
disabled={search.total <= 1}
|
|
233
|
+
onClick={() => void viewer.current?.previousSearchResult()}
|
|
234
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent disabled:opacity-40"
|
|
235
|
+
>
|
|
236
|
+
<ChevronLeft aria-hidden="true" className="size-3.5" />
|
|
237
|
+
</button>
|
|
238
|
+
<span
|
|
239
|
+
aria-hidden="true"
|
|
240
|
+
className="min-w-10 px-1 text-center text-xs tabular-nums"
|
|
241
|
+
>
|
|
242
|
+
{search.currentIndex + 1} / {search.total}
|
|
243
|
+
</span>
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
aria-label={i18n.t("attachment.viewer.nextResult")}
|
|
247
|
+
disabled={search.total <= 1}
|
|
248
|
+
onClick={() => void viewer.current?.nextSearchResult()}
|
|
249
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent disabled:opacity-40"
|
|
250
|
+
>
|
|
251
|
+
<ChevronRight aria-hidden="true" className="size-3.5" />
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
) : null}
|
|
255
|
+
{search?.query && search.total === 0 ? (
|
|
256
|
+
<span aria-hidden="true" className="min-w-10 px-1 text-center text-xs tabular-nums">
|
|
257
|
+
{i18n.t("attachment.viewer.searchNoResults")}
|
|
258
|
+
</span>
|
|
259
|
+
) : null}
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
aria-label={i18n.t("common.close")}
|
|
263
|
+
onClick={() => {
|
|
264
|
+
setSearchOpen(false);
|
|
265
|
+
setQuery("");
|
|
266
|
+
void viewer.current?.clearDocumentSearch();
|
|
267
|
+
}}
|
|
268
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent"
|
|
269
|
+
>
|
|
270
|
+
<X aria-hidden="true" className="size-3.5" />
|
|
271
|
+
</button>
|
|
272
|
+
</form>
|
|
273
|
+
) : (
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
aria-label={i18n.t("attachment.viewer.search")}
|
|
277
|
+
onClick={() => setSearchOpen(true)}
|
|
278
|
+
className="grid size-8 place-items-center rounded-full hover:bg-accent"
|
|
279
|
+
>
|
|
280
|
+
<Search aria-hidden="true" className="size-4" />
|
|
281
|
+
</button>
|
|
282
|
+
)}
|
|
283
|
+
{!searchOpen ? (
|
|
284
|
+
<div className="flex items-center rounded-full bg-muted p-0.5">
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
aria-label={i18n.t("attachment.viewer.zoomOut")}
|
|
288
|
+
disabled={state.zoom ? !state.zoom.canZoomOut : false}
|
|
289
|
+
onClick={() => void viewer.current?.zoomOut()}
|
|
290
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent disabled:opacity-40"
|
|
291
|
+
>
|
|
292
|
+
<Minus aria-hidden="true" className="size-3.5" />
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
aria-label={i18n.t("attachment.viewer.resetZoom")}
|
|
297
|
+
disabled={state.zoom ? !state.zoom.canReset : false}
|
|
298
|
+
onClick={() => void viewer.current?.resetZoom()}
|
|
299
|
+
className="min-w-12 px-1 text-xs tabular-nums disabled:opacity-40"
|
|
300
|
+
>
|
|
301
|
+
{Math.round(scale * 100)}%
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
aria-label={i18n.t("attachment.viewer.zoomIn")}
|
|
306
|
+
disabled={state.zoom ? !state.zoom.canZoomIn : false}
|
|
307
|
+
onClick={() => void viewer.current?.zoomIn()}
|
|
308
|
+
className="grid size-7 place-items-center rounded-full hover:bg-accent disabled:opacity-40"
|
|
309
|
+
>
|
|
310
|
+
<Plus aria-hidden="true" className="size-3.5" />
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
) : null}
|
|
314
|
+
{pdf && !searchOpen ? (
|
|
315
|
+
<div className="hidden items-center sm:flex">
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
aria-label={i18n.t("attachment.viewer.rotateLeft")}
|
|
319
|
+
onClick={() =>
|
|
320
|
+
void applyViewState({ rotation: ((viewState?.rotation ?? 0) - 90 + 360) % 360 })
|
|
321
|
+
}
|
|
322
|
+
className="grid size-8 place-items-center rounded-full hover:bg-accent"
|
|
323
|
+
>
|
|
324
|
+
<RotateCcw aria-hidden="true" className="size-4" />
|
|
325
|
+
</button>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
aria-label={i18n.t("attachment.viewer.rotateRight")}
|
|
329
|
+
onClick={() =>
|
|
330
|
+
void applyViewState({ rotation: ((viewState?.rotation ?? 0) + 90) % 360 })
|
|
331
|
+
}
|
|
332
|
+
className="grid size-8 place-items-center rounded-full hover:bg-accent"
|
|
333
|
+
>
|
|
334
|
+
<RotateCw aria-hidden="true" className="size-4" />
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
) : null}
|
|
338
|
+
</div>
|
|
339
|
+
) : null}
|
|
340
|
+
</div>
|
|
26
341
|
);
|
|
27
|
-
return <FileViewer file={file} className="h-full min-h-64" options={options} />;
|
|
28
342
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pdfRenderer } from "@file-viewer/renderer-pdf";
|
|
2
2
|
import { FilePreviewView } from "./file-preview-view.tsx";
|
|
3
3
|
export function PdfFilePreview({ file }: { file: File }) {
|
|
4
|
-
return <FilePreviewView file={file} renderer={pdfRenderer} />;
|
|
4
|
+
return <FilePreviewView file={file} renderer={pdfRenderer} pdf />;
|
|
5
5
|
}
|
package/src/i18n/de.json
CHANGED
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"board.backTo": "Zurück zu {board}",
|
|
43
43
|
"board.cardLabel": "{title} öffnen, in {column}. Alt und eine Pfeiltaste verschiebt sie.",
|
|
44
44
|
"board.clearAssignee": "Zuständigkeit von {name} entfernen",
|
|
45
|
+
"board.clearDependsOn": "nicht mehr darauf warten",
|
|
46
|
+
"board.clearDue": "entfernen",
|
|
47
|
+
"board.clearStart": "entfernen",
|
|
45
48
|
"board.column.archive": "Archiv",
|
|
46
49
|
"board.column.assignee": "Zuständig",
|
|
47
50
|
"board.column.default.backlog": "Backlog",
|
|
@@ -51,6 +54,7 @@
|
|
|
51
54
|
"board.column.dependsOn": "Wartet auf",
|
|
52
55
|
"board.column.due": "Fällig",
|
|
53
56
|
"board.column.labels": "Labels",
|
|
57
|
+
"board.column.start": "Start",
|
|
54
58
|
"board.column.status": "Spalte",
|
|
55
59
|
"board.column.title": "Titel",
|
|
56
60
|
"board.createFailed": "Die Aufgabe konnte nicht angelegt werden.",
|
|
@@ -72,9 +76,9 @@
|
|
|
72
76
|
"board.link.outside": "Außerhalb dieses Boards",
|
|
73
77
|
"board.link.subtask": "Unteraufgabe",
|
|
74
78
|
"board.links": "Verlinkt",
|
|
75
|
-
"board.moveTo": "
|
|
79
|
+
"board.moveTo": "in eine andere Spalte verschieben",
|
|
76
80
|
"board.progress": "{done} von {total} erledigt",
|
|
77
|
-
"board.removeLabel": "Label
|
|
81
|
+
"board.removeLabel": "dieses Label entfernen",
|
|
78
82
|
"board.settings.addColumn": "+ Spalte",
|
|
79
83
|
"board.settings.columnName": "Name der Spalte {column}",
|
|
80
84
|
"board.settings.description": "Spalten und was das Board zeigt.",
|
|
@@ -89,6 +93,10 @@
|
|
|
89
93
|
"board.settings.showArchive": "Archiv-Spalte zeigen",
|
|
90
94
|
"board.settings.showArchiveHint": "Sie ist auf jedem Board. Ausgeblendet sind ihre Karten überall aus dem Blick.",
|
|
91
95
|
"board.settings.title": "Board-Einstellungen",
|
|
96
|
+
"board.sort.asc": "{column}, A bis Z",
|
|
97
|
+
"board.sort.desc": "{column}, Z bis A",
|
|
98
|
+
"board.sort.none": "Unsortiert",
|
|
99
|
+
"board.sortBy": "Sortieren",
|
|
92
100
|
"board.stripes.root": "Ebene {level}, hat Unteraufgaben",
|
|
93
101
|
"board.stripes.under": "Ebene {level} · übergeordnet in {column}",
|
|
94
102
|
"board.table.collapse": "Verbergen, was unter {row} liegt",
|
|
@@ -249,6 +257,21 @@
|
|
|
249
257
|
"attachment.empty": "Es wurde noch keine Datei hochgeladen.",
|
|
250
258
|
"attachment.loadFailed": "Die Datei konnte nicht geladen werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
251
259
|
"attachment.unsupported": "Dieser Dateityp kann nicht angezeigt werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
260
|
+
"attachment.viewer.controls": "Dateisteuerung",
|
|
261
|
+
"attachment.viewer.navigation": "Seitennavigation anzeigen",
|
|
262
|
+
"attachment.viewer.nextPage": "Nächste Seite",
|
|
263
|
+
"attachment.viewer.nextResult": "Nächster Suchtreffer",
|
|
264
|
+
"attachment.viewer.previousPage": "Vorherige Seite",
|
|
265
|
+
"attachment.viewer.previousResult": "Vorheriger Suchtreffer",
|
|
266
|
+
"attachment.viewer.resetZoom": "Zoom zurücksetzen",
|
|
267
|
+
"attachment.viewer.rotateLeft": "Nach links drehen",
|
|
268
|
+
"attachment.viewer.rotateRight": "Nach rechts drehen",
|
|
269
|
+
"attachment.viewer.search": "Dokument durchsuchen",
|
|
270
|
+
"attachment.viewer.searchNoResults": "Keine Treffer",
|
|
271
|
+
"attachment.viewer.searchPlaceholder": "Dokument durchsuchen",
|
|
272
|
+
"attachment.viewer.searchResults": "Suchtreffer {current} von {total}",
|
|
273
|
+
"attachment.viewer.zoomIn": "Vergrößern",
|
|
274
|
+
"attachment.viewer.zoomOut": "Verkleinern",
|
|
252
275
|
"nav.primary": "Hauptnavigation",
|
|
253
276
|
"nav.tools": "Werkzeuge",
|
|
254
277
|
"node.access": "Zugriff",
|