docusaurus-plugin-album 0.0.0-stage → 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.
@@ -0,0 +1,47 @@
1
+ import clsx from 'clsx';
2
+ import { memo } from 'react';
3
+ import styles from './styles.module.css';
4
+
5
+ type Props = {
6
+ years: string[];
7
+ activeYear: string;
8
+ activeMonth: string;
9
+ visible: boolean;
10
+ onSelectYear: (year: string) => void;
11
+ };
12
+
13
+ function TimelineScrubber({ years, activeYear, activeMonth, visible, onSelectYear }: Props) {
14
+ if (years.length === 0) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <nav
20
+ className={clsx(styles.scrubberContainer, visible && styles.scrubberVisible)}
21
+ aria-label="相簿時間軸"
22
+ >
23
+ {/* Floating active date bubble */}
24
+ <div className={styles.timelineBubble} aria-live="polite">
25
+ {activeYear ? `${activeYear}年 ${activeMonth ? `${activeMonth}月` : ''}` : '探索相簿'}
26
+ </div>
27
+
28
+ {/* Rail with clickable year ticks */}
29
+ <div className={styles.timelineRail}>
30
+ {years.map((year) => (
31
+ <button
32
+ key={year}
33
+ type="button"
34
+ className={clsx(styles.yearTick, year === activeYear && styles.yearTickActive)}
35
+ onClick={() => onSelectYear(year)}
36
+ title={`跳轉至 ${year} 年`}
37
+ aria-label={`${year} 年`}
38
+ >
39
+ {year}
40
+ </button>
41
+ ))}
42
+ </div>
43
+ </nav>
44
+ );
45
+ }
46
+
47
+ export default memo(TimelineScrubber);
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Album Blog Styles (Pinterest Masonry + Google Photos Timeline + Lightbox)
3
+ */
4
+
5
+ /* ============================================
6
+ Album List Page (Header & Container)
7
+ ============================================ */
8
+ .albumContainer {
9
+ max-width: 1440px;
10
+ margin: 0 auto;
11
+ padding: 2rem 1.5rem 4rem;
12
+ position: relative;
13
+ }
14
+
15
+ .albumHeader {
16
+ text-align: center;
17
+ margin-bottom: 2.5rem;
18
+ }
19
+
20
+ .albumTitle {
21
+ font-size: 2.25rem;
22
+ font-weight: 800;
23
+ letter-spacing: -0.02em;
24
+ margin-bottom: 0.5rem;
25
+ background: linear-gradient(
26
+ 135deg,
27
+ var(--ifm-color-primary) 0%,
28
+ var(--ifm-color-primary-dark) 100%
29
+ );
30
+ -webkit-background-clip: text;
31
+ -webkit-text-fill-color: transparent;
32
+ background-clip: text;
33
+ }
34
+
35
+ .albumDescription {
36
+ font-size: 1.05rem;
37
+ color: var(--ifm-color-emphasis-700);
38
+ max-width: 600px;
39
+ margin: 0 auto;
40
+ }
41
+
42
+ /* ============================================
43
+ Pinterest Masonry Grid
44
+ ============================================ */
45
+ .masonryGrid {
46
+ display: flex;
47
+ gap: 1.5rem;
48
+ align-items: flex-start;
49
+ width: 100%;
50
+ }
51
+
52
+ .masonryColumn {
53
+ flex: 1;
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 1.5rem;
57
+ min-width: 0;
58
+ }
59
+
60
+ /* ============================================
61
+ Album Card
62
+ ============================================ */
63
+ .albumCard,
64
+ .albumCard:hover {
65
+ text-decoration: none;
66
+ color: inherit;
67
+ }
68
+
69
+ .albumCard {
70
+ border-radius: 16px;
71
+ overflow: hidden;
72
+ position: relative;
73
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
74
+ transition:
75
+ transform var(--transition-base, 250ms),
76
+ box-shadow var(--transition-base, 250ms);
77
+ display: block;
78
+ background: rgba(230, 126, 34, 0.06);
79
+ }
80
+
81
+ [data-theme="dark"] .albumCard {
82
+ background: rgba(245, 176, 65, 0.08);
83
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
84
+ }
85
+
86
+ .albumCard:hover {
87
+ transform: translateY(-6px);
88
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
89
+ }
90
+
91
+ [data-theme="dark"] .albumCard:hover {
92
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5);
93
+ }
94
+
95
+ .cardImageWrapper {
96
+ position: relative;
97
+ width: 100%;
98
+ min-height: 180px;
99
+ border-radius: 16px;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .cardImage {
104
+ width: 100%;
105
+ height: auto;
106
+ display: block;
107
+ object-fit: cover;
108
+ transition: transform var(--transition-slow, 350ms);
109
+ }
110
+
111
+ .albumCard:hover .cardImage {
112
+ transform: scale(1.05);
113
+ }
114
+
115
+ /* ============================================
116
+ Pinterest Hover Overlay
117
+ ============================================ */
118
+ .cardOverlay {
119
+ position: absolute;
120
+ inset: 0;
121
+ display: flex;
122
+ flex-direction: column;
123
+ justify-content: flex-end;
124
+ padding: 1.25rem 1.1rem;
125
+ background: linear-gradient(
126
+ to top,
127
+ rgba(0, 0, 0, 0.88) 0%,
128
+ rgba(0, 0, 0, 0.45) 55%,
129
+ transparent 100%
130
+ );
131
+ opacity: 0;
132
+ transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
133
+ border-radius: 16px;
134
+ pointer-events: none;
135
+ }
136
+
137
+ .albumCard:hover .cardOverlay,
138
+ .albumCard:focus .cardOverlay {
139
+ opacity: 1;
140
+ }
141
+
142
+ /* Touchscreens: subtly always show bottom info */
143
+ @media (hover: none) {
144
+ .cardOverlay {
145
+ opacity: 1;
146
+ background: linear-gradient(
147
+ to top,
148
+ rgba(0, 0, 0, 0.8) 0%,
149
+ rgba(0, 0, 0, 0.3) 50%,
150
+ transparent 100%
151
+ );
152
+ }
153
+ }
154
+
155
+ .cardOverlayContent {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 0.4rem;
159
+ }
160
+
161
+ .cardTitle {
162
+ font-size: 1.1rem;
163
+ font-weight: 700;
164
+ line-height: 1.35;
165
+ margin: 0;
166
+ color: #ffffff;
167
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
168
+ }
169
+
170
+ .cardMetaRow {
171
+ display: flex;
172
+ align-items: center;
173
+ flex-wrap: wrap;
174
+ gap: 0.45rem;
175
+ }
176
+
177
+ .pillBadge {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ gap: 0.25rem;
181
+ padding: 0.15rem 0.55rem;
182
+ border-radius: 999px;
183
+ font-size: 0.725rem;
184
+ font-weight: 500;
185
+ background: rgba(255, 255, 255, 0.22);
186
+ color: #ffffff;
187
+ backdrop-filter: blur(8px);
188
+ border: 1px solid rgba(255, 255, 255, 0.25);
189
+ }
190
+
191
+ .seriesBadge {
192
+ background: rgba(230, 126, 34, 0.55);
193
+ border-color: rgba(245, 176, 65, 0.5);
194
+ }
195
+
196
+ .cardDate {
197
+ font-size: 0.75rem;
198
+ color: rgba(255, 255, 255, 0.85);
199
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
200
+ }
201
+
202
+ /* ============================================
203
+ Sentinel & End of list
204
+ ============================================ */
205
+ .sentinel {
206
+ height: 40px;
207
+ width: 100%;
208
+ }
209
+
210
+ .endMessage {
211
+ text-align: center;
212
+ padding: 2.5rem 0 1rem;
213
+ color: var(--ifm-color-emphasis-500);
214
+ font-size: 0.95rem;
215
+ }
216
+
217
+ /* ============================================
218
+ Google Photos Style Timeline Scrubber
219
+ ============================================ */
220
+ .scrubberContainer {
221
+ position: fixed;
222
+ right: 1.25rem;
223
+ top: 50%;
224
+ transform: translateY(-50%);
225
+ z-index: 90;
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 0.75rem;
229
+ pointer-events: auto;
230
+ opacity: 0;
231
+ visibility: hidden;
232
+ transition:
233
+ opacity 0.25s ease,
234
+ visibility 0.25s ease;
235
+ }
236
+
237
+ .scrubberVisible {
238
+ opacity: 1;
239
+ visibility: visible;
240
+ }
241
+
242
+ .timelineBubble {
243
+ background: rgba(45, 35, 25, 0.88);
244
+ color: #ffffff;
245
+ padding: 0.4rem 0.85rem;
246
+ border-radius: 20px;
247
+ font-size: 0.85rem;
248
+ font-weight: 600;
249
+ letter-spacing: 0.02em;
250
+ white-space: nowrap;
251
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
252
+ backdrop-filter: blur(8px);
253
+ border: 1px solid rgba(255, 255, 255, 0.15);
254
+ }
255
+
256
+ [data-theme="dark"] .timelineBubble {
257
+ background: rgba(26, 21, 18, 0.9);
258
+ color: #fad7a0;
259
+ border-color: rgba(245, 176, 65, 0.2);
260
+ }
261
+
262
+ .timelineRail {
263
+ background: rgba(255, 255, 255, 0.75);
264
+ backdrop-filter: blur(10px);
265
+ border: 1px solid rgba(230, 126, 34, 0.2);
266
+ border-radius: 999px;
267
+ padding: 0.5rem 0.25rem;
268
+ display: flex;
269
+ flex-direction: column;
270
+ align-items: center;
271
+ gap: 0.4rem;
272
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
273
+ }
274
+
275
+ [data-theme="dark"] .timelineRail {
276
+ background: rgba(35, 28, 23, 0.85);
277
+ border-color: rgba(245, 176, 65, 0.2);
278
+ }
279
+
280
+ .yearTick {
281
+ background: transparent;
282
+ border: none;
283
+ cursor: pointer;
284
+ padding: 0.25rem 0.4rem;
285
+ font-size: 0.75rem;
286
+ font-weight: 700;
287
+ color: var(--ifm-color-emphasis-600);
288
+ border-radius: 999px;
289
+ transition:
290
+ transform var(--transition-fast, 150ms),
291
+ background-color var(--transition-fast, 150ms),
292
+ color var(--transition-fast, 150ms);
293
+ }
294
+
295
+ .yearTick:hover,
296
+ .yearTickActive {
297
+ background: var(--ifm-color-primary);
298
+ color: #ffffff;
299
+ transform: scale(1.1);
300
+ }
301
+
302
+ @media (max-width: 768px) {
303
+ .scrubberContainer {
304
+ right: 0.5rem;
305
+ }
306
+ .yearTick {
307
+ font-size: 0.7rem;
308
+ padding: 0.2rem 0.3rem;
309
+ }
310
+ }
311
+
312
+ /* ============================================
313
+ Album Post Detail (Hero Header)
314
+ ============================================ */
315
+ .postHero {
316
+ margin-bottom: 2.5rem;
317
+ border-radius: 20px;
318
+ overflow: hidden;
319
+ background: var(--ifm-card-background-color, #ffffff);
320
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
321
+ border: 1px solid rgba(230, 126, 34, 0.12);
322
+ }
323
+
324
+ [data-theme="dark"] .postHero {
325
+ background: #231c17;
326
+ border-color: rgba(245, 176, 65, 0.12);
327
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
328
+ }
329
+
330
+ .heroImageContainer {
331
+ position: relative;
332
+ width: 100%;
333
+ max-height: 580px;
334
+ overflow: hidden;
335
+ cursor: zoom-in;
336
+ }
337
+
338
+ .heroCoverImage {
339
+ width: 100%;
340
+ height: 100%;
341
+ object-fit: cover;
342
+ display: block;
343
+ transition: transform var(--transition-slow, 350ms);
344
+ }
345
+
346
+ .heroImageContainer:hover .heroCoverImage {
347
+ transform: scale(1.02);
348
+ }
349
+
350
+ .heroCaption {
351
+ position: absolute;
352
+ bottom: 0;
353
+ left: 0;
354
+ right: 0;
355
+ padding: 1.5rem 1.5rem 0.8rem;
356
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, transparent 100%);
357
+ color: #ffffff;
358
+ font-size: 0.95rem;
359
+ font-style: italic;
360
+ letter-spacing: 0.02em;
361
+ }
362
+
363
+ .heroMetaBody {
364
+ padding: 1.5rem 1.75rem;
365
+ display: flex;
366
+ flex-direction: column;
367
+ gap: 0.75rem;
368
+ }
369
+
370
+ .heroTitle {
371
+ font-size: 2rem;
372
+ font-weight: 800;
373
+ margin: 0;
374
+ line-height: 1.3;
375
+ }
376
+
377
+ .heroBadgesRow {
378
+ display: flex;
379
+ align-items: center;
380
+ flex-wrap: wrap;
381
+ gap: 0.6rem;
382
+ }
383
+
384
+ .heroBadge {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ gap: 0.35rem;
388
+ padding: 0.25rem 0.75rem;
389
+ border-radius: 999px;
390
+ font-size: 0.85rem;
391
+ font-weight: 600;
392
+ }
393
+
394
+ .locationBadge {
395
+ background: rgba(230, 126, 34, 0.12);
396
+ color: var(--ifm-color-primary-dark);
397
+ }
398
+
399
+ [data-theme="dark"] .locationBadge {
400
+ background: rgba(245, 176, 65, 0.15);
401
+ color: var(--ifm-color-primary-light);
402
+ }
403
+
404
+ .heroDateBadge {
405
+ background: var(--ifm-color-emphasis-100);
406
+ color: var(--ifm-color-emphasis-700);
407
+ }
408
+
409
+ .albumPostArticle :global(.markdown) img:not(:global(.avatar__photo)),
410
+ .albumPostArticle img[data-lightbox-index] {
411
+ cursor: zoom-in;
412
+ }
413
+
414
+ /* ============================================
415
+ Image Lightbox Modal
416
+ ============================================ */
417
+ .lightboxOverlay {
418
+ position: fixed;
419
+ inset: 0;
420
+ z-index: 9999;
421
+ background: rgba(0, 0, 0, 0.92);
422
+ backdrop-filter: blur(12px);
423
+ display: flex;
424
+ align-items: center;
425
+ justify-content: center;
426
+ animation: fadeIn 0.2s ease-out;
427
+ touch-action: none;
428
+ }
429
+
430
+ @keyframes fadeIn {
431
+ from {
432
+ opacity: 0;
433
+ }
434
+ to {
435
+ opacity: 1;
436
+ }
437
+ }
438
+
439
+ .lightboxContent {
440
+ position: relative;
441
+ max-width: 92vw;
442
+ max-height: 90vh;
443
+ display: flex;
444
+ flex-direction: column;
445
+ align-items: center;
446
+ justify-content: center;
447
+ }
448
+
449
+ .lightboxImage {
450
+ max-width: 90vw;
451
+ max-height: 82vh;
452
+ object-fit: contain;
453
+ border-radius: 8px;
454
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
455
+ user-select: none;
456
+ }
457
+
458
+ .lightboxCaption {
459
+ color: #e0e0e0;
460
+ margin-top: 0.75rem;
461
+ font-size: 0.95rem;
462
+ text-align: center;
463
+ max-width: 80vw;
464
+ }
465
+
466
+ .lightboxCounter {
467
+ position: absolute;
468
+ top: 1.5rem;
469
+ left: 2rem;
470
+ color: #fad7a0;
471
+ font-size: 0.95rem;
472
+ font-weight: 600;
473
+ letter-spacing: 0.05em;
474
+ background: rgba(255, 255, 255, 0.1);
475
+ padding: 0.25rem 0.75rem;
476
+ border-radius: 999px;
477
+ backdrop-filter: blur(8px);
478
+ }
479
+
480
+ .lightboxCloseBtn {
481
+ position: absolute;
482
+ top: 1.5rem;
483
+ right: 2rem;
484
+ background: rgba(255, 255, 255, 0.15);
485
+ border: none;
486
+ color: #ffffff;
487
+ width: 42px;
488
+ height: 42px;
489
+ border-radius: 50%;
490
+ cursor: pointer;
491
+ display: flex;
492
+ align-items: center;
493
+ justify-content: center;
494
+ font-size: 1.25rem;
495
+ transition:
496
+ transform 0.2s ease,
497
+ background-color 0.2s ease;
498
+ backdrop-filter: blur(8px);
499
+ }
500
+
501
+ .lightboxCloseBtn:hover {
502
+ background: var(--ifm-color-primary);
503
+ transform: rotate(90deg);
504
+ }
505
+
506
+ .lightboxNavBtn {
507
+ position: absolute;
508
+ top: 50%;
509
+ transform: translateY(-50%);
510
+ background: rgba(255, 255, 255, 0.15);
511
+ border: none;
512
+ color: #ffffff;
513
+ width: 50px;
514
+ height: 50px;
515
+ border-radius: 50%;
516
+ cursor: pointer;
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ font-size: 1.5rem;
521
+ transition:
522
+ transform 0.2s ease,
523
+ background-color 0.2s ease;
524
+ backdrop-filter: blur(8px);
525
+ }
526
+
527
+ .lightboxNavBtn:hover {
528
+ background: var(--ifm-color-primary);
529
+ transform: translateY(-50%) scale(1.1);
530
+ }
531
+
532
+ .lightboxPrevBtn {
533
+ left: 1.5rem;
534
+ }
535
+
536
+ .lightboxNextBtn {
537
+ right: 1.5rem;
538
+ }
539
+
540
+ @media (max-width: 600px) {
541
+ .lightboxNavBtn {
542
+ width: 38px;
543
+ height: 38px;
544
+ font-size: 1.1rem;
545
+ }
546
+ .lightboxPrevBtn {
547
+ left: 0.5rem;
548
+ }
549
+ .lightboxNextBtn {
550
+ right: 0.5rem;
551
+ }
552
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Options as BlogOptions } from '@docusaurus/plugin-content-blog';
2
+ export type AlbumFrontMatter = {
3
+ cover?: string;
4
+ cover_caption?: string;
5
+ location?: string;
6
+ album_series?: string;
7
+ tags?: string[];
8
+ hide_table_of_contents?: boolean;
9
+ toc_min_heading_level?: number;
10
+ toc_max_heading_level?: number;
11
+ [key: string]: unknown;
12
+ };
13
+ export type AlbumPostMeta = {
14
+ permalink: string;
15
+ title: string;
16
+ date: string;
17
+ year?: string;
18
+ month?: string;
19
+ displayDate?: string;
20
+ frontMatter: AlbumFrontMatter;
21
+ };
22
+ export type LightboxImage = {
23
+ src: string;
24
+ alt?: string;
25
+ caption?: string;
26
+ };
27
+ export type AlbumPluginOptions = BlogOptions;
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-album",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Pinterest-style masonry photo album plugin for Docusaurus with a Google Photos timeline scrubber and zero-dependency lightbox.",
5
+ "license": "MIT",
6
+ "author": "kywk",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kywk/docusaurus-kit.git",
10
+ "directory": "packages/plugin-album"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/kywk/docusaurus-kit/issues"
14
+ },
15
+ "homepage": "https://github.com/kywk/docusaurus-kit/tree/main/packages/plugin-album#readme",
16
+ "keywords": [
17
+ "docusaurus",
18
+ "docusaurus-plugin",
19
+ "album",
20
+ "gallery",
21
+ "pinterest",
22
+ "masonry",
23
+ "photo",
24
+ "lightbox"
25
+ ],
26
+ "main": "lib/index.js",
27
+ "types": "lib/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./lib/index.d.ts",
31
+ "default": "./lib/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "lib",
37
+ "src/theme",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20.19.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@docusaurus/core": "^3.8.0",
46
+ "@docusaurus/plugin-content-blog": "^3.8.0",
47
+ "@docusaurus/theme-common": "^3.8.0",
48
+ "clsx": "^2.0.0",
49
+ "react": "^18.0.0 || ^19.0.0",
50
+ "react-dom": "^18.0.0 || ^19.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@docusaurus/core": "^3.10.2",
54
+ "@docusaurus/module-type-aliases": "^3.10.2",
55
+ "@docusaurus/plugin-content-blog": "^3.10.2",
56
+ "@docusaurus/theme-classic": "^3.10.2",
57
+ "@docusaurus/theme-common": "^3.10.2",
58
+ "@docusaurus/types": "^3.10.2",
59
+ "@types/node": "^22.19.1",
60
+ "@types/react": "^19.2.2",
61
+ "@types/react-dom": "^19.2.2",
62
+ "clsx": "^2.1.1",
63
+ "react": "^19.2.3",
64
+ "react-dom": "^19.2.3",
65
+ "typescript": "^5.9.3",
66
+ "vitest": "^2.1.9"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "scripts": {
72
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
73
+ "typecheck": "tsc -p tsconfig.json --noEmit",
74
+ "test": "pnpm run build && vitest run"
75
+ }
6
76
  }