@joewinke/jatui 0.36.3 → 0.38.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/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/dist/components/Button.svelte +8 -0
- package/dist/components/TextInput.svelte +39 -6
- package/dist/components/TextInput.svelte.d.ts +15 -7
- package/dist/components/basic/Checkbox.svelte +52 -14
- package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
- package/package.json +11 -2
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* collage block — 2-40 captioned images as masonry / grid / strip, with an
|
|
4
|
+
* optional full-width hero and an accessible lightbox.
|
|
5
|
+
*
|
|
6
|
+
* The lightbox is view-only interaction (no per-viewer state): it works on
|
|
7
|
+
* public share pages exactly as in the app. Esc closes, arrow keys walk the
|
|
8
|
+
* images in DISPLAY order (hero first), focus moves into the dialog on
|
|
9
|
+
* open and returns to the trigger on close, Tab is trapped inside, and the
|
|
10
|
+
* body scroll locks while it is open (all restored on close/unmount).
|
|
11
|
+
*
|
|
12
|
+
* SSR-safe: the dialog exists only after a click; no browser APIs run at
|
|
13
|
+
* init. Images are lazy + async-decoded; captions render as text.
|
|
14
|
+
*/
|
|
15
|
+
import type { BlockProps } from "../../types"
|
|
16
|
+
import {
|
|
17
|
+
altOf,
|
|
18
|
+
defaultColumnsFor,
|
|
19
|
+
GRID_COLS_CLASS,
|
|
20
|
+
itemLabel,
|
|
21
|
+
MASONRY_COLS_CLASS,
|
|
22
|
+
viewOrder,
|
|
23
|
+
type CollageItem,
|
|
24
|
+
type CollageParams,
|
|
25
|
+
} from "./schema"
|
|
26
|
+
import SectionTools from "../../SectionTools.svelte"
|
|
27
|
+
|
|
28
|
+
let { params, ctx }: BlockProps<CollageParams> = $props()
|
|
29
|
+
|
|
30
|
+
const layout = $derived(params?.layout ?? "grid")
|
|
31
|
+
const items = $derived(params?.items ?? [])
|
|
32
|
+
const heroIndex = $derived(params?.heroIndex)
|
|
33
|
+
const lightboxOn = $derived(params?.lightbox !== false)
|
|
34
|
+
|
|
35
|
+
/** Display order the lightbox walks: hero first, then the rest. */
|
|
36
|
+
const order = $derived(viewOrder(items.length, heroIndex))
|
|
37
|
+
const cols = $derived(params?.columns ?? defaultColumnsFor(items.length))
|
|
38
|
+
const colsClass = $derived(GRID_COLS_CLASS[cols] ?? GRID_COLS_CLASS[2])
|
|
39
|
+
const masonryClass = $derived(
|
|
40
|
+
MASONRY_COLS_CLASS[cols] ?? MASONRY_COLS_CLASS[2],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// ---- lightbox --------------------------------------------------------------
|
|
44
|
+
let open = $state(false)
|
|
45
|
+
/** Position within `order`, not the raw item index. */
|
|
46
|
+
let pos = $state(0)
|
|
47
|
+
let lastFocus: Element | null = null
|
|
48
|
+
let closeBtn: HTMLButtonElement | null = $state(null)
|
|
49
|
+
let dialogRef: HTMLDivElement | null = $state(null)
|
|
50
|
+
|
|
51
|
+
const current = $derived(items[order[pos] ?? 0] as CollageItem | undefined)
|
|
52
|
+
|
|
53
|
+
/** Item index → its position in display order. */
|
|
54
|
+
function posOf(index: number): number {
|
|
55
|
+
return Math.max(0, order.indexOf(index))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function openAt(p: number) {
|
|
59
|
+
if (!lightboxOn) return
|
|
60
|
+
pos = p
|
|
61
|
+
lastFocus = document.activeElement
|
|
62
|
+
open = true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function close() {
|
|
66
|
+
open = false
|
|
67
|
+
if (lastFocus instanceof HTMLElement) lastFocus.focus()
|
|
68
|
+
lastFocus = null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function step(delta: number) {
|
|
72
|
+
const n = order.length
|
|
73
|
+
if (n === 0) return
|
|
74
|
+
pos = (((pos + delta) % n) + n) % n
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onWindowKey(e: KeyboardEvent) {
|
|
78
|
+
if (!open) return
|
|
79
|
+
if (e.key === "Escape") {
|
|
80
|
+
e.preventDefault()
|
|
81
|
+
close()
|
|
82
|
+
} else if (e.key === "ArrowRight") {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
step(1)
|
|
85
|
+
} else if (e.key === "ArrowLeft") {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
step(-1)
|
|
88
|
+
} else if (e.key === "Tab") {
|
|
89
|
+
// Minimal focus trap: wrap at the dialog's button bounds.
|
|
90
|
+
const focusables = dialogRef
|
|
91
|
+
? [...dialogRef.querySelectorAll<HTMLButtonElement>("button")]
|
|
92
|
+
: []
|
|
93
|
+
if (focusables.length === 0) return
|
|
94
|
+
const first = focusables[0]
|
|
95
|
+
const last = focusables[focusables.length - 1]
|
|
96
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
last.focus()
|
|
99
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
first.focus()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Focus the dialog's close button once it exists; lock body scroll while
|
|
107
|
+
// open (both restore on close / unmount via the effect teardown).
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (open && closeBtn) closeBtn.focus()
|
|
110
|
+
})
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (!open) return
|
|
113
|
+
const prev = document.body.style.overflow
|
|
114
|
+
document.body.style.overflow = "hidden"
|
|
115
|
+
return () => {
|
|
116
|
+
document.body.style.overflow = prev
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// ---- media -----------------------------------------------------------------
|
|
121
|
+
/** Object-fit classes per surface; masonry keeps the natural ratio. */
|
|
122
|
+
function mediaClass(surface: "hero" | "cell"): string {
|
|
123
|
+
if (layout === "masonry") return "w-full h-auto"
|
|
124
|
+
if (surface === "hero") return "aspect-video w-full object-cover"
|
|
125
|
+
return "aspect-[4/3] w-full object-cover"
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
{#snippet figure(item: CollageItem, index: number, surface: "hero" | "cell")}
|
|
130
|
+
{#if lightboxOn}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class="group block w-full text-left"
|
|
134
|
+
aria-label={itemLabel(item, index + 1)}
|
|
135
|
+
onclick={() => openAt(posOf(index))}
|
|
136
|
+
>
|
|
137
|
+
<span class="block overflow-hidden rounded-field">
|
|
138
|
+
<img
|
|
139
|
+
src={item.src}
|
|
140
|
+
alt={altOf(item)}
|
|
141
|
+
loading="lazy"
|
|
142
|
+
decoding="async"
|
|
143
|
+
class={mediaClass(surface)}
|
|
144
|
+
/>
|
|
145
|
+
</span>
|
|
146
|
+
{#if item.caption}
|
|
147
|
+
<span class="mt-1 block text-xs text-base-content/70">
|
|
148
|
+
{item.caption}
|
|
149
|
+
</span>
|
|
150
|
+
{/if}
|
|
151
|
+
</button>
|
|
152
|
+
{:else}
|
|
153
|
+
<figure class="m-0">
|
|
154
|
+
<span class="block overflow-hidden rounded-field">
|
|
155
|
+
<img
|
|
156
|
+
src={item.src}
|
|
157
|
+
alt={altOf(item)}
|
|
158
|
+
loading="lazy"
|
|
159
|
+
decoding="async"
|
|
160
|
+
class={mediaClass(surface)}
|
|
161
|
+
/>
|
|
162
|
+
</span>
|
|
163
|
+
{#if item.caption}
|
|
164
|
+
<figcaption class="mt-1 text-xs text-base-content/70">
|
|
165
|
+
{item.caption}
|
|
166
|
+
</figcaption>
|
|
167
|
+
{/if}
|
|
168
|
+
</figure>
|
|
169
|
+
{/if}
|
|
170
|
+
{/snippet}
|
|
171
|
+
|
|
172
|
+
<section class="artifact-collage w-full">
|
|
173
|
+
{#if params?.title}
|
|
174
|
+
<div class="mb-2 flex items-center gap-2">
|
|
175
|
+
<div class="min-w-0 text-sm font-medium">{params.title}</div>
|
|
176
|
+
<SectionTools {ctx} inline />
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
{#if heroIndex !== undefined && items[heroIndex]}
|
|
181
|
+
<div class="mb-3">
|
|
182
|
+
{@render figure(items[heroIndex], heroIndex, "hero")}
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if layout === "masonry"}
|
|
187
|
+
<div class="columns-2 gap-3 {masonryClass}">
|
|
188
|
+
{#each items as item, i (i)}
|
|
189
|
+
{@const isHero = heroIndex === i}
|
|
190
|
+
{#if !isHero}
|
|
191
|
+
<div class="mb-3 break-inside-avoid">
|
|
192
|
+
{@render figure(item, i, "cell")}
|
|
193
|
+
</div>
|
|
194
|
+
{/if}
|
|
195
|
+
{/each}
|
|
196
|
+
</div>
|
|
197
|
+
{:else if layout === "strip"}
|
|
198
|
+
<div
|
|
199
|
+
class="flex gap-3 overflow-x-auto pb-1 snap-x"
|
|
200
|
+
role="region"
|
|
201
|
+
aria-label={params?.title ?? "Image strip"}
|
|
202
|
+
tabindex="0"
|
|
203
|
+
>
|
|
204
|
+
{#each items as item, i (i)}
|
|
205
|
+
{@const isHero = heroIndex === i}
|
|
206
|
+
{#if !isHero}
|
|
207
|
+
<div class="w-60 shrink-0 snap-center sm:w-72">
|
|
208
|
+
{@render figure(item, i, "cell")}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
{/each}
|
|
212
|
+
</div>
|
|
213
|
+
{:else}
|
|
214
|
+
<div class="grid grid-cols-2 gap-3 {colsClass}">
|
|
215
|
+
{#each items as item, i (i)}
|
|
216
|
+
{@const isHero = heroIndex === i}
|
|
217
|
+
{#if !isHero}
|
|
218
|
+
{@render figure(item, i, "cell")}
|
|
219
|
+
{/if}
|
|
220
|
+
{/each}
|
|
221
|
+
</div>
|
|
222
|
+
{/if}
|
|
223
|
+
</section>
|
|
224
|
+
|
|
225
|
+
{#if open && current}
|
|
226
|
+
<!-- The lightbox: a fixed overlay, NOT a <dialog> — jsdom + print + the
|
|
227
|
+
blocks host all treat a plain overlay the same, and focus stays ours. -->
|
|
228
|
+
<!-- The backdrop is a deliberate click-only dismiss surface: the keyboard
|
|
229
|
+
path is Esc and the Close button inside the dialog. -->
|
|
230
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
231
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
232
|
+
<div
|
|
233
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-neutral/90 p-4 sm:p-8"
|
|
234
|
+
role="presentation"
|
|
235
|
+
onclick={(e) => {
|
|
236
|
+
if (e.target === e.currentTarget) close()
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<div
|
|
240
|
+
class="flex max-h-full w-full max-w-4xl flex-col items-center"
|
|
241
|
+
bind:this={dialogRef}
|
|
242
|
+
role="dialog"
|
|
243
|
+
aria-modal="true"
|
|
244
|
+
aria-label={params?.title ?? "Image viewer"}
|
|
245
|
+
>
|
|
246
|
+
<img
|
|
247
|
+
src={current.src}
|
|
248
|
+
alt={altOf(current)}
|
|
249
|
+
class="max-h-[78vh] w-auto max-w-full rounded-box object-contain"
|
|
250
|
+
/>
|
|
251
|
+
{#if current.caption}
|
|
252
|
+
<p class="mt-2 text-center text-sm text-neutral-content">
|
|
253
|
+
{current.caption}
|
|
254
|
+
</p>
|
|
255
|
+
{/if}
|
|
256
|
+
<div class="mt-2 flex items-center gap-2">
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
class="btn btn-sm rounded-field border-base-content/15 bg-base-100 text-base-content"
|
|
260
|
+
aria-label="Previous image"
|
|
261
|
+
onclick={() => step(-1)}
|
|
262
|
+
>
|
|
263
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>
|
|
264
|
+
</button>
|
|
265
|
+
<span class="text-sm tabular-nums text-neutral-content">
|
|
266
|
+
{pos + 1} / {order.length}
|
|
267
|
+
</span>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
class="btn btn-sm rounded-field border-base-content/15 bg-base-100 text-base-content"
|
|
271
|
+
aria-label="Next image"
|
|
272
|
+
onclick={() => step(1)}
|
|
273
|
+
>
|
|
274
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
|
|
275
|
+
</button>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
class="btn btn-sm rounded-field border-base-content/15 bg-base-100 text-base-content"
|
|
279
|
+
aria-label="Close viewer"
|
|
280
|
+
bind:this={closeBtn}
|
|
281
|
+
onclick={close}
|
|
282
|
+
>
|
|
283
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
{/if}
|
|
289
|
+
|
|
290
|
+
<svelte:window onkeydown={onWindowKey} />
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collage block — 2-40 captioned images as masonry / grid / strip, with an
|
|
3
|
+
* optional full-width hero and an accessible lightbox.
|
|
4
|
+
*
|
|
5
|
+
* The lightbox is view-only interaction (no per-viewer state): it works on
|
|
6
|
+
* public share pages exactly as in the app. Esc closes, arrow keys walk the
|
|
7
|
+
* images in DISPLAY order (hero first), focus moves into the dialog on
|
|
8
|
+
* open and returns to the trigger on close, Tab is trapped inside, and the
|
|
9
|
+
* body scroll locks while it is open (all restored on close/unmount).
|
|
10
|
+
*
|
|
11
|
+
* SSR-safe: the dialog exists only after a click; no browser APIs run at
|
|
12
|
+
* init. Images are lazy + async-decoded; captions render as text.
|
|
13
|
+
*/
|
|
14
|
+
import type { BlockProps } from "../../types";
|
|
15
|
+
declare const CollageBlock: import("svelte").Component<BlockProps<{
|
|
16
|
+
layout: "grid" | "masonry" | "strip";
|
|
17
|
+
items: {
|
|
18
|
+
src: string;
|
|
19
|
+
caption?: string | undefined;
|
|
20
|
+
alt?: string | undefined;
|
|
21
|
+
}[];
|
|
22
|
+
lightbox: boolean;
|
|
23
|
+
title?: string | undefined;
|
|
24
|
+
columns?: number | undefined;
|
|
25
|
+
heroIndex?: number | undefined;
|
|
26
|
+
dataKey?: string | undefined;
|
|
27
|
+
}>, {}, "">;
|
|
28
|
+
type CollageBlock = ReturnType<typeof CollageBlock>;
|
|
29
|
+
export default CollageBlock;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage block definition (block type "collage").
|
|
3
|
+
* Contract: src/lib/artifact-blocks/CONTRACT.md — registry.ts discovers this
|
|
4
|
+
* folder; nothing else registers a block.
|
|
5
|
+
*/
|
|
6
|
+
import type { ArtifactBlockDef } from "../../types";
|
|
7
|
+
import { type CollageParams } from "./schema";
|
|
8
|
+
declare const def: ArtifactBlockDef<CollageParams>;
|
|
9
|
+
export default def;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import CollageBlock from "./CollageBlock.svelte";
|
|
2
|
+
import { collageSchema } from "./schema";
|
|
3
|
+
const EXAMPLE = {
|
|
4
|
+
title: "Site relaunch — art direction round 2",
|
|
5
|
+
layout: "masonry",
|
|
6
|
+
items: [
|
|
7
|
+
{ src: "/media/relaunch/hero-dusk.jpg", caption: "Hero, dusk grade" },
|
|
8
|
+
{ src: "/media/relaunch/service-cards.jpg", caption: "Service card set" },
|
|
9
|
+
{ src: "/media/relaunch/logo-lockup.jpg", caption: "Logo lockup" },
|
|
10
|
+
{ src: "/media/relaunch/booking-panel.jpg", caption: "Booking panel" },
|
|
11
|
+
{ src: "/media/relaunch/404.jpg", caption: "404 page" },
|
|
12
|
+
],
|
|
13
|
+
heroIndex: 0,
|
|
14
|
+
columns: 3,
|
|
15
|
+
lightbox: true,
|
|
16
|
+
};
|
|
17
|
+
const def = {
|
|
18
|
+
type: "collage",
|
|
19
|
+
title: "Collage (image gallery)",
|
|
20
|
+
description: "2-40 captioned images in one gallery: layout 'masonry' (CSS columns, natural aspect ratios — art/brand rounds), 'grid' (uniform 4:3 cells — UI screenshots, contact sheets) or 'strip' (horizontal scroll row — before/after sets, film strips). Every item is { src, caption?, alt? } — src is an https:// URL or a root-relative MOS path (never base64), captions render as text under each image, alt defaults to the caption. heroIndex lifts one item to a full-width 16:9 hero above the rest. Clicking an image opens an accessible lightbox (Esc closes, arrow keys move, focus returns to the trigger) walking items in DISPLAY order — hero first; lightbox:false turns the whole thing static figures. Grid/masonry columns default by count (≤4 → 2, ≤9 → 3, else 4; phones always 2). Tip: masonry for mixed-ratio art, grid when rows should align; keep captions to one line — the story goes in a neighboring markdown block.",
|
|
21
|
+
schema: collageSchema,
|
|
22
|
+
component: CollageBlock,
|
|
23
|
+
// mos-8fvt9: the section's Listen + comment sit on the heading row.
|
|
24
|
+
hostsTools: (p) => Boolean(p.title),
|
|
25
|
+
example: EXAMPLE,
|
|
26
|
+
reviewableByDefault: true,
|
|
27
|
+
dataParam: "dataKey",
|
|
28
|
+
};
|
|
29
|
+
export default def;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage block — params schema + pure layout helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only (no DOM) so `block.test.ts` can exercise them and
|
|
5
|
+
* `CollageBlock.svelte` stays a thin renderer. Untrusted input rules
|
|
6
|
+
* (CONTRACT.md): image srcs go through the shared `urlSchema` (https:// or
|
|
7
|
+
* root-relative — never data:/javascript:), every string is length-capped,
|
|
8
|
+
* media is by URL only (never base64).
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
declare const COLLAGE_LAYOUTS: readonly ["masonry", "grid", "strip"];
|
|
12
|
+
export type CollageLayout = (typeof COLLAGE_LAYOUTS)[number];
|
|
13
|
+
declare const itemSchema: z.ZodObject<{
|
|
14
|
+
src: z.ZodString;
|
|
15
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
16
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type CollageItem = z.infer<typeof itemSchema>;
|
|
19
|
+
export declare const collageSchema: z.ZodObject<{
|
|
20
|
+
title: z.ZodOptional<z.ZodString>;
|
|
21
|
+
layout: z.ZodDefault<z.ZodEnum<{
|
|
22
|
+
grid: "grid";
|
|
23
|
+
masonry: "masonry";
|
|
24
|
+
strip: "strip";
|
|
25
|
+
}>>;
|
|
26
|
+
items: z.ZodArray<z.ZodObject<{
|
|
27
|
+
src: z.ZodString;
|
|
28
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
29
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
columns: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
heroIndex: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
lightbox: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
+
dataKey: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type CollageParams = z.infer<typeof collageSchema>;
|
|
37
|
+
/** Column count when the spec doesn't name one: small sets stay chunky. */
|
|
38
|
+
export declare function defaultColumnsFor(count: number): 2 | 3 | 4;
|
|
39
|
+
/** Static Tailwind classes — dynamic class names break extraction. */
|
|
40
|
+
export declare const GRID_COLS_CLASS: Record<number, string>;
|
|
41
|
+
export declare const MASONRY_COLS_CLASS: Record<number, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Display order of item indexes: the hero (when set) comes first, then the
|
|
44
|
+
* remaining items in their array order. The lightbox walks THIS order, so
|
|
45
|
+
* its prev/next matches what a viewer sees left-to-right.
|
|
46
|
+
*/
|
|
47
|
+
export declare function viewOrder(count: number, heroIndex: number | undefined): number[];
|
|
48
|
+
/** Effective alt text: authored alt, else the caption, else none. */
|
|
49
|
+
export declare function altOf(item: CollageItem): string;
|
|
50
|
+
/** Screen-reader label for an item's open button. */
|
|
51
|
+
export declare function itemLabel(item: CollageItem, position: number): string;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage block — params schema + pure layout helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only (no DOM) so `block.test.ts` can exercise them and
|
|
5
|
+
* `CollageBlock.svelte` stays a thin renderer. Untrusted input rules
|
|
6
|
+
* (CONTRACT.md): image srcs go through the shared `urlSchema` (https:// or
|
|
7
|
+
* root-relative — never data:/javascript:), every string is length-capped,
|
|
8
|
+
* media is by URL only (never base64).
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { shortTextSchema, urlSchema } from "../../params";
|
|
12
|
+
const COLLAGE_LAYOUTS = ["masonry", "grid", "strip"];
|
|
13
|
+
/** A collage of fewer than 2 images is not a collage; 41 is a page. */
|
|
14
|
+
const MIN_ITEMS = 2;
|
|
15
|
+
const MAX_ITEMS = 40;
|
|
16
|
+
const itemSchema = z.object({
|
|
17
|
+
src: urlSchema,
|
|
18
|
+
/** Short line under the image. */
|
|
19
|
+
caption: shortTextSchema.optional(),
|
|
20
|
+
/** Alt text; defaults to the caption (both empty → decorative). */
|
|
21
|
+
alt: shortTextSchema.optional(),
|
|
22
|
+
});
|
|
23
|
+
export const collageSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
title: shortTextSchema.optional(),
|
|
26
|
+
layout: z.enum(COLLAGE_LAYOUTS).default("grid"),
|
|
27
|
+
items: z.array(itemSchema).min(MIN_ITEMS).max(MAX_ITEMS),
|
|
28
|
+
/**
|
|
29
|
+
* Grid/masonry column count at sm+ (phone is always 2). Defaults by
|
|
30
|
+
* count — see defaultColumnsFor().
|
|
31
|
+
*/
|
|
32
|
+
columns: z.number().int().min(2).max(4).optional(),
|
|
33
|
+
/** Index into `items` of the large hero rendered above the rest. */
|
|
34
|
+
heroIndex: z.number().int().min(0).optional(),
|
|
35
|
+
/** Click an image to open the lightbox (default true). */
|
|
36
|
+
lightbox: z.boolean().default(true),
|
|
37
|
+
/** Phase-3 live data: key into ctx.data whose value replaces params. */
|
|
38
|
+
dataKey: z.string().max(80).optional(),
|
|
39
|
+
})
|
|
40
|
+
.superRefine((v, ctx) => {
|
|
41
|
+
if (v.heroIndex !== undefined && v.heroIndex >= v.items.length)
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: "custom",
|
|
44
|
+
path: ["heroIndex"],
|
|
45
|
+
message: `heroIndex must be an index into items (0-${v.items.length - 1})`,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
// ── Pure helpers ──────────────────────────────────────────────────────────
|
|
49
|
+
/** Column count when the spec doesn't name one: small sets stay chunky. */
|
|
50
|
+
export function defaultColumnsFor(count) {
|
|
51
|
+
if (count <= 4)
|
|
52
|
+
return 2;
|
|
53
|
+
if (count <= 9)
|
|
54
|
+
return 3;
|
|
55
|
+
return 4;
|
|
56
|
+
}
|
|
57
|
+
/** Static Tailwind classes — dynamic class names break extraction. */
|
|
58
|
+
export const GRID_COLS_CLASS = {
|
|
59
|
+
2: "sm:grid-cols-2",
|
|
60
|
+
3: "sm:grid-cols-3",
|
|
61
|
+
4: "sm:grid-cols-4",
|
|
62
|
+
};
|
|
63
|
+
export const MASONRY_COLS_CLASS = {
|
|
64
|
+
2: "sm:columns-2",
|
|
65
|
+
3: "sm:columns-3",
|
|
66
|
+
4: "sm:columns-4",
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Display order of item indexes: the hero (when set) comes first, then the
|
|
70
|
+
* remaining items in their array order. The lightbox walks THIS order, so
|
|
71
|
+
* its prev/next matches what a viewer sees left-to-right.
|
|
72
|
+
*/
|
|
73
|
+
export function viewOrder(count, heroIndex) {
|
|
74
|
+
const all = Array.from({ length: count }, (_, i) => i);
|
|
75
|
+
if (heroIndex === undefined || heroIndex < 0 || heroIndex >= count)
|
|
76
|
+
return all;
|
|
77
|
+
return [heroIndex, ...all.filter((i) => i !== heroIndex)];
|
|
78
|
+
}
|
|
79
|
+
/** Effective alt text: authored alt, else the caption, else none. */
|
|
80
|
+
export function altOf(item) {
|
|
81
|
+
return item.alt ?? item.caption ?? "";
|
|
82
|
+
}
|
|
83
|
+
/** Screen-reader label for an item's open button. */
|
|
84
|
+
export function itemLabel(item, position) {
|
|
85
|
+
return item.caption ? `Open: ${item.caption}` : `Open image ${position}`;
|
|
86
|
+
}
|