@jtakeit/astro 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/bin/jtk.mjs +41 -0
- package/docs/booking.md +164 -0
- package/docs/catalogue.md +459 -0
- package/docs/collections.md +249 -0
- package/docs/css.md +86 -0
- package/docs/gallery.md +127 -0
- package/docs/hero-motion.md +189 -0
- package/docs/kit.md +454 -0
- package/docs/languages.md +182 -0
- package/docs/lead-form.md +109 -0
- package/docs/pages.md +193 -0
- package/docs/photos.md +314 -0
- package/docs/scaffold.md +75 -0
- package/docs/shapes.md +140 -0
- package/docs/surface.md +187 -0
- package/lib/catalogue.mjs +1678 -0
- package/lib/codes.mjs +171 -0
- package/lib/create.mjs +282 -0
- package/package.json +16 -0
- package/template/astro.config.mjs +84 -0
- package/template/figures.mjs +122 -0
- package/template/gitignore +16 -0
- package/template/jtakeit-meta.mjs +112 -0
- package/template/jtk/content/index.json +38 -0
- package/template/jtk/design.json +24 -0
- package/template/markdown.mjs +36 -0
- package/template/package-lock.json +5320 -0
- package/template/package.json +26 -0
- package/template/specimens.mjs +46 -0
- package/template/src/components/Blocks.astro +151 -0
- package/template/src/components/BookingForm.astro +506 -0
- package/template/src/components/Clip.astro +155 -0
- package/template/src/components/Hero.astro +66 -0
- package/template/src/components/LeadForm.astro +347 -0
- package/template/src/components/OpeningHours.astro +69 -0
- package/template/src/components/Pile.astro +185 -0
- package/template/src/components/Shot.astro +472 -0
- package/template/src/components/gallery/Gallery.astro +381 -0
- package/template/src/components/gallery/galleries.ts +139 -0
- package/template/src/components/motion/HeroField.astro +520 -0
- package/template/src/components/motion/fields.ts +430 -0
- package/template/src/components/surface/Pattern.astro +278 -0
- package/template/src/components/surface/patterns.ts +187 -0
- package/template/src/content/blocks.ts +758 -0
- package/template/src/content.config.ts +19 -0
- package/template/src/copy/LOCALE.ts +324 -0
- package/template/src/data/site.ts +137 -0
- package/template/src/layouts/Layout.astro +282 -0
- package/template/src/lib/alive.ts +49 -0
- package/template/src/lib/entries.ts +106 -0
- package/template/src/lib/entryLoader.ts +315 -0
- package/template/src/lib/noise.ts +26 -0
- package/template/src/lib/page.ts +287 -0
- package/template/src/lib/photos.ts +168 -0
- package/template/src/lib/under.ts +32 -0
- package/template/src/lib/uploads.ts +85 -0
- package/template/src/pages/[...entry].astro +207 -0
- package/template/src/pages/[...feed].xml.ts +64 -0
- package/template/src/pages/index.astro +90 -0
- package/template/src/pages/llms.txt.ts +50 -0
- package/template/src/pages/privacy.astro +59 -0
- package/template/src/pages/robots.txt.ts +21 -0
- package/template/src/pages/sitemap.xml.ts +50 -0
- package/template/src/styles/global.css +411 -0
- package/template/src/styles/surface.css +375 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The work, shown.
|
|
4
|
+
*
|
|
5
|
+
* Six arrangements, and the choice is made by the material: how much work there
|
|
6
|
+
* is, and whether it has an order. What each one is for is in ./galleries.ts
|
|
7
|
+
* and in the table in `docs/gallery.md` of @jtakeit/astro.
|
|
8
|
+
*
|
|
9
|
+
* <Gallery arrangement="wall" items={WORK} />
|
|
10
|
+
* <Gallery arrangement="index" items={[{ name: 'sleeve', caption: 'Forearm, two sittings' }]} />
|
|
11
|
+
*
|
|
12
|
+
* ── one of them moves, and that is a bounded exception ──────────────────────
|
|
13
|
+
*
|
|
14
|
+
* The kit's Motion rule is that nothing moves while it is being read. A wall of
|
|
15
|
+
* photographs is *looked at* rather than read, which is what makes the
|
|
16
|
+
* exception arguable at all — and it is only allowed on three conditions, all
|
|
17
|
+
* of them here rather than in a comment somewhere:
|
|
18
|
+
*
|
|
19
|
+
* · it stops on hover and on keyboard focus, so it is still whenever anybody
|
|
20
|
+
* is actually looking at one frame;
|
|
21
|
+
* · reduced motion gets the wall laid out still and in full — the finished
|
|
22
|
+
* thing, not a shorter one;
|
|
23
|
+
* · nothing readable is inside it and nothing is positioned against it.
|
|
24
|
+
*
|
|
25
|
+
* It also declares itself: `data-motion="gallery"` marks the region as one
|
|
26
|
+
* thing that moves forever, and `fl-check` counts those regions rather than
|
|
27
|
+
* counting animated elements. A page whose hero already moves and whose wall
|
|
28
|
+
* moves too is two, and two is the number the check argues about.
|
|
29
|
+
*/
|
|
30
|
+
import Shot from '../Shot.astro';
|
|
31
|
+
import { GALLERIES, asPieces, deal, type Arrangement, type Piece } from './galleries';
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
/**
|
|
35
|
+
* ragged · wall · contact · strip · spread · index.
|
|
36
|
+
* Required: there is no default arrangement, because the right one is decided
|
|
37
|
+
* by how much work there is and a default would decide it by habit.
|
|
38
|
+
*/
|
|
39
|
+
arrangement: Arrangement;
|
|
40
|
+
/** File names in src/assets, or `{ name, caption }` where the words matter. */
|
|
41
|
+
items: readonly (string | Piece)[];
|
|
42
|
+
/** Overrides the arrangement's own column count. */
|
|
43
|
+
columns?: number;
|
|
44
|
+
/**
|
|
45
|
+
* `wall` only: how long one full cycle takes. Long. A wall that completes in
|
|
46
|
+
* thirty seconds is a wall somebody is watching instead of reading the page.
|
|
47
|
+
*/
|
|
48
|
+
cycle?: string;
|
|
49
|
+
/** `wall` only: how tall the window is. About two photographs. */
|
|
50
|
+
window?: string;
|
|
51
|
+
class?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { arrangement, items, columns, cycle, window: windowHeight, class: className } = Astro.props;
|
|
55
|
+
|
|
56
|
+
const recipe = GALLERIES[arrangement];
|
|
57
|
+
if (!recipe) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`<Gallery arrangement="${arrangement}"> is not one of: ${Object.keys(GALLERIES).join(', ')}. ` +
|
|
60
|
+
'See references/gallery.md — the arrangement is chosen from the material, not from taste.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pieces = asPieces(items);
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* How much work there is decides the arrangement, so a count outside what an
|
|
68
|
+
* arrangement is for is worth saying out loud at the moment it can still be
|
|
69
|
+
* changed. It does not fail the build: a `spread` of twelve is a decision
|
|
70
|
+
* somebody may have made on purpose, and a warning in front of the person who
|
|
71
|
+
* chose it is enough.
|
|
72
|
+
*/
|
|
73
|
+
const [floor, ceiling] = recipe.works;
|
|
74
|
+
if (pieces.length < floor || pieces.length > ceiling) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`[gallery] ${arrangement} is for ${floor}–${ceiling} pieces and it has been given ${pieces.length}. ` +
|
|
77
|
+
(pieces.length < floor
|
|
78
|
+
? 'Below that it reads as a business that has done very little — see gallery.md for the arrangement that suits a short list.'
|
|
79
|
+
: 'Above that it stops reading as one thing.'),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (arrangement === 'index' && !pieces.some((p) => p.caption)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'<Gallery arrangement="index"> is a list of work *with words beside it*, and none of these items ' +
|
|
86
|
+
'has a caption. Give them captions, or use `ragged` — an index with nothing to read is a worse ragged.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cols = Math.max(1, columns ?? recipe.columns);
|
|
91
|
+
const columnsOf = arrangement === 'ragged' || arrangement === 'wall' ? deal(pieces, cols) : [];
|
|
92
|
+
|
|
93
|
+
const vars = [
|
|
94
|
+
`--gallery-columns:${cols}`,
|
|
95
|
+
cycle ? `--gallery-cycle:${cycle}` : '',
|
|
96
|
+
windowHeight ? `--gallery-window:${windowHeight}` : '',
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(';');
|
|
100
|
+
|
|
101
|
+
/** The wall runs its outer columns one way and its inner ones the other. */
|
|
102
|
+
const drift = (n: number) => (n % 2 === 1 ? 'up' : 'down');
|
|
103
|
+
|
|
104
|
+
const shotWidth = arrangement === 'contact' ? 420 : arrangement === 'spread' ? 1400 : 700;
|
|
105
|
+
const sizes =
|
|
106
|
+
arrangement === 'spread'
|
|
107
|
+
? '(min-width: 62rem) 70vw, 92vw'
|
|
108
|
+
: arrangement === 'contact'
|
|
109
|
+
? '(min-width: 62rem) 16vw, 30vw'
|
|
110
|
+
: '(min-width: 62rem) 25vw, 44vw';
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
class:list={['gallery', `gallery--${arrangement}`, className]}
|
|
115
|
+
data-arrangement={arrangement}
|
|
116
|
+
data-motion={recipe.moves ? 'gallery' : undefined}
|
|
117
|
+
style={vars}
|
|
118
|
+
>
|
|
119
|
+
{
|
|
120
|
+
arrangement === 'wall' || arrangement === 'ragged' ? (
|
|
121
|
+
columnsOf.map((column, n) => (
|
|
122
|
+
<div class="gallery__col" data-drift={recipe.moves ? drift(n) : undefined}>
|
|
123
|
+
<div class="gallery__track">
|
|
124
|
+
{/*
|
|
125
|
+
A moving column is rendered twice and travels exactly half its own
|
|
126
|
+
height, which is what makes the loop seamless: the halfway point
|
|
127
|
+
is pixel-identical to the start. The second pass is the same
|
|
128
|
+
pictures saying nothing new, so it is hidden from a screen reader
|
|
129
|
+
rather than read out again.
|
|
130
|
+
*/}
|
|
131
|
+
{(recipe.moves ? [0, 1] : [0]).map((pass) =>
|
|
132
|
+
column.map((piece) => (
|
|
133
|
+
<figure class="gallery__frame" aria-hidden={pass === 1 ? 'true' : undefined} data-pass={pass}>
|
|
134
|
+
<Shot
|
|
135
|
+
name={piece.name}
|
|
136
|
+
alt={piece.alt}
|
|
137
|
+
path={pass === 0 ? piece.path : undefined}
|
|
138
|
+
decorative={pass === 1}
|
|
139
|
+
width={shotWidth}
|
|
140
|
+
sizes={sizes}
|
|
141
|
+
/>
|
|
142
|
+
</figure>
|
|
143
|
+
)),
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
))
|
|
148
|
+
) : (
|
|
149
|
+
pieces.map((piece) => (
|
|
150
|
+
<figure class="gallery__frame">
|
|
151
|
+
<Shot
|
|
152
|
+
name={piece.name}
|
|
153
|
+
alt={piece.alt}
|
|
154
|
+
path={piece.path}
|
|
155
|
+
width={shotWidth}
|
|
156
|
+
sizes={sizes}
|
|
157
|
+
ratio={arrangement === 'contact' ? '1 / 1' : undefined}
|
|
158
|
+
/>
|
|
159
|
+
{/* A caption comes from a `list` of rows rather than from a gallery
|
|
160
|
+
field — see the `index` arrangement in references/gallery.md — and
|
|
161
|
+
it is content, so it carries its own path. */}
|
|
162
|
+
{piece.caption && (
|
|
163
|
+
<figcaption class="gallery__caption" data-jtk-path={piece.captionPath}>
|
|
164
|
+
{piece.caption}
|
|
165
|
+
</figcaption>
|
|
166
|
+
)}
|
|
167
|
+
</figure>
|
|
168
|
+
))
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
.gallery {
|
|
175
|
+
--gap: clamp(0.75rem, 1.6vw, 1.25rem);
|
|
176
|
+
margin-block-start: clamp(2rem, 4vw, 3.5rem);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.gallery__frame {
|
|
180
|
+
margin: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.gallery__caption {
|
|
184
|
+
padding-block-start: 0.5rem;
|
|
185
|
+
font-size: 0.875rem;
|
|
186
|
+
line-height: 1.5;
|
|
187
|
+
color: var(--ink-quiet);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ── ragged: columns, still, nothing cropped ───────────────────────────── */
|
|
191
|
+
|
|
192
|
+
.gallery--ragged,
|
|
193
|
+
.gallery--wall {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
196
|
+
gap: var(--gap);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (min-width: 62rem) {
|
|
200
|
+
.gallery--ragged,
|
|
201
|
+
.gallery--wall {
|
|
202
|
+
grid-template-columns: repeat(var(--gallery-columns, 3), minmax(0, 1fr));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.gallery__track {
|
|
207
|
+
display: grid;
|
|
208
|
+
align-content: start;
|
|
209
|
+
gap: var(--gap);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
* Below 62rem the last column is dropped rather than squeezed. Each column is
|
|
214
|
+
* a fair sample — the pieces are dealt, not sliced — so what goes is a third
|
|
215
|
+
* of the wall and not a period of the work.
|
|
216
|
+
*/
|
|
217
|
+
@media (max-width: 61.99rem) {
|
|
218
|
+
.gallery--ragged .gallery__col:nth-child(n + 3),
|
|
219
|
+
.gallery--wall .gallery__col:nth-child(n + 3) {
|
|
220
|
+
display: none;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ── wall: the one that moves ──────────────────────────────────────────── */
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
* A window about two photographs tall. The block is a view onto the work
|
|
228
|
+
* rather than the work laid end to end, and that is what stops a body of
|
|
229
|
+
* thirty-seven from becoming most of the page.
|
|
230
|
+
*/
|
|
231
|
+
.gallery--wall {
|
|
232
|
+
block-size: var(--gallery-window, min(86svh, 52rem));
|
|
233
|
+
overflow: clip;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.gallery--wall .gallery__col {
|
|
237
|
+
min-block-size: 0;
|
|
238
|
+
overflow: clip;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
* Half the track's own height, forever. The track is the same pictures twice,
|
|
243
|
+
* so half of it is exactly one pass — the one place a percentage is the right
|
|
244
|
+
* unit, because the distance is a property of the track and not of the
|
|
245
|
+
* screen.
|
|
246
|
+
*/
|
|
247
|
+
@keyframes gallery-down {
|
|
248
|
+
from {
|
|
249
|
+
transform: translate3d(0, -50%, 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
to {
|
|
253
|
+
transform: translate3d(0, 0, 0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@keyframes gallery-up {
|
|
258
|
+
from {
|
|
259
|
+
transform: translate3d(0, 0, 0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
to {
|
|
263
|
+
transform: translate3d(0, -50%, 0);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
* Still whenever somebody is actually looking at one frame — and it is an
|
|
269
|
+
* inherited custom property rather than a `:hover` rule on the track, which
|
|
270
|
+
* is the version that does not work.
|
|
271
|
+
*
|
|
272
|
+
* `animation: … infinite` is a shorthand, so it resets `animation-play-state`
|
|
273
|
+
* to `running`; a `.gallery:hover .gallery__track` rule is *less* specific
|
|
274
|
+
* than the one carrying that shorthand, so the pause loses and the wall never
|
|
275
|
+
* stops. It reads as working — the wall drifts, the page looks right — and
|
|
276
|
+
* the one condition that buys this component its exception is quietly absent.
|
|
277
|
+
*
|
|
278
|
+
* A custom property set on the hovered ancestor inherits down and is read
|
|
279
|
+
* after the shorthand in the same declaration, so specificity never enters
|
|
280
|
+
* into it.
|
|
281
|
+
*/
|
|
282
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
283
|
+
.gallery--wall .gallery__col[data-drift='down'] .gallery__track {
|
|
284
|
+
animation: gallery-down var(--gallery-cycle, 150s) linear infinite;
|
|
285
|
+
animation-play-state: var(--gallery-play, running);
|
|
286
|
+
will-change: transform;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.gallery--wall .gallery__col[data-drift='up'] .gallery__track {
|
|
290
|
+
animation: gallery-up var(--gallery-cycle, 150s) linear infinite;
|
|
291
|
+
animation-play-state: var(--gallery-play, running);
|
|
292
|
+
will-change: transform;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.gallery--wall:hover,
|
|
296
|
+
.gallery--wall:focus-within {
|
|
297
|
+
--gallery-play: paused;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* Reduced motion gets the whole wall, laid out and still: the window opens,
|
|
303
|
+
* the animation never starts, and the second pass — which exists only to make
|
|
304
|
+
* a loop seamless — is not shown, because with nothing moving it would simply
|
|
305
|
+
* be every photograph twice.
|
|
306
|
+
*/
|
|
307
|
+
@media (prefers-reduced-motion: reduce) {
|
|
308
|
+
.gallery--wall {
|
|
309
|
+
block-size: auto;
|
|
310
|
+
overflow: visible;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.gallery--wall .gallery__frame[data-pass='1'] {
|
|
314
|
+
display: none;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ── contact: dense, uniform, many ─────────────────────────────────────── */
|
|
319
|
+
|
|
320
|
+
.gallery--contact {
|
|
321
|
+
display: grid;
|
|
322
|
+
grid-template-columns: repeat(auto-fill, minmax(min(7.5rem, 30vw), 1fr));
|
|
323
|
+
gap: calc(var(--gap) * 0.6);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* ── strip: one row, moved by the visitor ──────────────────────────────── */
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
* It scrolls inside its own container and never takes the page with it, which
|
|
330
|
+
* is the difference between a rail that is allowed and one that is not.
|
|
331
|
+
* Snapping is what makes a drag land on a picture rather than between two.
|
|
332
|
+
*/
|
|
333
|
+
.gallery--strip {
|
|
334
|
+
display: grid;
|
|
335
|
+
grid-auto-flow: column;
|
|
336
|
+
grid-auto-columns: min(22rem, 78vw);
|
|
337
|
+
gap: var(--gap);
|
|
338
|
+
overflow-x: auto;
|
|
339
|
+
overscroll-behavior-inline: contain;
|
|
340
|
+
scroll-snap-type: x mandatory;
|
|
341
|
+
padding-block-end: 0.5rem;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.gallery--strip .gallery__frame {
|
|
345
|
+
scroll-snap-align: start;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ── spread: a few, large ──────────────────────────────────────────────── */
|
|
349
|
+
|
|
350
|
+
.gallery--spread {
|
|
351
|
+
display: grid;
|
|
352
|
+
gap: clamp(2rem, 5vw, 4rem);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ── index: rows with words ────────────────────────────────────────────── */
|
|
356
|
+
|
|
357
|
+
.gallery--index {
|
|
358
|
+
display: grid;
|
|
359
|
+
gap: clamp(1rem, 2.5vw, 2rem);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.gallery--index .gallery__frame {
|
|
363
|
+
display: grid;
|
|
364
|
+
grid-template-columns: minmax(0, 7rem) minmax(0, 1fr);
|
|
365
|
+
gap: clamp(1rem, 3vw, 2rem);
|
|
366
|
+
align-items: start;
|
|
367
|
+
padding-block-end: clamp(1rem, 2.5vw, 2rem);
|
|
368
|
+
border-block-end: 1px solid var(--line);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@media (min-width: 48rem) {
|
|
372
|
+
.gallery--index .gallery__frame {
|
|
373
|
+
grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.gallery--index .gallery__caption {
|
|
378
|
+
padding-block-start: 0;
|
|
379
|
+
font-size: 1rem;
|
|
380
|
+
}
|
|
381
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ways a body of work can be shown.
|
|
3
|
+
*
|
|
4
|
+
* Data only — no DOM. `Gallery.astro` owns the markup, the motion and every
|
|
5
|
+
* path back to a still page; this file is what each arrangement is *for*, which
|
|
6
|
+
* is the part worth reading before choosing one.
|
|
7
|
+
*
|
|
8
|
+
* ── the choice is made by the material, not by taste ────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* A tattooist with eight hundred posts and thirty-seven usable frames is not
|
|
11
|
+
* the same problem as a baker with nine. The first needs volume to read as
|
|
12
|
+
* volume; the second needs nine pictures each given room, because a wall of
|
|
13
|
+
* nine looks like a business that has done nine jobs.
|
|
14
|
+
*
|
|
15
|
+
* So the first question is how much work there is, and the second is whether
|
|
16
|
+
* the work has an order. Everything else follows.
|
|
17
|
+
*
|
|
18
|
+
* ── what an arrangement may not do ─────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* Whatever it looks like, all six clear the same floor: no photograph is
|
|
21
|
+
* stretched, nothing readable sits inside a moving surface, nothing scrolls the
|
|
22
|
+
* page sideways, and `prefers-reduced-motion` gets the work laid out in full
|
|
23
|
+
* and still — the finished thing, never a shorter one.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type Arrangement = 'ragged' | 'wall' | 'contact' | 'strip' | 'spread' | 'index';
|
|
27
|
+
|
|
28
|
+
export interface Recipe {
|
|
29
|
+
/** One line, for the table in references/gallery.md. */
|
|
30
|
+
readonly what: string;
|
|
31
|
+
/** How many pieces it wants. Below the floor it looks thin; above the ceiling it stops reading. */
|
|
32
|
+
readonly works: readonly [number, number];
|
|
33
|
+
/** Whether it moves on its own. Only one arrangement does, and it is bounded. */
|
|
34
|
+
readonly moves: boolean;
|
|
35
|
+
/** Columns, where the arrangement has them. */
|
|
36
|
+
readonly columns: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const GALLERIES: Record<Arrangement, Recipe> = {
|
|
40
|
+
/*
|
|
41
|
+
* The default, and the one to argue against rather than for. Columns of
|
|
42
|
+
* photographs at their own proportions, nothing cropped to a square, nothing
|
|
43
|
+
* moving. A calf is not the shape a forearm is, and a grid of squares takes
|
|
44
|
+
* the tattoo out of half of them.
|
|
45
|
+
*/
|
|
46
|
+
ragged: { what: 'Still columns, every picture its own proportions.', works: [6, 24], moves: false, columns: 3 },
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Volume as the argument. Columns drifting in opposite directions inside a
|
|
50
|
+
* window about two photographs tall, so thirty-seven frames read as one
|
|
51
|
+
* moving surface rather than as a very long list.
|
|
52
|
+
*
|
|
53
|
+
* It is the only arrangement here that moves on its own, and the kit argues
|
|
54
|
+
* against that by default — see the Motion section in the astro-kit skill.
|
|
55
|
+
* What buys the exception is in Gallery.astro, and it is three conditions
|
|
56
|
+
* rather than an opinion.
|
|
57
|
+
*/
|
|
58
|
+
wall: { what: 'Columns drifting in opposite directions inside a window.', works: [18, 200], moves: true, columns: 3 },
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* A contact sheet: uniform cells, small, many at once, deliberately dense.
|
|
62
|
+
* Where the wall says "there is a lot of this" by moving, this says it by
|
|
63
|
+
* fitting eighty things on one screen. Wants work that survives being small —
|
|
64
|
+
* strong silhouettes, not fine line detail.
|
|
65
|
+
*/
|
|
66
|
+
contact: { what: 'Uniform small cells, dense, many at once.', works: [24, 200], moves: false, columns: 6 },
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* One row, scroll-snapped, moved by the visitor. The arrangement for work
|
|
70
|
+
* that has an *order*: a process, a before and an after, a series. It scrolls
|
|
71
|
+
* inside its own container and never takes the page sideways with it.
|
|
72
|
+
*/
|
|
73
|
+
strip: { what: 'One row, snapped, dragged by the visitor.', works: [3, 20], moves: false, columns: 1 },
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* A few, large. One piece per band, given the width it deserves. For a master
|
|
77
|
+
* with six outstanding photographs rather than sixty adequate ones — and the
|
|
78
|
+
* honest answer whenever the feed has been picked over and there is simply
|
|
79
|
+
* not much.
|
|
80
|
+
*/
|
|
81
|
+
spread: { what: 'A few pieces, each given the full width.', works: [3, 10], moves: false, columns: 1 },
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* A list: a small frame, and words beside it. Where what the work *is* —
|
|
85
|
+
* where on the body, how long it took, what it covered — matters as much as
|
|
86
|
+
* how it looks, and where a caption is the argument. Needs captions; without
|
|
87
|
+
* them it is a bad `ragged`.
|
|
88
|
+
*/
|
|
89
|
+
index: { what: 'Rows: a small frame and the words beside it.', works: [4, 30], moves: false, columns: 1 },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const ARRANGEMENTS = Object.keys(GALLERIES) as Arrangement[];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One item. A name is the file in src/assets, the rest is what the page says
|
|
96
|
+
* about it — and `caption` is what `index` exists for.
|
|
97
|
+
*/
|
|
98
|
+
export interface Piece {
|
|
99
|
+
/**
|
|
100
|
+
* A slot in `src/assets`, or a key like `media/<site>/<hash>.jpg` from the
|
|
101
|
+
* content document. `<Shot>` resolves both, so a gallery fed from either
|
|
102
|
+
* looks the same from here — `gallery()` in the copy adapter is what turns a
|
|
103
|
+
* content field into these.
|
|
104
|
+
*/
|
|
105
|
+
readonly name: string;
|
|
106
|
+
readonly caption?: string;
|
|
107
|
+
readonly alt?: string;
|
|
108
|
+
/**
|
|
109
|
+
* `data-jtk-path` for this picture, where the gallery is fed from the content
|
|
110
|
+
* document. Without it the owner can see their work on the page and cannot
|
|
111
|
+
* touch it — `fl-catalogue` fails on exactly that, and a gallery is the
|
|
112
|
+
* longest list on the site to have to fix afterwards.
|
|
113
|
+
*
|
|
114
|
+
* It ends up on the `<img>` rather than on the frame around it, because what
|
|
115
|
+
* the admin does with it is set `src`.
|
|
116
|
+
*/
|
|
117
|
+
readonly path?: string;
|
|
118
|
+
/** `data-jtk-path` for the caption, where the words come from content too. */
|
|
119
|
+
readonly captionPath?: string;
|
|
120
|
+
/** A clip's poster. Galleries do not render clips; `<Clip>` does. */
|
|
121
|
+
readonly poster?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Names alone are the common case; a caption promotes an item to a row. */
|
|
125
|
+
export const asPieces = (items: readonly (string | Piece)[]): Piece[] =>
|
|
126
|
+
items.map((item) => (typeof item === 'string' ? { name: item } : item));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Deal, do not slice.
|
|
130
|
+
*
|
|
131
|
+
* Slicing a date-ordered list into columns puts this year in one column and
|
|
132
|
+
* three years ago in another, so a visitor reading down one column sees one
|
|
133
|
+
* period of the work. Dealing keeps every column mixed in subject and date,
|
|
134
|
+
* which is what makes any column a fair sample of the whole.
|
|
135
|
+
*/
|
|
136
|
+
export const deal = <T,>(items: readonly T[], columns: number): T[][] =>
|
|
137
|
+
Array.from({ length: Math.max(1, columns) }, (_, n) =>
|
|
138
|
+
items.filter((_item, i) => i % Math.max(1, columns) === n),
|
|
139
|
+
);
|