Compare commits
3 Commits
8bf8e22448
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 111041672e | |||
| 36d8dae1fc | |||
| 92d7df2a38 |
+239
-130
@@ -4594,6 +4594,87 @@ function SidePanelDock({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CardLibraryTreeKind = "subject" | "level" | "series" | "card" | "task";
|
||||||
|
|
||||||
|
function CardLibraryTreeRow({
|
||||||
|
id,
|
||||||
|
depth,
|
||||||
|
kind,
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
refValue,
|
||||||
|
stateValue,
|
||||||
|
stateKind,
|
||||||
|
hasChildren = false,
|
||||||
|
expanded = false,
|
||||||
|
onToggle,
|
||||||
|
href,
|
||||||
|
current = false,
|
||||||
|
unavailable = false,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
depth: number;
|
||||||
|
kind: CardLibraryTreeKind;
|
||||||
|
title: string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
refValue: React.ReactNode;
|
||||||
|
stateValue: React.ReactNode;
|
||||||
|
stateKind?: string;
|
||||||
|
hasChildren?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
|
href?: string;
|
||||||
|
current?: boolean;
|
||||||
|
unavailable?: boolean;
|
||||||
|
onNavigate?: () => void;
|
||||||
|
}) {
|
||||||
|
const nodeLabel = href ? (
|
||||||
|
<a
|
||||||
|
className="card-library-tree-link"
|
||||||
|
href={href}
|
||||||
|
onClick={onNavigate}
|
||||||
|
aria-current={current ? "page" : undefined}
|
||||||
|
title={current ? "Pokaż bieżącą kartę" : `Otwórz: ${title}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="card-library-tree-label" title={title}>{label}</span>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`card-library-treegrid-row is-${kind}${current ? " is-current" : ""}${unavailable ? " is-unavailable" : ""}`}
|
||||||
|
role="row"
|
||||||
|
aria-level={depth + 1}
|
||||||
|
aria-expanded={hasChildren ? expanded : undefined}
|
||||||
|
aria-disabled={unavailable || undefined}
|
||||||
|
data-node-id={id}
|
||||||
|
style={{ "--card-library-depth": depth } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<span className="card-library-tree-title" role="gridcell">
|
||||||
|
{hasChildren ? (
|
||||||
|
<button
|
||||||
|
className="card-library-tree-expander"
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
aria-label={`${expanded ? "Zwiń" : "Rozwiń"}: ${title}`}
|
||||||
|
>
|
||||||
|
{expanded ? "▾" : "▸"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="card-library-tree-expander is-empty" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
{nodeLabel}
|
||||||
|
</span>
|
||||||
|
<span role="gridcell">{refValue}</span>
|
||||||
|
<span role="gridcell">
|
||||||
|
<b className="card-library-tree-state" data-state={stateKind}>{stateValue}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CardLibraryPanel({
|
function CardLibraryPanel({
|
||||||
library,
|
library,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
@@ -4626,141 +4707,169 @@ function CardLibraryPanel({
|
|||||||
),
|
),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => {
|
||||||
|
const initial = new Set<string>();
|
||||||
|
library.subjects.forEach(subject => {
|
||||||
|
const subjectKey = `subject:${subject.id}`;
|
||||||
|
if (subject.current) initial.add(subjectKey);
|
||||||
|
subject.levels.forEach(level => {
|
||||||
|
const levelKey = `${subjectKey}:level:${level.id}`;
|
||||||
|
if (level.current) initial.add(levelKey);
|
||||||
|
level.series.forEach(series => {
|
||||||
|
const seriesKey = `${levelKey}:series:${series.id}`;
|
||||||
|
if (series.current) initial.add(seriesKey);
|
||||||
|
series.cards.forEach(card => {
|
||||||
|
if (card.current && (card.tasks?.length ?? 0) > 0) {
|
||||||
|
initial.add(`${seriesKey}:card:${card.id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
const isExpanded = (id: string) => expandedNodes.has(id);
|
||||||
|
const toggle = (id: string) => {
|
||||||
|
setExpandedNodes(current => {
|
||||||
|
const next = new Set(current);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="card-library-panel">
|
<div className="card-library-panel">
|
||||||
<header className="card-library-header">
|
<div
|
||||||
<div>
|
className="card-library-treegrid"
|
||||||
<small>{library.workspace || "STEM"} · KATALOG</small>
|
role="treegrid"
|
||||||
<h2 id="card-library-title">Serie i karty</h2>
|
aria-label="Serie, karty i taski"
|
||||||
<p>{seriesCount} serii · {availableCount}/{cardCount} kart online</p>
|
aria-colcount={3}
|
||||||
|
>
|
||||||
|
<div className="card-library-treegrid-row card-library-treegrid-head" role="row">
|
||||||
|
<span role="columnheader">DRZEWO</span>
|
||||||
|
<span role="columnheader">REF</span>
|
||||||
|
<span role="columnheader">STATUS</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
{library.subjects.map(subject => {
|
||||||
<nav className="card-library-tree" aria-label="Drzewo serii i kart">
|
const subjectKey = `subject:${subject.id}`;
|
||||||
{library.subjects.map(subject => (
|
return (
|
||||||
<details
|
<React.Fragment key={subjectKey}>
|
||||||
className="card-library-subject"
|
<CardLibraryTreeRow
|
||||||
key={subject.id}
|
id={subjectKey}
|
||||||
open={subject.current || undefined}
|
depth={0}
|
||||||
>
|
kind="subject"
|
||||||
<summary>
|
title={subject.title}
|
||||||
<span className="card-library-branch" aria-hidden="true">▾</span>
|
label={<strong>{subject.title}</strong>}
|
||||||
<span>
|
refValue="PRZEDMIOT"
|
||||||
<strong>{subject.title}</strong>
|
stateValue={`${subject.levels.length} POZ.`}
|
||||||
<small>PRZEDMIOT</small>
|
hasChildren={subject.levels.length > 0}
|
||||||
</span>
|
expanded={isExpanded(subjectKey)}
|
||||||
<output>{subject.levels.length}</output>
|
onToggle={() => toggle(subjectKey)}
|
||||||
</summary>
|
/>
|
||||||
<div className="card-library-level-list">
|
{isExpanded(subjectKey) && subject.levels.map(level => {
|
||||||
{subject.levels.map(level => (
|
const levelKey = `${subjectKey}:level:${level.id}`;
|
||||||
<details
|
return (
|
||||||
className="card-library-level"
|
<React.Fragment key={levelKey}>
|
||||||
key={level.id}
|
<CardLibraryTreeRow
|
||||||
open={level.current || undefined}
|
id={levelKey}
|
||||||
>
|
depth={1}
|
||||||
<summary>
|
kind="level"
|
||||||
<span className="card-library-branch" aria-hidden="true">▾</span>
|
title={level.title}
|
||||||
<strong>{level.title}</strong>
|
label={<strong>{level.title}</strong>}
|
||||||
<output>{level.series.length}</output>
|
refValue="ROK"
|
||||||
</summary>
|
stateValue={`${level.series.length} SERII`}
|
||||||
<div className="card-library-series-list">
|
hasChildren={level.series.length > 0}
|
||||||
{level.series.map(series => (
|
expanded={isExpanded(levelKey)}
|
||||||
<details
|
onToggle={() => toggle(levelKey)}
|
||||||
className="card-library-series"
|
/>
|
||||||
key={`${level.id}:${series.id}`}
|
{isExpanded(levelKey) && level.series.map(series => {
|
||||||
open={series.current || undefined}
|
const seriesKey = `${levelKey}:series:${series.id}`;
|
||||||
>
|
return (
|
||||||
<summary>
|
<React.Fragment key={seriesKey}>
|
||||||
<span className="card-library-branch" aria-hidden="true">▾</span>
|
<CardLibraryTreeRow
|
||||||
<span>
|
id={seriesKey}
|
||||||
<strong>{series.title}</strong>
|
depth={2}
|
||||||
{series.role === "reference" && <small>REFERENCJA</small>}
|
kind="series"
|
||||||
</span>
|
title={series.title}
|
||||||
<output>{series.cards.length}</output>
|
label={<strong>{series.title}</strong>}
|
||||||
</summary>
|
refValue={series.role === "reference" ? "REF." : "SERIA"}
|
||||||
{series.cards.length > 0 && (
|
stateValue={`${series.cards.length} KART`}
|
||||||
<ul className="card-library-cards">
|
hasChildren={series.cards.length > 0}
|
||||||
{series.cards.map((card, cardIndex) => {
|
expanded={isExpanded(seriesKey)}
|
||||||
const tasks = card.tasks ?? [];
|
onToggle={() => toggle(seriesKey)}
|
||||||
const body = (
|
/>
|
||||||
<>
|
{isExpanded(seriesKey) && series.cards.map(card => {
|
||||||
<span className="card-library-node" aria-hidden="true">
|
const cardKey = `${seriesKey}:card:${card.id}`;
|
||||||
{cardIndex === series.cards.length - 1 ? "└─" : "├─"}
|
const tasks = card.tasks ?? [];
|
||||||
</span>
|
const available = card.current || card.available;
|
||||||
<span className="card-library-card-copy">
|
const state = card.current ? "current" : card.available ? "online" : "plan";
|
||||||
<strong>{card.id}</strong>
|
return (
|
||||||
<span>{card.title}</span>
|
<React.Fragment key={cardKey}>
|
||||||
</span>
|
<CardLibraryTreeRow
|
||||||
<span className="card-library-card-meta">
|
id={cardKey}
|
||||||
{card.version && <small>{card.version}</small>}
|
depth={3}
|
||||||
<small>{card.current ? "BIEŻĄCA" : card.available ? "ONLINE" : "PLAN"}</small>
|
kind="card"
|
||||||
</span>
|
title={`${card.id} · ${card.title}`}
|
||||||
</>
|
label={(
|
||||||
);
|
<>
|
||||||
return (
|
<strong>{card.id}</strong>
|
||||||
<li className="card-library-card-entry" key={`${series.id}:${card.id}`}>
|
<span>{card.title}</span>
|
||||||
{card.current || card.available ? (
|
</>
|
||||||
<a
|
|
||||||
className={`card-library-card is-available${card.current ? " is-current" : ""}`}
|
|
||||||
href={card.current ? "#" : card.href}
|
|
||||||
onClick={onNavigate}
|
|
||||||
aria-current={card.current ? "page" : undefined}
|
|
||||||
title={card.current ? "Pokaż bieżącą kartę" : `Otwórz kartę ${card.id}`}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="card-library-card is-unavailable"
|
|
||||||
aria-disabled="true"
|
|
||||||
title="Karta nie ma jeszcze wygenerowanego adresu"
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{tasks.length > 0 && (
|
refValue={card.version || "—"}
|
||||||
<ul className="card-library-tasks">
|
stateValue={card.current ? "BIEŻĄCA" : card.available ? "ONLINE" : "PLAN"}
|
||||||
{tasks.map((task, taskIndex) => {
|
stateKind={state}
|
||||||
const taskBody = (
|
hasChildren={tasks.length > 0}
|
||||||
<>
|
expanded={isExpanded(cardKey)}
|
||||||
<span className="card-library-node" aria-hidden="true">
|
onToggle={() => toggle(cardKey)}
|
||||||
{taskIndex === tasks.length - 1 ? "└─" : "├─"}
|
href={available ? (card.current ? "#" : card.href) : undefined}
|
||||||
</span>
|
current={card.current}
|
||||||
<strong>{task.id.toUpperCase()}</strong>
|
unavailable={!available}
|
||||||
<span>{task.title}</span>
|
onNavigate={onNavigate}
|
||||||
</>
|
/>
|
||||||
);
|
{isExpanded(cardKey) && tasks.map(task => {
|
||||||
const taskHref = card.current
|
const taskHref = card.current
|
||||||
? `#${task.id}`
|
? `#${task.id}`
|
||||||
: `${card.href}#${task.id}`;
|
: `${card.href}#${task.id}`;
|
||||||
return (
|
return (
|
||||||
<li key={`${card.id}:${task.id}`}>
|
<CardLibraryTreeRow
|
||||||
{card.current || card.available ? (
|
key={`${cardKey}:task:${task.id}`}
|
||||||
<a href={taskHref} onClick={onNavigate}>{taskBody}</a>
|
id={`${cardKey}:task:${task.id}`}
|
||||||
) : (
|
depth={4}
|
||||||
<span>{taskBody}</span>
|
kind="task"
|
||||||
)}
|
title={`${task.id.toUpperCase()} · ${task.title}`}
|
||||||
</li>
|
label={(
|
||||||
);
|
<>
|
||||||
})}
|
<strong>{task.id.toUpperCase()}</strong>
|
||||||
</ul>
|
<span>{task.title}</span>
|
||||||
)}
|
</>
|
||||||
</li>
|
)}
|
||||||
);
|
refValue="TASK"
|
||||||
})}
|
stateValue={available ? "GOTOWY" : "PLAN"}
|
||||||
</ul>
|
stateKind={available ? "online" : "plan"}
|
||||||
)}
|
href={available ? taskHref : undefined}
|
||||||
</details>
|
unavailable={!available}
|
||||||
))}
|
onNavigate={onNavigate}
|
||||||
</div>
|
/>
|
||||||
</details>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</React.Fragment>
|
||||||
</details>
|
);
|
||||||
))}
|
})}
|
||||||
</nav>
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<footer className="card-library-footer">
|
<footer className="card-library-footer">
|
||||||
<span><i className="is-online" /> online</span>
|
<span>{library.workspace || "STEM"}</span>
|
||||||
<span><i className="is-draft" /> plan / brak strony</span>
|
<span>{seriesCount} serii</span>
|
||||||
|
<span><i className="is-online" /> {availableCount}/{cardCount} online</span>
|
||||||
<kbd>Esc</kbd>
|
<kbd>Esc</kbd>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -679,6 +679,178 @@
|
|||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
font: 600 16px/1 ui-monospace, monospace;
|
font: 600 16px/1 ui-monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shared tree-widget presentation: the library uses the same tabular grammar
|
||||||
|
as the classroom tree (tree column, fixed metadata columns, sticky head). */
|
||||||
|
.card-library-treegrid {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 8px;
|
||||||
|
border: 1px solid rgb(31 35 40 / 18%);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
scrollbar-color: #93a2a8 #edf1f2;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row {
|
||||||
|
display: grid;
|
||||||
|
min-width: 760px;
|
||||||
|
min-height: 38px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
grid-template-columns: minmax(500px, 1fr) 110px 120px;
|
||||||
|
align-items: stretch;
|
||||||
|
border-bottom: 1px solid rgb(31 35 40 / 9%);
|
||||||
|
color: #273941;
|
||||||
|
font: 500 16px/1.25 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row > [role="gridcell"],
|
||||||
|
.card-library-treegrid-row > [role="columnheader"] {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 7px 9px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row > [role="gridcell"] + [role="gridcell"],
|
||||||
|
.card-library-treegrid-row > [role="columnheader"] + [role="columnheader"] {
|
||||||
|
border-left: 1px solid rgb(31 35 40 / 9%);
|
||||||
|
}
|
||||||
|
.card-library-treegrid-head {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0;
|
||||||
|
min-height: 34px;
|
||||||
|
border-bottom-color: rgb(31 35 40 / 20%);
|
||||||
|
background: #eef1f2;
|
||||||
|
color: #3e525b;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .045em;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row:not(.card-library-treegrid-head):hover {
|
||||||
|
background: rgb(31 35 40 / 4%);
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-subject {
|
||||||
|
background: #dfe8e5;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-level {
|
||||||
|
background: #f5f8f8;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-series {
|
||||||
|
background: #f9fbfb;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-card {
|
||||||
|
min-height: 42px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-task {
|
||||||
|
background: #fbfcfc;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-current {
|
||||||
|
box-shadow: inset 3px 0 #1680a5;
|
||||||
|
background: #e1f1f6;
|
||||||
|
}
|
||||||
|
.card-library-tree-title {
|
||||||
|
gap: 6px;
|
||||||
|
padding-left: calc(8px + (var(--card-library-depth, 0) * 21px)) !important;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.card-library-tree-expander {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: none;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #405961;
|
||||||
|
padding: 0;
|
||||||
|
font: 18px/1 ui-monospace, monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.card-library-tree-expander:hover,
|
||||||
|
.card-library-tree-expander:focus-visible {
|
||||||
|
background: rgb(22 93 107 / 10%);
|
||||||
|
color: #165d6b;
|
||||||
|
outline: 1px solid rgb(22 93 107 / 28%);
|
||||||
|
}
|
||||||
|
.card-library-tree-expander.is-empty {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.card-library-tree-link,
|
||||||
|
.card-library-tree-label {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #10181c;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-library-tree-link:hover,
|
||||||
|
.card-library-tree-link:focus-visible {
|
||||||
|
color: #075d7c;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
.card-library-tree-link strong,
|
||||||
|
.card-library-tree-label strong {
|
||||||
|
flex: none;
|
||||||
|
color: inherit;
|
||||||
|
font: 700 16px/1.25 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.card-library-tree-link > span,
|
||||||
|
.card-library-tree-label > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: inherit;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 500 16px/1.25 system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-subject .card-library-tree-label strong,
|
||||||
|
.card-library-treegrid-row.is-level .card-library-tree-label strong,
|
||||||
|
.card-library-treegrid-row.is-series .card-library-tree-label strong {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.card-library-tree-state {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 76px;
|
||||||
|
min-height: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #33474f;
|
||||||
|
padding: 2px 7px;
|
||||||
|
font: 650 14px/1 ui-monospace, monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-library-tree-state[data-state="current"] {
|
||||||
|
border-color: #3184a0;
|
||||||
|
background: #e9f5f8;
|
||||||
|
color: #075d7c;
|
||||||
|
}
|
||||||
|
.card-library-tree-state[data-state="online"] {
|
||||||
|
border-color: #4b9a7a;
|
||||||
|
background: #edf8f2;
|
||||||
|
color: #176144;
|
||||||
|
}
|
||||||
|
.card-library-tree-state[data-state="plan"] {
|
||||||
|
border-color: #c59a49;
|
||||||
|
background: #fff7e5;
|
||||||
|
color: #775313;
|
||||||
|
}
|
||||||
|
.card-library-treegrid-row.is-unavailable .card-library-tree-label,
|
||||||
|
.card-library-treegrid-row.is-unavailable .card-library-tree-label strong {
|
||||||
|
color: #10181c;
|
||||||
|
}
|
||||||
.side-toc,
|
.side-toc,
|
||||||
.side-progress {
|
.side-progress {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -154,9 +154,13 @@ def check_render() -> None:
|
|||||||
raise AssertionError(f"brak adresu nagłówka {expected_url!r}")
|
raise AssertionError(f"brak adresu nagłówka {expected_url!r}")
|
||||||
if tex.count(r"\qrcode[level=L,height=22mm]") < 2:
|
if tex.count(r"\qrcode[level=L,height=22mm]") < 2:
|
||||||
raise AssertionError("TeX nie zawiera obu kodów QR nagłówka")
|
raise AssertionError("TeX nie zawiera obu kodów QR nagłówka")
|
||||||
|
if tex.count(r"}{\qrcode[level=L,height=22mm]") < 2:
|
||||||
|
raise AssertionError("oba kody QR PDF muszą być aktywnymi hiperłączami")
|
||||||
for marker in ("resource-qr-repository", "resource-qr-page"):
|
for marker in ("resource-qr-repository", "resource-qr-page"):
|
||||||
if marker not in html:
|
if marker not in html:
|
||||||
raise AssertionError(f"HTML nie zawiera komórki {marker!r}")
|
raise AssertionError(f"HTML nie zawiera komórki {marker!r}")
|
||||||
|
if html.count('target="_blank" rel="noopener noreferrer"') < 2:
|
||||||
|
raise AssertionError("oba kody QR HTML muszą otwierać bezpieczną nową kartę")
|
||||||
if "PROJECT" not in html:
|
if "PROJECT" not in html:
|
||||||
raise AssertionError("HTML nie zawiera nowej tabliczki projektu")
|
raise AssertionError("HTML nie zawiera nowej tabliczki projektu")
|
||||||
if "resource-qr-label" in html:
|
if "resource-qr-label" in html:
|
||||||
|
|||||||
+14
-4
@@ -3928,6 +3928,7 @@ def render_page_header_html(data: dict, page: int, total: int) -> str:
|
|||||||
if qr:
|
if qr:
|
||||||
return (
|
return (
|
||||||
f'<a class="{classes}" href="{html_escape(target)}" '
|
f'<a class="{classes}" href="{html_escape(target)}" '
|
||||||
|
'target="_blank" rel="noopener noreferrer" '
|
||||||
f'aria-label="{html_escape(label)}: otwórz {html_escape(target)}">'
|
f'aria-label="{html_escape(label)}: otwórz {html_escape(target)}">'
|
||||||
f'{qr}</a>'
|
f'{qr}</a>'
|
||||||
)
|
)
|
||||||
@@ -4192,11 +4193,15 @@ def render_page_header_tex_macro(data: dict) -> list[str]:
|
|||||||
f"title_block.{field_name} zawiera znaki nieobsługiwane przez nagłówek TeX"
|
f"title_block.{field_name} zawiera znaki nieobsługiwane przez nagłówek TeX"
|
||||||
)
|
)
|
||||||
page_qr = (
|
page_qr = (
|
||||||
rf"\qrcode[level=L,height=22mm]{{{url}}}"
|
rf"\href{{{url}}}{{\qrcode[level=L,height=22mm]{{{url}}}}}"
|
||||||
if fields["show_qr"]
|
if fields["show_qr"]
|
||||||
else r"{\tiny QR wyłączony}"
|
else r"{\tiny QR wyłączony}"
|
||||||
)
|
)
|
||||||
source_qr = rf"\qrcode[level=L,height=22mm]{{{source_url}}}" if dual_qr else ""
|
source_qr = (
|
||||||
|
rf"\href{{{source_url}}}{{\qrcode[level=L,height=22mm]{{{source_url}}}}}"
|
||||||
|
if dual_qr
|
||||||
|
else ""
|
||||||
|
)
|
||||||
layout = resource_header_layout(data, r"\thepage/\pageref{LastPage}")
|
layout = resource_header_layout(data, r"\thepage/\pageref{LastPage}")
|
||||||
row_units = [3, 3, 3, 2, 2]
|
row_units = [3, 3, 3, 2, 2]
|
||||||
unit_height_mm = (
|
unit_height_mm = (
|
||||||
@@ -4458,6 +4463,7 @@ def render_title_block_html(data: dict) -> str:
|
|||||||
qr = qr_svg(str(fields["url"])) if fields["show_qr"] else ""
|
qr = qr_svg(str(fields["url"])) if fields["show_qr"] else ""
|
||||||
qr_cell = (
|
qr_cell = (
|
||||||
f'<a class="title-qr-cell" href="{html_escape(str(fields["url"]))}" '
|
f'<a class="title-qr-cell" href="{html_escape(str(fields["url"]))}" '
|
||||||
|
'target="_blank" rel="noopener noreferrer" '
|
||||||
f'aria-label="Otwórz adres z kodu QR">{qr}</a>'
|
f'aria-label="Otwórz adres z kodu QR">{qr}</a>'
|
||||||
if qr
|
if qr
|
||||||
else '<div class="title-qr-cell title-qr-empty"><span>QR</span><strong>'
|
else '<div class="title-qr-cell title-qr-empty"><span>QR</span><strong>'
|
||||||
@@ -5463,11 +5469,15 @@ def render_page_header_5x_tex(data: dict, page: int, total: int) -> list[str]:
|
|||||||
f"title_block.{field_name} zawiera znaki nieobsługiwane przez nagłówek TeX"
|
f"title_block.{field_name} zawiera znaki nieobsługiwane przez nagłówek TeX"
|
||||||
)
|
)
|
||||||
page_qr = (
|
page_qr = (
|
||||||
rf"\qrcode[level=L,height=22mm]{{{url}}}"
|
rf"\href{{{url}}}{{\qrcode[level=L,height=22mm]{{{url}}}}}"
|
||||||
if fields["show_qr"]
|
if fields["show_qr"]
|
||||||
else r"{\tiny QR wyłączony}"
|
else r"{\tiny QR wyłączony}"
|
||||||
)
|
)
|
||||||
source_qr = rf"\qrcode[level=L,height=22mm]{{{source_url}}}" if dual_qr else ""
|
source_qr = (
|
||||||
|
rf"\href{{{source_url}}}{{\qrcode[level=L,height=22mm]{{{source_url}}}}}"
|
||||||
|
if dual_qr
|
||||||
|
else ""
|
||||||
|
)
|
||||||
title = f"{card_prefix(card)}: {card.get('title', '')}"
|
title = f"{card_prefix(card)}: {card.get('title', '')}"
|
||||||
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
|
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
|
||||||
primary_width = r"\dimexpr\linewidth-111mm\relax" if dual_qr else r"\dimexpr\linewidth-85mm\relax"
|
primary_width = r"\dimexpr\linewidth-111mm\relax" if dual_qr else r"\dimexpr\linewidth-85mm\relax"
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+9
-9
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user