@cardinali-studio/adminkit 2.2.24 → 2.2.33
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 +12 -0
- package/README.md +213 -0
- package/dist/app/app.css +3 -3
- package/dist/app/delivery.d.ts +11 -0
- package/dist/app/delivery.js +46 -0
- package/dist/app/index.d.ts +4 -1
- package/dist/app/index.js +3 -2
- package/dist/app/lib/deliveries.d.ts +8 -0
- package/dist/app/lib/delivery-network.d.ts +25 -0
- package/dist/app/lib/delivery-network.js +159 -0
- package/dist/app/lib/delivery-upload.d.ts +29 -11
- package/dist/app/lib/delivery-upload.js +408 -131
- package/dist/app/lib/upload-queue.d.ts +21 -3
- package/dist/app/lib/upload-queue.js +76 -19
- package/dist/app/routes/Deliveries.js +108 -29
- package/dist/app/routes/UploadDock.js +6 -2
- package/dist/direct-publication.d.ts +20 -0
- package/dist/direct-publication.js +75 -0
- package/dist/functions/_shared/b2.js +22 -3
- package/dist/functions/_shared/delivery-crypto.js +5 -0
- package/dist/functions/_shared/delivery-limits.d.ts +9 -0
- package/dist/functions/_shared/delivery-limits.js +29 -0
- package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
- package/dist/functions/_shared/delivery-manifest.js +392 -0
- package/dist/functions/_shared/delivery-purge.js +17 -1
- package/dist/functions/api/deliveries.js +177 -49
- package/dist/functions/api/delivery/image.js +11 -6
- package/dist/functions/api/delivery/multipart.js +196 -64
- package/dist/functions/delivery-public/assets/client.js +246 -0
- package/dist/functions/delivery-public/assets/delivery.css +297 -0
- package/dist/functions/delivery-public/assets/gallery.js +243 -0
- package/dist/functions/delivery-public/assets/stage.js +107 -0
- package/dist/functions/delivery-public/index.js +637 -0
- package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
- package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
- package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
- package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
- package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
- package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
- package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
- package/dist/functions/index.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +11 -4
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/* Pagina di consegna — /c/<token>
|
|
2
|
+
*
|
|
3
|
+
* Il layout viene dalla delivery di un consumer precedente (due colonne, le fotografie che
|
|
4
|
+
* ruotano a destra, i testi a sinistra, la gallery sotto); l'ASPETTO no. Qui vale il
|
|
5
|
+
* linguaggio del sito. È autonomo: la pagina è servita da una Function e non dipende dal
|
|
6
|
+
* bundle CSS della home.
|
|
7
|
+
* pulsanti -> come .contact-send: squadrati, maiuscoli, spaziati
|
|
8
|
+
* campi -> come .contact-field: nessun riquadro, solo la riga sotto
|
|
9
|
+
* etichette -> maiuscolo .7rem, spaziatura .14em, colore muto
|
|
10
|
+
* movimento -> cubic-bezier(.16,1,.3,1), la stessa curva di .reach-roll
|
|
11
|
+
*
|
|
12
|
+
* NB: `[hidden]` va forzato in cima. Diversi blocchi qui sotto hanno un `display`
|
|
13
|
+
* esplicito, e una classe con display batte l'attributo hidden: è la trappola che sul
|
|
14
|
+
* progetto di riferimento aveva lasciato in pagina il form della password già sbloccato.
|
|
15
|
+
*/
|
|
16
|
+
[hidden] { display: none !important; }
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #0a0a0a;
|
|
20
|
+
--fg: #f1f1ee;
|
|
21
|
+
--muted: #9c9c98;
|
|
22
|
+
--accent: #ff315f;
|
|
23
|
+
--line: #292927;
|
|
24
|
+
--dc-mono: 2rem;
|
|
25
|
+
--dc-pad-x: clamp(1.25rem, 2.4vw, 3rem);
|
|
26
|
+
--dc-edge: 4vw;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
html { color-scheme: dark; -webkit-text-size-adjust: 100%; }
|
|
31
|
+
body.is-delivery {
|
|
32
|
+
margin: 0; overflow-x: hidden; color: var(--fg); background: var(--bg);
|
|
33
|
+
font: 400 1rem/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
34
|
+
}
|
|
35
|
+
a { color: inherit; text-decoration: none; }
|
|
36
|
+
button, input { font: inherit; }
|
|
37
|
+
.dc-brand {
|
|
38
|
+
position: fixed; z-index: 20; left: var(--dc-edge); bottom: 1.25rem;
|
|
39
|
+
font-size: .7rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
|
|
40
|
+
}
|
|
41
|
+
.dc-brand:focus-visible { outline: 1px solid currentColor; outline-offset: 4px; }
|
|
42
|
+
|
|
43
|
+
/* ===== Le due colonne ================================================================ */
|
|
44
|
+
.dc {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: 34% 1fr;
|
|
47
|
+
align-items: start;
|
|
48
|
+
}
|
|
49
|
+
/* Il fondo della colonna è del MONOGRAMMA: `--dc-mono` è il suo ingombro reale e niente
|
|
50
|
+
deve finirci sotto — meno che mai qualcosa da premere, perché il bersaglio del menu
|
|
51
|
+
(#menu-btn, `fixed`, z-index 31) si prenderebbe il click. Per questo qui dentro sta solo
|
|
52
|
+
ciò che serve subito: note d'uso e pulsante finale vivono in .dc-close, in fondo. */
|
|
53
|
+
.dc-info {
|
|
54
|
+
display: flex; flex-direction: column; align-items: flex-start;
|
|
55
|
+
gap: clamp(1rem, 2.2vh, 1.8rem);
|
|
56
|
+
min-height: 100svh;
|
|
57
|
+
padding: clamp(2rem, 6vh, 4rem) var(--dc-pad-x) calc(var(--dc-mono) + 1.5rem) var(--dc-edge);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Consegna protetta: si vede chi è e cos'è — mai una fotografia. Senza immagini le due
|
|
61
|
+
colonne non hanno ragione di esistere, e mezzo schermo vuoto sembrerebbe un errore. */
|
|
62
|
+
body.is-locked .dc { display: block; }
|
|
63
|
+
body.is-locked .dc-stage { display: none; }
|
|
64
|
+
body.is-locked .dc-info { max-width: 46rem; }
|
|
65
|
+
|
|
66
|
+
/* La colonna delle immagini resta ferma mentre scorre quella dei testi: a scorrere è la
|
|
67
|
+
pagina intera, così non ci sono aree di scorrimento annidate (la rotellina sopra le foto
|
|
68
|
+
fa quello che ci si aspetta) e la gallery arriva sotto senza salti. */
|
|
69
|
+
.dc-stage {
|
|
70
|
+
position: sticky; top: 0;
|
|
71
|
+
height: 100svh; padding: clamp(0.75rem, 1.6vw, 1.75rem);
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ===== Testi ========================================================================= */
|
|
76
|
+
.dc-eyebrow, .dc-label {
|
|
77
|
+
margin: 0;
|
|
78
|
+
font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
|
|
79
|
+
}
|
|
80
|
+
.dc-client {
|
|
81
|
+
margin: 0;
|
|
82
|
+
font-weight: 700; line-height: 0.95; letter-spacing: 0.01em; text-transform: uppercase;
|
|
83
|
+
font-size: clamp(2.4rem, 3.6vw, 4.6rem);
|
|
84
|
+
overflow-wrap: anywhere;
|
|
85
|
+
}
|
|
86
|
+
.dc-work {
|
|
87
|
+
margin: 0;
|
|
88
|
+
font-weight: 700; line-height: 1.15; letter-spacing: 0.08em; text-transform: uppercase;
|
|
89
|
+
font-size: clamp(0.85rem, 1.05vw, 1.05rem); color: var(--muted);
|
|
90
|
+
}
|
|
91
|
+
.dc-intro {
|
|
92
|
+
margin: 0; max-width: 46ch;
|
|
93
|
+
font-size: clamp(0.9rem, 1.02vw, 1rem); line-height: 1.62; color: #c9c9c9;
|
|
94
|
+
white-space: pre-line;
|
|
95
|
+
}
|
|
96
|
+
.dc-expiry {
|
|
97
|
+
margin: 0;
|
|
98
|
+
font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
|
|
99
|
+
}
|
|
100
|
+
.dc-expiry.is-soon { color: var(--accent); }
|
|
101
|
+
.dc-hint { margin: 0; font-size: 0.72rem; line-height: 1.5; color: var(--muted); }
|
|
102
|
+
.dc-error { margin: 0; font-size: 0.78rem; line-height: 1.5; color: var(--accent); }
|
|
103
|
+
.dc-notice {
|
|
104
|
+
padding: 0.9rem 1rem; border-left: 2px solid var(--accent);
|
|
105
|
+
background: #131313; font-size: 0.82rem; line-height: 1.55; color: #c9c9c9;
|
|
106
|
+
}
|
|
107
|
+
.dc-notes { display: flex; flex-direction: column; gap: 0.5rem; max-width: 52ch; }
|
|
108
|
+
.dc-notes-body { font-size: 0.82rem; line-height: 1.6; color: var(--muted); white-space: pre-line; }
|
|
109
|
+
|
|
110
|
+
/* La chiusura della pagina: note d'uso e richiesta di recensione, dopo le fotografie.
|
|
111
|
+
Il pulsante va a DESTRA, non sotto le note: a sinistra, in fondo alla pagina, c'è il
|
|
112
|
+
monogramma, e quel che gli finisce sotto non si può premere. */
|
|
113
|
+
.dc-close {
|
|
114
|
+
display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
|
|
115
|
+
gap: clamp(1.5rem, 4vh, 2.5rem);
|
|
116
|
+
padding: clamp(2.5rem, 7vh, 5rem) var(--dc-pad-x) clamp(2.5rem, 6vh, 4rem) var(--dc-edge);
|
|
117
|
+
border-top: 1px solid var(--line);
|
|
118
|
+
}
|
|
119
|
+
.dc-close .dc-btn { margin-left: auto; }
|
|
120
|
+
|
|
121
|
+
/* ===== Pulsanti ====================================================================== */
|
|
122
|
+
.dc-btn {
|
|
123
|
+
display: inline-block; align-self: flex-start;
|
|
124
|
+
font: inherit; font-size: 0.78rem; font-weight: 400;
|
|
125
|
+
letter-spacing: 0.14em; text-transform: uppercase;
|
|
126
|
+
padding: 0.85rem clamp(1.2rem, 2vw, 2rem);
|
|
127
|
+
border: 1px solid transparent; border-radius: 0; cursor: pointer;
|
|
128
|
+
transition: background-color 240ms ease, color 240ms ease, border-color 240ms ease, opacity 240ms ease;
|
|
129
|
+
}
|
|
130
|
+
.dc-btn-solid { background: var(--fg); color: var(--bg); }
|
|
131
|
+
.dc-btn-solid:hover, .dc-btn-solid:focus-visible { background: var(--accent); color: #fff; }
|
|
132
|
+
.dc-btn-line { background: transparent; color: var(--fg); border-color: #3a3a3a; }
|
|
133
|
+
.dc-btn-line:hover, .dc-btn-line:focus-visible { border-color: var(--fg); background: #151515; }
|
|
134
|
+
.dc-btn:focus-visible { outline: 1px solid currentColor; outline-offset: 3px; }
|
|
135
|
+
.dc-btn[disabled] { opacity: 0.5; cursor: default; }
|
|
136
|
+
.dc-btn[disabled]:hover { background: var(--fg); color: var(--bg); }
|
|
137
|
+
.dc-btn-line[disabled]:hover { background: transparent; color: var(--fg); }
|
|
138
|
+
|
|
139
|
+
/* ===== Download ====================================================================== */
|
|
140
|
+
.dc-downloads { display: flex; flex-direction: column; gap: 1.1rem; width: 100%; }
|
|
141
|
+
.dc-pack { display: flex; flex-direction: column; align-items: flex-start; gap: 0.55rem; }
|
|
142
|
+
.dc-pack-name {
|
|
143
|
+
margin: 0;
|
|
144
|
+
font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
|
|
145
|
+
}
|
|
146
|
+
.dc-pack-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
147
|
+
.dc-dl-note { margin: 0; font-size: 0.7rem; letter-spacing: 0.06em; color: var(--muted); }
|
|
148
|
+
|
|
149
|
+
/* ===== Codice di accesso ============================================================= */
|
|
150
|
+
.dc-gate { display: flex; flex-direction: column; gap: 0.6rem; width: 100%; max-width: 24rem; }
|
|
151
|
+
.dc-gate-row { display: flex; gap: 0.5rem; align-items: stretch; }
|
|
152
|
+
/* font-size 16px: sotto, iOS Safari zooma la pagina al focus e la sposta sotto le dita. */
|
|
153
|
+
.dc-gate-input {
|
|
154
|
+
flex: 1 1 auto; min-width: 0;
|
|
155
|
+
font: inherit; font-size: 16px; letter-spacing: 0.16em; text-transform: uppercase;
|
|
156
|
+
color: var(--fg); background: transparent;
|
|
157
|
+
border: 0; border-bottom: 1px solid #3a3a3a; border-radius: 0;
|
|
158
|
+
padding: 0.5rem 0;
|
|
159
|
+
transition: border-color 260ms ease;
|
|
160
|
+
}
|
|
161
|
+
.dc-gate-input:focus { outline: none; border-bottom-color: var(--fg); }
|
|
162
|
+
.dc-gate .dc-btn { flex: 0 0 auto; }
|
|
163
|
+
|
|
164
|
+
/* ===== Palcoscenico ================================================================== */
|
|
165
|
+
/* Le fotografie non vengono MAI ritagliate: `contain` su fondo pieno. Due verticali
|
|
166
|
+
consecutivi vanno affiancati (lo decide stage.js, che conosce le proporzioni). */
|
|
167
|
+
.dc-stage-box { position: relative; width: 100%; height: 100%; }
|
|
168
|
+
.dc-set { position: absolute; inset: 0; display: flex; gap: clamp(0.5rem, 1vw, 1rem); }
|
|
169
|
+
.dc-shot {
|
|
170
|
+
display: block; min-width: 0; height: 100%; object-fit: contain;
|
|
171
|
+
opacity: 0; transform: translate3d(0, 13%, 0) scale(0.985);
|
|
172
|
+
animation: dc-shot-in 0.9s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
173
|
+
will-change: transform, opacity;
|
|
174
|
+
}
|
|
175
|
+
.dc-shot--single { width: 100%; }
|
|
176
|
+
.dc-shot--pair { width: 50%; }
|
|
177
|
+
/* Lo staggered: la seconda foto della coppia entra 140ms dopo la prima. */
|
|
178
|
+
.dc-shot--second { animation-delay: 0.14s; }
|
|
179
|
+
.dc-set.is-out { pointer-events: none; }
|
|
180
|
+
.dc-set.is-out .dc-shot {
|
|
181
|
+
opacity: 1; transform: none;
|
|
182
|
+
animation: dc-shot-out 0.52s cubic-bezier(0.7, 0, 0.84, 0) forwards;
|
|
183
|
+
}
|
|
184
|
+
.dc-set.is-out .dc-shot--second { animation-delay: 0.05s; }
|
|
185
|
+
@keyframes dc-shot-in { to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } }
|
|
186
|
+
@keyframes dc-shot-out { to { opacity: 0; transform: translate3d(0, -8%, 0) scale(1.018); } }
|
|
187
|
+
|
|
188
|
+
/* ===== Gallery ======================================================================= */
|
|
189
|
+
.dc-gallery {
|
|
190
|
+
padding: clamp(3rem, 9vh, 7rem) var(--dc-pad-x) clamp(3rem, 8vh, 6rem) var(--dc-edge);
|
|
191
|
+
display: flex; flex-direction: column; gap: 1.5rem;
|
|
192
|
+
}
|
|
193
|
+
.dc-grid { width: 100%; }
|
|
194
|
+
.dc-row { display: flex; gap: 8px; margin-bottom: 8px; }
|
|
195
|
+
.dc-cell {
|
|
196
|
+
display: block; flex: 0 0 auto; padding: 0; border: 0; border-radius: 0;
|
|
197
|
+
background: #141414; overflow: hidden; cursor: zoom-in;
|
|
198
|
+
}
|
|
199
|
+
.dc-cell img { display: block; width: 100%; height: 100%; object-fit: cover; opacity: 0.86; transition: opacity 420ms ease; }
|
|
200
|
+
.dc-cell:hover img, .dc-cell:focus-visible img { opacity: 1; }
|
|
201
|
+
.dc-cell:focus-visible { outline: 1px solid var(--fg); outline-offset: 2px; }
|
|
202
|
+
|
|
203
|
+
/* ===== Lightbox ====================================================================== */
|
|
204
|
+
.dc-lb {
|
|
205
|
+
width: 100vw; height: 100dvh; max-width: 100vw; max-height: 100dvh;
|
|
206
|
+
margin: 0; padding: 0; border: 0; background: transparent; color: var(--fg); overflow: hidden;
|
|
207
|
+
}
|
|
208
|
+
.dc-lb::backdrop { background: rgba(8, 8, 8, 0.96); }
|
|
209
|
+
.dc-lb[open] {
|
|
210
|
+
display: grid; grid-template-columns: auto 1fr auto; grid-template-rows: 1fr auto;
|
|
211
|
+
align-items: center;
|
|
212
|
+
}
|
|
213
|
+
.dc-lb-stage {
|
|
214
|
+
grid-column: 1 / -1; grid-row: 1 / -1; margin: 0;
|
|
215
|
+
display: flex; align-items: center; justify-content: center;
|
|
216
|
+
padding: clamp(0.75rem, 4vw, 3.5rem);
|
|
217
|
+
}
|
|
218
|
+
.dc-lb-img {
|
|
219
|
+
max-width: 92vw; max-height: 82dvh; width: auto; height: auto; object-fit: contain;
|
|
220
|
+
opacity: 0; animation: dc-lb-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
221
|
+
}
|
|
222
|
+
.dc-lb-btn {
|
|
223
|
+
position: relative; z-index: 2; display: grid; place-items: center;
|
|
224
|
+
border: 1px solid #2e2e2e; background: rgba(20, 20, 20, 0.7); color: var(--fg);
|
|
225
|
+
cursor: pointer; transition: border-color 200ms ease, background-color 200ms ease;
|
|
226
|
+
}
|
|
227
|
+
.dc-lb-btn:hover { border-color: var(--fg); background: #1c1c1c; }
|
|
228
|
+
.dc-lb-btn:focus-visible { outline: 1px solid var(--fg); outline-offset: 2px; }
|
|
229
|
+
.dc-lb-nav {
|
|
230
|
+
grid-row: 1 / -1; align-self: center; margin: clamp(0.5rem, 2vw, 1.5rem);
|
|
231
|
+
width: clamp(2.5rem, 4vw, 3.25rem); height: clamp(2.5rem, 4vw, 3.25rem);
|
|
232
|
+
font-size: 1.4rem; line-height: 1;
|
|
233
|
+
}
|
|
234
|
+
.dc-lb-prev { grid-column: 1; }
|
|
235
|
+
.dc-lb-next { grid-column: 3; }
|
|
236
|
+
.dc-lb-close {
|
|
237
|
+
grid-column: 3; grid-row: 1; align-self: start; justify-self: end; z-index: 3;
|
|
238
|
+
width: 2.5rem; height: 2.5rem; margin: clamp(0.5rem, 2vw, 1.25rem); font-size: 0.9rem;
|
|
239
|
+
}
|
|
240
|
+
.dc-lb-bar {
|
|
241
|
+
grid-column: 1 / -1; grid-row: 2; z-index: 2;
|
|
242
|
+
display: flex; align-items: center; justify-content: center; gap: clamp(1rem, 2vw, 1.75rem);
|
|
243
|
+
padding: 0 1rem clamp(1rem, 3vh, 2rem);
|
|
244
|
+
}
|
|
245
|
+
.dc-lb-count { font-size: 0.72rem; letter-spacing: 0.14em; color: var(--muted); }
|
|
246
|
+
@keyframes dc-lb-in { to { opacity: 1; } }
|
|
247
|
+
|
|
248
|
+
/* ===== Messaggio a tutta pagina ====================================================== */
|
|
249
|
+
.dc-msg {
|
|
250
|
+
min-height: 100svh;
|
|
251
|
+
display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
|
|
252
|
+
gap: 1.1rem;
|
|
253
|
+
padding: clamp(2rem, 6vh, 4rem) var(--dc-pad-x) calc(var(--dc-mono) + 1.5rem) var(--dc-edge);
|
|
254
|
+
max-width: 46rem;
|
|
255
|
+
}
|
|
256
|
+
.dc-msg h1 {
|
|
257
|
+
margin: 0;
|
|
258
|
+
font-weight: 700; line-height: 0.98; letter-spacing: 0.01em; text-transform: uppercase;
|
|
259
|
+
font-size: clamp(2rem, 3.2vw, 3.6rem);
|
|
260
|
+
}
|
|
261
|
+
.dc-msg-body { margin: 0; max-width: 42ch; font-size: 0.95rem; line-height: 1.6; color: var(--muted); }
|
|
262
|
+
.dc-msg .dc-btn { margin-top: 0.75rem; }
|
|
263
|
+
|
|
264
|
+
/* ===== Piede ========================================================================= */
|
|
265
|
+
.dc-foot {
|
|
266
|
+
display: flex; flex-wrap: wrap; gap: 0.6rem clamp(1rem, 3vw, 2.5rem);
|
|
267
|
+
padding: 1.5rem var(--dc-pad-x) calc(var(--dc-mono) + 1rem) var(--dc-edge);
|
|
268
|
+
border-top: 1px solid var(--line);
|
|
269
|
+
font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
|
|
270
|
+
}
|
|
271
|
+
.dc-foot a:hover, .dc-foot a:focus-visible { color: var(--fg); }
|
|
272
|
+
/* Con il messaggio a tutta pagina il piede sarebbe una seconda schermata vuota. */
|
|
273
|
+
body.is-message .dc-foot { display: none; }
|
|
274
|
+
|
|
275
|
+
/* ===== Mobile ======================================================================== */
|
|
276
|
+
@media (max-width: 860px) {
|
|
277
|
+
/* `align-items` torna a `stretch`: quello del desktop è `start`, che serve al `sticky`
|
|
278
|
+
della colonna destra, ma in una colonna flex significa "largo quanto il contenuto" —
|
|
279
|
+
e il palcoscenico, che di contenuto non ne ha finché il JS non monta la prima foto,
|
|
280
|
+
collasserebbe a due padding. */
|
|
281
|
+
.dc { display: flex; flex-direction: column; align-items: stretch; }
|
|
282
|
+
/* Le fotografie per prime: su un telefono sono ciò che si vuole vedere aprendo il link. */
|
|
283
|
+
.dc-stage { order: 1; position: static; height: 54svh; min-height: 17rem; padding: 0.75rem 1rem; }
|
|
284
|
+
.dc-info { order: 2; min-height: 0; padding: 1.75rem 1.25rem calc(var(--dc-mono) + 1.5rem); gap: 2rem; }
|
|
285
|
+
.dc-gallery { padding: 2.5rem 1.25rem 3rem; }
|
|
286
|
+
.dc-shot--pair { width: 100%; }
|
|
287
|
+
.dc-gate-row { flex-direction: column; align-items: stretch; }
|
|
288
|
+
.dc-gate .dc-btn { align-self: stretch; text-align: center; }
|
|
289
|
+
.dc-msg, .dc-foot { padding-left: 1.25rem; padding-right: 1.25rem; }
|
|
290
|
+
.dc-lb-img { max-width: 96vw; max-height: 74dvh; }
|
|
291
|
+
.dc-lb-nav { margin: 0.35rem; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@media (prefers-reduced-motion: reduce) {
|
|
295
|
+
.dc-shot, .dc-set.is-out .dc-shot, .dc-lb-img { animation: none; opacity: 1; transform: none; }
|
|
296
|
+
.dc-btn, .dc-cell img, .dc-lb-btn, .dc-gate-input { transition: none; }
|
|
297
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/* La gallery della consegna: righe giustificate + lightbox.
|
|
2
|
+
*
|
|
3
|
+
* Righe giustificate (stile Flickr/Google Foto) e non colonne fisse: le foto seguono
|
|
4
|
+
* l'ordine in orizzontale, riga per riga, ognuna scalata alla stessa altezza fino a
|
|
5
|
+
* riempire la larghezza. Le proporzioni sono già nel manifest, quindi il calcolo si fa
|
|
6
|
+
* prima di scaricare qualsiasi miniatura — nessun salto di layout.
|
|
7
|
+
*
|
|
8
|
+
* Il download della singola foto passa da una CONVERSIONE in JPEG fatta col canvas. Le
|
|
9
|
+
* derivate sono WebP, e su iPhone «Salva nelle Foto» dallo share sheet con un WebP è
|
|
10
|
+
* inaffidabile; il JPEG si salva ovunque. Le immagini arrivano dalla stessa origine
|
|
11
|
+
* (le serve la Function), quindi il canvas non è contaminato e `toBlob` funziona.
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
"use strict";
|
|
15
|
+
var DC = window.DC;
|
|
16
|
+
var section = document.querySelector("[data-dc-gallery]");
|
|
17
|
+
var grid = document.querySelector("[data-dc-grid]");
|
|
18
|
+
if (!DC || !section || !grid || DC.data.state !== "ready") return;
|
|
19
|
+
|
|
20
|
+
var items = [];
|
|
21
|
+
(DC.data.packages || []).forEach(function (pkg) {
|
|
22
|
+
(pkg.images || []).forEach(function (image) { items.push(image); });
|
|
23
|
+
});
|
|
24
|
+
var pending = (DC.data.packages || []).filter(function (pkg) { return pkg.nextCursor != null; });
|
|
25
|
+
if (!items.length && !pending.length) return;
|
|
26
|
+
var more = document.createElement("button");
|
|
27
|
+
more.type = "button"; more.className = "dc-btn dc-btn-line";
|
|
28
|
+
more.textContent = "Mostra altre foto"; more.hidden = !pending.length;
|
|
29
|
+
section.appendChild(more);
|
|
30
|
+
more.addEventListener("click", async function () {
|
|
31
|
+
more.disabled = true; more.textContent = "Caricamento…";
|
|
32
|
+
try {
|
|
33
|
+
var pkg = pending[0];
|
|
34
|
+
var base = location.pathname.replace(/\/$/, "");
|
|
35
|
+
var response = await fetch(base + "/images/" + encodeURIComponent(pkg.id) + "?after=" + pkg.nextCursor, { credentials: "same-origin" });
|
|
36
|
+
var page = await response.json();
|
|
37
|
+
if (!response.ok || !page.ok) throw new Error(page.error || "Caricamento non riuscito");
|
|
38
|
+
items.push.apply(items, page.images);
|
|
39
|
+
pkg.nextCursor = page.nextCursor;
|
|
40
|
+
if (page.nextCursor == null) pending.shift();
|
|
41
|
+
layout(grid.clientWidth);
|
|
42
|
+
more.hidden = !pending.length;
|
|
43
|
+
more.textContent = "Mostra altre foto";
|
|
44
|
+
} catch (_) { more.textContent = "Riprova a caricare le foto"; }
|
|
45
|
+
finally { more.disabled = false; }
|
|
46
|
+
});
|
|
47
|
+
section.hidden = false;
|
|
48
|
+
|
|
49
|
+
var reduce = matchMedia("(prefers-reduced-motion: reduce)");
|
|
50
|
+
var GAP = 8;
|
|
51
|
+
|
|
52
|
+
/* Il calcolo parte dalla larghezza della griglia, quindi va rifatto quando quella cambia
|
|
53
|
+
— e SOLO allora: le righe cambiano l'altezza della griglia, non la sua larghezza, così
|
|
54
|
+
l'osservatore non si rincorre da solo. Serve anche al primo giro: se al momento del
|
|
55
|
+
montaggio la larghezza è ancora 0 (succede in una scheda mai dipinta, o dentro un
|
|
56
|
+
pannello di anteprima), senza questo la gallery resterebbe vuota per sempre. */
|
|
57
|
+
var lastWidth = -1;
|
|
58
|
+
var relayout = function () {
|
|
59
|
+
var width = grid.clientWidth;
|
|
60
|
+
if (width === lastWidth) return;
|
|
61
|
+
lastWidth = width;
|
|
62
|
+
layout(width);
|
|
63
|
+
};
|
|
64
|
+
relayout();
|
|
65
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(relayout).observe(grid);
|
|
66
|
+
var resizeTimer;
|
|
67
|
+
addEventListener("resize", function () {
|
|
68
|
+
clearTimeout(resizeTimer);
|
|
69
|
+
resizeTimer = setTimeout(relayout, 150);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function rowHeight() {
|
|
73
|
+
var w = innerWidth;
|
|
74
|
+
return w < 700 ? 150 : (w < 1100 ? 210 : 270);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function layout(width) {
|
|
78
|
+
if (!width) return;
|
|
79
|
+
var target = rowHeight();
|
|
80
|
+
var frag = document.createDocumentFragment();
|
|
81
|
+
var row = [], sum = 0;
|
|
82
|
+
|
|
83
|
+
var flush = function (last) {
|
|
84
|
+
if (!row.length) return;
|
|
85
|
+
var h = last ? Math.min(target, (width - (row.length - 1) * GAP) / sum)
|
|
86
|
+
: (width - (row.length - 1) * GAP) / sum;
|
|
87
|
+
var node = document.createElement("div");
|
|
88
|
+
node.className = "dc-row";
|
|
89
|
+
row.forEach(function (entry) {
|
|
90
|
+
node.appendChild(cell(entry, h));
|
|
91
|
+
});
|
|
92
|
+
frag.appendChild(node);
|
|
93
|
+
row = []; sum = 0;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
items.forEach(function (item, index) {
|
|
97
|
+
var ratio = (item.width || 3) / (item.height || 2);
|
|
98
|
+
row.push({ item: item, ratio: ratio, index: index });
|
|
99
|
+
sum += ratio;
|
|
100
|
+
if (sum * target + (row.length - 1) * GAP >= width) flush(false);
|
|
101
|
+
});
|
|
102
|
+
flush(true);
|
|
103
|
+
|
|
104
|
+
grid.textContent = "";
|
|
105
|
+
grid.appendChild(frag);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cell(entry, height) {
|
|
109
|
+
var node = document.createElement("button");
|
|
110
|
+
node.type = "button";
|
|
111
|
+
node.className = "dc-cell";
|
|
112
|
+
node.style.width = Math.round(entry.ratio * height) + "px";
|
|
113
|
+
node.style.height = Math.round(height) + "px";
|
|
114
|
+
node.setAttribute("aria-label", "Apri la fotografia " + (entry.index + 1) + " di " + items.length);
|
|
115
|
+
var image = new Image();
|
|
116
|
+
image.className = "dc-thumb";
|
|
117
|
+
image.src = entry.item.thumb;
|
|
118
|
+
image.alt = "";
|
|
119
|
+
image.loading = "lazy";
|
|
120
|
+
image.decoding = "async";
|
|
121
|
+
node.appendChild(image);
|
|
122
|
+
node.addEventListener("click", function () { open(entry.index); });
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---- lightbox ---------------------------------------------------------------------- */
|
|
127
|
+
var dialog, view, counter, saver, at = 0;
|
|
128
|
+
|
|
129
|
+
function build() {
|
|
130
|
+
dialog = document.createElement("dialog");
|
|
131
|
+
dialog.className = "dc-lb";
|
|
132
|
+
dialog.innerHTML =
|
|
133
|
+
'<button type="button" class="dc-lb-btn dc-lb-close" aria-label="Chiudi">✕</button>' +
|
|
134
|
+
'<button type="button" class="dc-lb-btn dc-lb-nav dc-lb-prev" aria-label="Fotografia precedente">‹</button>' +
|
|
135
|
+
'<figure class="dc-lb-stage"><img class="dc-lb-img" alt=""></figure>' +
|
|
136
|
+
'<button type="button" class="dc-lb-btn dc-lb-nav dc-lb-next" aria-label="Fotografia successiva">›</button>' +
|
|
137
|
+
'<div class="dc-lb-bar"><span class="dc-lb-count" aria-live="polite"></span>' +
|
|
138
|
+
'<button type="button" class="dc-btn dc-btn-solid dc-lb-dl">Salva questa foto</button></div>';
|
|
139
|
+
document.body.appendChild(dialog);
|
|
140
|
+
view = dialog.querySelector(".dc-lb-img");
|
|
141
|
+
counter = dialog.querySelector(".dc-lb-count");
|
|
142
|
+
saver = dialog.querySelector(".dc-lb-dl");
|
|
143
|
+
|
|
144
|
+
dialog.querySelector(".dc-lb-close").addEventListener("click", function () { dialog.close(); });
|
|
145
|
+
dialog.querySelector(".dc-lb-prev").addEventListener("click", function () { go(-1); });
|
|
146
|
+
dialog.querySelector(".dc-lb-next").addEventListener("click", function () { go(1); });
|
|
147
|
+
saver.addEventListener("click", save);
|
|
148
|
+
// Click sullo sfondo (fuori dalla figura) = chiudi.
|
|
149
|
+
dialog.addEventListener("click", function (event) { if (event.target === dialog) dialog.close(); });
|
|
150
|
+
dialog.addEventListener("keydown", function (event) {
|
|
151
|
+
if (event.key === "ArrowLeft") { event.preventDefault(); go(-1); }
|
|
152
|
+
else if (event.key === "ArrowRight") { event.preventDefault(); go(1); }
|
|
153
|
+
});
|
|
154
|
+
var from = null;
|
|
155
|
+
dialog.addEventListener("touchstart", function (e) { from = e.touches[0].clientX; }, { passive: true });
|
|
156
|
+
dialog.addEventListener("touchend", function (e) {
|
|
157
|
+
if (from === null) return;
|
|
158
|
+
var dx = e.changedTouches[0].clientX - from; from = null;
|
|
159
|
+
if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1);
|
|
160
|
+
}, { passive: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function open(index) {
|
|
164
|
+
if (!dialog) build();
|
|
165
|
+
at = index;
|
|
166
|
+
paint();
|
|
167
|
+
if (typeof dialog.showModal === "function") dialog.showModal();
|
|
168
|
+
else dialog.setAttribute("open", "");
|
|
169
|
+
}
|
|
170
|
+
function go(step) {
|
|
171
|
+
if (items.length < 2) return;
|
|
172
|
+
at = (at + step + items.length) % items.length;
|
|
173
|
+
paint();
|
|
174
|
+
}
|
|
175
|
+
function paint() {
|
|
176
|
+
var item = items[at];
|
|
177
|
+
if (!reduce.matches) { view.style.animation = "none"; view.offsetWidth; view.style.animation = ""; }
|
|
178
|
+
view.src = item.display;
|
|
179
|
+
view.alt = item.name || "";
|
|
180
|
+
counter.textContent = (at + 1) + " / " + items.length;
|
|
181
|
+
prepare(at);
|
|
182
|
+
[items[at + 1], items[at - 1]].forEach(function (near) {
|
|
183
|
+
if (near) { var pre = new Image(); pre.src = near.display; }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Conversione in JPEG, tenuta in cache per foto: al click lo share sheet deve partire
|
|
188
|
+
nello stesso gesto dell'utente, altrimenti iOS lo blocca. */
|
|
189
|
+
var jpegs = {};
|
|
190
|
+
function prepare(index) {
|
|
191
|
+
if (!(index in jpegs)) jpegs[index] = toJpeg(items[index].display);
|
|
192
|
+
return jpegs[index];
|
|
193
|
+
}
|
|
194
|
+
function toJpeg(url) {
|
|
195
|
+
return new Promise(function (resolve) {
|
|
196
|
+
var image = new Image();
|
|
197
|
+
image.decoding = "async";
|
|
198
|
+
image.onload = function () {
|
|
199
|
+
try {
|
|
200
|
+
var canvas = document.createElement("canvas");
|
|
201
|
+
canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
|
|
202
|
+
var ctx = canvas.getContext("2d");
|
|
203
|
+
ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, canvas.width, canvas.height); // il JPEG non ha alpha
|
|
204
|
+
ctx.drawImage(image, 0, 0);
|
|
205
|
+
canvas.toBlob(function (blob) { resolve(blob); }, "image/jpeg", 0.92);
|
|
206
|
+
} catch (e) { resolve(null); }
|
|
207
|
+
};
|
|
208
|
+
image.onerror = function () { resolve(null); };
|
|
209
|
+
image.src = url;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function baseName(item) { return String(item.name || "foto").split("/").pop().replace(/\.[^.]+$/, ""); }
|
|
213
|
+
function push(blob, name) {
|
|
214
|
+
var url = URL.createObjectURL(blob);
|
|
215
|
+
var link = document.createElement("a");
|
|
216
|
+
link.href = url; link.download = name;
|
|
217
|
+
document.body.appendChild(link); link.click(); link.remove();
|
|
218
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 15000);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function save() {
|
|
222
|
+
var index = at, item = items[index], label = saver.textContent;
|
|
223
|
+
saver.disabled = true; saver.textContent = "Preparo…";
|
|
224
|
+
var done = function () { saver.disabled = false; saver.textContent = label; };
|
|
225
|
+
prepare(index).then(function (jpeg) {
|
|
226
|
+
if (jpeg) return { blob: jpeg, name: baseName(item) + ".jpg" };
|
|
227
|
+
return fetch(item.display, { credentials: "same-origin" })
|
|
228
|
+
.then(function (res) { return res.blob(); })
|
|
229
|
+
.then(function (blob) { return { blob: blob, name: baseName(item) + ".webp" }; });
|
|
230
|
+
}).then(function (out) {
|
|
231
|
+
var file = new File([out.blob], out.name, { type: out.blob.type });
|
|
232
|
+
var touch = matchMedia("(pointer: coarse)").matches;
|
|
233
|
+
if (touch && navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
234
|
+
return navigator.share({ files: [file] }).then(function () { return true; }, function (error) {
|
|
235
|
+
return !!(error && error.name === "AbortError"); // annullato dall'utente: sta bene così
|
|
236
|
+
}).then(function (handled) { if (!handled) push(out.blob, out.name); });
|
|
237
|
+
}
|
|
238
|
+
push(out.blob, out.name);
|
|
239
|
+
}).catch(function () {
|
|
240
|
+
location.href = item.download;
|
|
241
|
+
}).then(done, done);
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* Il palcoscenico: la colonna destra della pagina di consegna.
|
|
2
|
+
*
|
|
3
|
+
* Le fotografie si alternano da sole, una (o due) per volta. Le regole vengono dalla
|
|
4
|
+
* delivery di un consumer precedente, e sono tre:
|
|
5
|
+
* 1. mai un ritaglio — `object-fit: contain` su fondo pieno, la foto è il soggetto;
|
|
6
|
+
* 2. due VERTICALI consecutivi stanno affiancati, e il secondo entra 140ms dopo il primo
|
|
7
|
+
* (è lo staggered: l'animazione la fa il CSS, qui si decide solo chi sta con chi);
|
|
8
|
+
* 3. mai una dissolvenza incrociata — il set entrante si monta solo QUANDO il precedente
|
|
9
|
+
* è finito di uscire, altrimenti per mezzo secondo si vedono due foto sovrapposte.
|
|
10
|
+
*
|
|
11
|
+
* Le proporzioni si conoscono solo a immagine caricata, quindi si carica prima la prima e
|
|
12
|
+
* si mostra subito quella; le altre arrivano dopo e il raggruppamento si rifà.
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
"use strict";
|
|
16
|
+
var DC = window.DC;
|
|
17
|
+
var stage = document.querySelector("[data-dc-stage]");
|
|
18
|
+
if (!DC || !stage || DC.data.state !== "ready") return;
|
|
19
|
+
var urls = (DC.data.heroes || []).filter(Boolean);
|
|
20
|
+
if (!urls.length) return;
|
|
21
|
+
|
|
22
|
+
var box = document.createElement("div");
|
|
23
|
+
box.className = "dc-stage-box";
|
|
24
|
+
stage.appendChild(box);
|
|
25
|
+
|
|
26
|
+
var reduce = matchMedia("(prefers-reduced-motion: reduce)");
|
|
27
|
+
var narrow = matchMedia("(max-width: 860px)");
|
|
28
|
+
var loaded = [], groups = [], at = 0, timer = 0, swapping = 0;
|
|
29
|
+
|
|
30
|
+
load(urls[0]).then(function (first) {
|
|
31
|
+
if (!first) return;
|
|
32
|
+
loaded = [first];
|
|
33
|
+
rebuild();
|
|
34
|
+
// Le altre in coda, in ordine: la prima schermata non aspetta il download di otto foto.
|
|
35
|
+
var rest = urls.slice(1);
|
|
36
|
+
return rest.reduce(function (chain, url) {
|
|
37
|
+
return chain.then(function () {
|
|
38
|
+
return load(url).then(function (image) { if (image) loaded.push(image); });
|
|
39
|
+
});
|
|
40
|
+
}, Promise.resolve()).then(function () {
|
|
41
|
+
if (loaded.length > 1) { rebuild(); play(); }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
narrow.addEventListener("change", rebuild);
|
|
46
|
+
reduce.addEventListener("change", play);
|
|
47
|
+
document.addEventListener("visibilitychange", function () { document.hidden ? stop() : play(); });
|
|
48
|
+
|
|
49
|
+
function load(src) {
|
|
50
|
+
return new Promise(function (resolve) {
|
|
51
|
+
var image = new Image();
|
|
52
|
+
image.decoding = "async";
|
|
53
|
+
image.onload = function () { resolve({ src: src, w: image.naturalWidth, h: image.naturalHeight }); };
|
|
54
|
+
image.onerror = function () { resolve(null); };
|
|
55
|
+
image.src = src;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function makeGroups() {
|
|
60
|
+
var out = [];
|
|
61
|
+
for (var i = 0; i < loaded.length; i += 1) {
|
|
62
|
+
var one = loaded[i], next = loaded[i + 1];
|
|
63
|
+
var pair = !narrow.matches && one.h > one.w && next && next.h > next.w;
|
|
64
|
+
if (pair) { out.push([one, next]); i += 1; }
|
|
65
|
+
else out.push([one]);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rebuild() {
|
|
71
|
+
groups = makeGroups();
|
|
72
|
+
if (at >= groups.length) at = 0;
|
|
73
|
+
render();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function render() {
|
|
77
|
+
var group = groups[at];
|
|
78
|
+
if (!group) return;
|
|
79
|
+
var out = box.querySelector(".dc-set:not(.is-out)");
|
|
80
|
+
var set = document.createElement("div");
|
|
81
|
+
set.className = "dc-set";
|
|
82
|
+
group.forEach(function (item, index) {
|
|
83
|
+
var image = new Image();
|
|
84
|
+
image.src = item.src;
|
|
85
|
+
image.alt = "";
|
|
86
|
+
image.className = "dc-shot " + (group.length === 1 ? "dc-shot--single" : "dc-shot--pair") +
|
|
87
|
+
(index ? " dc-shot--second" : "");
|
|
88
|
+
set.appendChild(image);
|
|
89
|
+
});
|
|
90
|
+
if (!out) { box.appendChild(set); return; }
|
|
91
|
+
if (reduce.matches) { out.remove(); box.appendChild(set); return; }
|
|
92
|
+
out.classList.add("is-out");
|
|
93
|
+
clearTimeout(swapping);
|
|
94
|
+
swapping = setTimeout(function () { out.remove(); box.appendChild(set); }, 540);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stop() { if (timer) { clearInterval(timer); timer = 0; } }
|
|
98
|
+
function play() {
|
|
99
|
+
stop();
|
|
100
|
+
if (groups.length < 2 || reduce.matches) return;
|
|
101
|
+
timer = setInterval(function () {
|
|
102
|
+
if (document.hidden) return;
|
|
103
|
+
at = (at + 1) % groups.length;
|
|
104
|
+
render();
|
|
105
|
+
}, 3000);
|
|
106
|
+
}
|
|
107
|
+
})();
|