flipfolio 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,109 @@
1
+ import {
2
+ createFolderGallery
3
+ } from "./chunk-DT6YOBGV.js";
4
+
5
+ // src/folder-gallery-element.js
6
+ var asBool = (v) => v !== null && v !== "false";
7
+ var ElementBase = typeof HTMLElement !== "undefined" ? HTMLElement : class {
8
+ };
9
+ var FolderGalleryElement = class extends ElementBase {
10
+ static get observedAttributes() {
11
+ return ["mode", "peek", "loop", "scroll-nav", "reduced-motion", "default-active-index", "label", "folder-path"];
12
+ }
13
+ constructor() {
14
+ super();
15
+ this._handle = null;
16
+ this._items = [];
17
+ }
18
+ /** Items are an array - set via property (attributes can't hold objects). */
19
+ get items() {
20
+ return this._items;
21
+ }
22
+ set items(value) {
23
+ this._items = Array.isArray(value) ? value : [];
24
+ if (this.isConnected) this._render();
25
+ }
26
+ connectedCallback() {
27
+ if (!this._items.length && this.getAttribute("items")) {
28
+ try {
29
+ this._items = JSON.parse(this.getAttribute("items"));
30
+ } catch (_) {
31
+ }
32
+ }
33
+ this._render();
34
+ }
35
+ disconnectedCallback() {
36
+ this._destroy();
37
+ }
38
+ attributeChangedCallback(name, _oldValue, newValue) {
39
+ if (!this._handle) return;
40
+ if (name === "mode") {
41
+ this._handle.setMode(newValue);
42
+ return;
43
+ }
44
+ if (name === "peek") {
45
+ this._handle.setPeek(newValue);
46
+ return;
47
+ }
48
+ this._render();
49
+ }
50
+ _options() {
51
+ const opts = { items: this._items };
52
+ if (this.hasAttribute("mode")) opts.mode = this.getAttribute("mode");
53
+ if (this.hasAttribute("peek")) opts.peek = this.getAttribute("peek");
54
+ if (this.hasAttribute("loop")) opts.loop = asBool(this.getAttribute("loop"));
55
+ if (this.hasAttribute("scroll-nav")) opts.scrollNav = asBool(this.getAttribute("scroll-nav"));
56
+ if (this.hasAttribute("reduced-motion")) opts.reducedMotion = this.getAttribute("reduced-motion");
57
+ if (this.hasAttribute("default-active-index")) opts.defaultActiveIndex = parseInt(this.getAttribute("default-active-index"), 10) || 0;
58
+ if (this.hasAttribute("label")) opts.label = this.getAttribute("label");
59
+ if (this.hasAttribute("folder-path")) opts.folderPath = this.getAttribute("folder-path");
60
+ if (typeof this.contentRenderer === "function") opts.contentRenderer = this.contentRenderer;
61
+ if (typeof this.onSelect === "function") opts.onSelect = this.onSelect;
62
+ return opts;
63
+ }
64
+ _render() {
65
+ this._destroy();
66
+ this._handle = createFolderGallery(this, this._options());
67
+ }
68
+ _destroy() {
69
+ if (this._handle) {
70
+ this._handle.destroy();
71
+ this._handle = null;
72
+ }
73
+ }
74
+ /* Imperative API - delegate to the core handle. */
75
+ next() {
76
+ this._handle && this._handle.next();
77
+ }
78
+ prev() {
79
+ this._handle && this._handle.prev();
80
+ }
81
+ goTo(i) {
82
+ this._handle && this._handle.goTo(i);
83
+ }
84
+ setMode(m) {
85
+ this.setAttribute("mode", m);
86
+ }
87
+ setPeek(p) {
88
+ this.setAttribute("peek", p);
89
+ }
90
+ getActiveIndex() {
91
+ return this._handle ? this._handle.getActiveIndex() : -1;
92
+ }
93
+ getMode() {
94
+ return this._handle ? this._handle.getMode() : this.getAttribute("mode") || "stack";
95
+ }
96
+ };
97
+ function defineFolderGallery(tag = "folder-gallery") {
98
+ if (typeof customElements !== "undefined" && !customElements.get(tag)) {
99
+ customElements.define(tag, FolderGalleryElement);
100
+ }
101
+ return FolderGalleryElement;
102
+ }
103
+ var folder_gallery_element_default = FolderGalleryElement;
104
+ export {
105
+ FolderGalleryElement,
106
+ folder_gallery_element_default as default,
107
+ defineFolderGallery
108
+ };
109
+ //# sourceMappingURL=element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/folder-gallery-element.js"],"sourcesContent":["/* ============================================================================\n * <folder-gallery> - Custom Element wrapper around the framework-agnostic core.\n *\n * import { defineFolderGallery } from 'folder-gallery/element';\n * defineFolderGallery(); // registers <folder-gallery>\n *\n * <folder-gallery mode=\"grid\"></folder-gallery>\n * el.items = [{ label, color, src }, ...]; // set items via property\n * el.addEventListener('fg-select', e => ...); // events bubble from the core\n *\n * Uses the element itself as the light-DOM root, so the shared\n * folder-gallery.css styles it. (Shadow-DOM encapsulation is a later option.)\n * ========================================================================== */\n\nimport { createFolderGallery } from './folder-gallery.js';\n\nconst asBool = (v) => v !== null && v !== 'false';\n\n// SSR-safe base: on a server (Next/Nuxt) there is no HTMLElement, and merely\n// evaluating `class extends HTMLElement` at import time would throw. Fall back\n// to a stub there; the real element is only ever registered in the browser via\n// defineFolderGallery (guarded on customElements below).\nconst ElementBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};\n\nexport class FolderGalleryElement extends ElementBase {\n static get observedAttributes() {\n return ['mode', 'peek', 'loop', 'scroll-nav', 'reduced-motion', 'default-active-index', 'label', 'folder-path'];\n }\n\n constructor() {\n super();\n this._handle = null;\n this._items = [];\n }\n\n /** Items are an array - set via property (attributes can't hold objects). */\n get items() { return this._items; }\n set items(value) {\n this._items = Array.isArray(value) ? value : [];\n if (this.isConnected) this._render();\n }\n\n connectedCallback() {\n // Optional declarative items via a JSON `items` attribute.\n if (!this._items.length && this.getAttribute('items')) {\n try { this._items = JSON.parse(this.getAttribute('items')); } catch (_) { /* ignore malformed */ }\n }\n this._render();\n }\n\n disconnectedCallback() { this._destroy(); }\n\n attributeChangedCallback(name, _oldValue, newValue) {\n if (!this._handle) return;\n if (name === 'mode') { this._handle.setMode(newValue); return; }\n if (name === 'peek') { this._handle.setPeek(newValue); return; }\n this._render(); // any other observed attribute → rebuild\n }\n\n _options() {\n const opts = { items: this._items };\n if (this.hasAttribute('mode')) opts.mode = this.getAttribute('mode');\n if (this.hasAttribute('peek')) opts.peek = this.getAttribute('peek');\n if (this.hasAttribute('loop')) opts.loop = asBool(this.getAttribute('loop'));\n if (this.hasAttribute('scroll-nav')) opts.scrollNav = asBool(this.getAttribute('scroll-nav'));\n if (this.hasAttribute('reduced-motion')) opts.reducedMotion = this.getAttribute('reduced-motion');\n if (this.hasAttribute('default-active-index')) opts.defaultActiveIndex = parseInt(this.getAttribute('default-active-index'), 10) || 0;\n if (this.hasAttribute('label')) opts.label = this.getAttribute('label');\n if (this.hasAttribute('folder-path')) opts.folderPath = this.getAttribute('folder-path');\n if (typeof this.contentRenderer === 'function') opts.contentRenderer = this.contentRenderer;\n if (typeof this.onSelect === 'function') opts.onSelect = this.onSelect;\n return opts;\n }\n\n _render() {\n this._destroy();\n this._handle = createFolderGallery(this, this._options());\n }\n _destroy() {\n if (this._handle) { this._handle.destroy(); this._handle = null; }\n }\n\n /* Imperative API - delegate to the core handle. */\n next() { this._handle && this._handle.next(); }\n prev() { this._handle && this._handle.prev(); }\n goTo(i) { this._handle && this._handle.goTo(i); }\n setMode(m) { this.setAttribute('mode', m); }\n setPeek(p) { this.setAttribute('peek', p); }\n getActiveIndex() { return this._handle ? this._handle.getActiveIndex() : -1; }\n getMode() { return this._handle ? this._handle.getMode() : (this.getAttribute('mode') || 'stack'); }\n}\n\nexport function defineFolderGallery(tag = 'folder-gallery') {\n if (typeof customElements !== 'undefined' && !customElements.get(tag)) {\n customElements.define(tag, FolderGalleryElement);\n }\n return FolderGalleryElement;\n}\n\nexport default FolderGalleryElement;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,265 @@
1
+ /* folder-gallery - default stylesheet (v0.1.0, WIP)
2
+ * Themeable via the custom properties on .fg-root. Light and dark are both
3
+ * first-class: `prefers-color-scheme` picks the default, and an explicit
4
+ * `data-fg-theme="light" | "dark"` on .fg-root always wins.
5
+ * Scoped to .fg-* classes; ships no global selectors. */
6
+
7
+ .fg-root {
8
+ /* ── structural tokens ── */
9
+ --fg-radius: 18px;
10
+ --fg-perspective: 1200px;
11
+ --fg-ease: cubic-bezier(0.2, 0, 0, 1);
12
+ --fg-transition-transform: 700ms;
13
+ --fg-transition-fade: 300ms;
14
+ --fg-active-blur: 12px;
15
+
16
+ /* ── surface tokens (LIGHT is the base; dark overrides below) ──
17
+ --fg-front / --fg-front-solid are normally set PER CARD by the engine,
18
+ derived from each item's color. These are the no-color fallbacks. */
19
+ --fg-folder-bg: #e8c96a;
20
+ --fg-front: rgba(255, 255, 255, 0.55);
21
+ --fg-front-solid: rgb(240, 236, 224);
22
+ --fg-label: #1c1c1a;
23
+ --fg-dot: #8a8a86;
24
+ --fg-dot-active: #1c1c1a;
25
+ --fg-shadow: 0 12px 32px rgba(30, 30, 30, 0.16), 0 4px 12px rgba(30, 30, 30, 0.08);
26
+ --fg-inset-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.35);
27
+ --fg-inset-highlight-active: inset 0 1px 0 rgba(255, 255, 255, 0.45);
28
+ /* peek: how far contents slide out of the folder */
29
+ --fg-peek-shift: -46%;
30
+
31
+ position: relative;
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ gap: 1rem;
36
+ }
37
+
38
+ @media (prefers-color-scheme: dark) {
39
+ .fg-root {
40
+ --fg-folder-bg: #2a3a3a;
41
+ --fg-front: rgba(90, 90, 90, 0.55);
42
+ --fg-front-solid: rgb(115, 115, 115);
43
+ --fg-label: #ffffff;
44
+ --fg-dot: #9a9a9a;
45
+ --fg-dot-active: #f4f4f2;
46
+ --fg-shadow: 0 12px 32px rgba(0, 0, 0, 0.28), 0 4px 12px rgba(0, 0, 0, 0.12);
47
+ --fg-inset-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.12);
48
+ --fg-inset-highlight-active: inset 0 1px 0 rgba(255, 255, 255, 0.15);
49
+ }
50
+ }
51
+ /* Explicit theme attribute beats the media query in BOTH directions. */
52
+ .fg-root[data-fg-theme='light'] {
53
+ --fg-folder-bg: #e8c96a;
54
+ --fg-front: rgba(255, 255, 255, 0.55);
55
+ --fg-front-solid: rgb(240, 236, 224);
56
+ --fg-label: #1c1c1a;
57
+ --fg-dot: #8a8a86;
58
+ --fg-dot-active: #1c1c1a;
59
+ --fg-shadow: 0 12px 32px rgba(30, 30, 30, 0.16), 0 4px 12px rgba(30, 30, 30, 0.08);
60
+ --fg-inset-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.35);
61
+ --fg-inset-highlight-active: inset 0 1px 0 rgba(255, 255, 255, 0.45);
62
+ }
63
+ .fg-root[data-fg-theme='dark'] {
64
+ --fg-folder-bg: #2a3a3a;
65
+ --fg-front: rgba(90, 90, 90, 0.55);
66
+ --fg-front-solid: rgb(115, 115, 115);
67
+ --fg-label: #ffffff;
68
+ --fg-dot: #9a9a9a;
69
+ --fg-dot-active: #f4f4f2;
70
+ --fg-shadow: 0 12px 32px rgba(0, 0, 0, 0.28), 0 4px 12px rgba(0, 0, 0, 0.12);
71
+ --fg-inset-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.12);
72
+ --fg-inset-highlight-active: inset 0 1px 0 rgba(255, 255, 255, 0.15);
73
+ }
74
+
75
+ .fg-scene {
76
+ position: relative;
77
+ width: min(480px, 92vw); /* JS overrides per mode; this is the stack default */
78
+ height: 440px;
79
+ perspective: var(--fg-perspective);
80
+ transform-style: preserve-3d;
81
+ }
82
+
83
+ .fg-card {
84
+ position: absolute;
85
+ inset: 0 0 auto 0;
86
+ width: 100%;
87
+ /* 3/2 matches the engine's layout math (cardH = cardW * 2/3); the folder
88
+ SVG stretches to fit via preserveAspectRatio="none". */
89
+ aspect-ratio: 3 / 2;
90
+ transition:
91
+ transform var(--fg-transition-transform) var(--fg-ease),
92
+ opacity var(--fg-transition-fade) var(--fg-ease);
93
+ transform-origin: center bottom;
94
+ backface-visibility: hidden;
95
+ will-change: transform;
96
+ cursor: pointer;
97
+ contain: layout;
98
+ outline: none;
99
+ }
100
+ .fg-card.is-active { cursor: default; }
101
+ .fg-card:focus-visible {
102
+ outline: 2px solid var(--fg-dot-active);
103
+ outline-offset: 4px;
104
+ border-radius: var(--fg-radius);
105
+ }
106
+
107
+ .fg-folder {
108
+ position: absolute;
109
+ inset: 0;
110
+ width: 100%;
111
+ height: 100%;
112
+ filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.22));
113
+ }
114
+
115
+ .fg-content {
116
+ position: absolute;
117
+ left: 5%;
118
+ right: 5%;
119
+ top: 12%;
120
+ bottom: 30%;
121
+ display: grid;
122
+ place-items: center;
123
+ overflow: hidden;
124
+ border-radius: calc(var(--fg-radius) - 6px);
125
+ transition:
126
+ transform var(--fg-transition-fade) var(--fg-ease),
127
+ opacity var(--fg-transition-fade) var(--fg-ease),
128
+ box-shadow var(--fg-transition-fade) var(--fg-ease);
129
+ z-index: 1;
130
+ }
131
+ .fg-image { width: 100%; height: 100%; object-fit: cover; display: block; }
132
+
133
+ /* ── peek: contents slide out of the folder ──
134
+ data-fg-peek on the root: 'hover' (default), 'always', or 'off'.
135
+ 'off' keeps a subtle lift so hover still gives feedback. */
136
+ /* Empty interiors are inert: no box to lift, no shadow to cast. */
137
+ .fg-content:empty { display: none; }
138
+
139
+ [data-fg-peek='off'] .fg-card.is-active:hover .fg-content { transform: translateY(-8px); }
140
+ /* Peek moves the ACTIVE card's contents only. In 'hover' mode the pointer
141
+ anywhere over the scene peeks the active folder (grid peeks the hovered
142
+ card instead), and the slide-out is delayed a beat while the retract is
143
+ instant, so navigating reads as retract, cycle, peek. No shadow on the
144
+ content box itself: the box is transparent around whatever it holds, so a
145
+ box shadow reads as a floating empty sheet. Contents bring their own. */
146
+ [data-fg-peek='hover']:not([data-fg-mode='grid']) .fg-scene:hover .fg-card.is-active .fg-content,
147
+ [data-fg-peek='hover'][data-fg-mode='grid'] .fg-card:hover .fg-content,
148
+ [data-fg-peek='always'] .fg-card.is-active .fg-content {
149
+ transform: translateY(var(--fg-peek-shift));
150
+ opacity: 1; /* decal cards hide their contents at rest; peek reveals them */
151
+ transition-delay: 180ms;
152
+ }
153
+
154
+ /* Front panel - the folder's face. Sits over the lower 3/4 like the original
155
+ (top 25%, hairline inset), radius 18. */
156
+ .fg-front {
157
+ position: absolute;
158
+ top: 25%;
159
+ left: 4px;
160
+ right: 4px;
161
+ bottom: 4px;
162
+ border-radius: var(--fg-radius);
163
+ overflow: hidden;
164
+ display: flex;
165
+ align-items: flex-end;
166
+ padding: 0.9rem 1.1rem;
167
+ z-index: 2;
168
+ transition:
169
+ background var(--fg-transition-fade) var(--fg-ease),
170
+ box-shadow var(--fg-transition-fade) var(--fg-ease);
171
+ }
172
+
173
+ /* Behind folders: SOLID opaque front panel - no frosted glass off the active
174
+ card (both a design rule and a perf rule: backdrop-filter never stacks). */
175
+ .fg-card:not(.is-active) .fg-front {
176
+ background: var(--fg-front-solid);
177
+ box-shadow: var(--fg-inset-highlight);
178
+ }
179
+ .fg-card.is-active .fg-front {
180
+ background: var(--fg-front);
181
+ backdrop-filter: blur(var(--fg-active-blur));
182
+ -webkit-backdrop-filter: blur(var(--fg-active-blur));
183
+ box-shadow: var(--fg-shadow), var(--fg-inset-highlight-active);
184
+ }
185
+
186
+ /* ── decal: the folder is skinned with an image (item.decal) ──
187
+ The image is clipped to the silhouette inside the SVG; the front panel
188
+ becomes a label scrim so the photo shows through and the label stays
189
+ readable. Overrides both the solid and frosted front treatments. */
190
+ /* ── decal: the photo prints on the folder's front panel ──
191
+ The back and tab keep their color, so the stack still reads as folders.
192
+ The print carries its own lighting: a lip hairline where the front edge
193
+ catches the light, a soft lamination sheen from the top left, and a label
194
+ scrim along the bottom. */
195
+ .fg-card--decal .fg-front,
196
+ .fg-card--decal:not(.is-active) .fg-front,
197
+ .fg-card--decal.is-active .fg-front {
198
+ background: var(--fg-front-solid);
199
+ backdrop-filter: none;
200
+ -webkit-backdrop-filter: none;
201
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30);
202
+ }
203
+ .fg-card--decal.is-active .fg-front {
204
+ box-shadow: var(--fg-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.30);
205
+ }
206
+ .fg-decal {
207
+ position: absolute;
208
+ inset: 0;
209
+ width: 100%;
210
+ height: 100%;
211
+ object-fit: cover;
212
+ border-radius: inherit;
213
+ }
214
+ .fg-card--decal .fg-front::after {
215
+ content: '';
216
+ position: absolute;
217
+ inset: 0;
218
+ border-radius: inherit;
219
+ pointer-events: none;
220
+ background:
221
+ linear-gradient(115deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04) 34%, rgba(255, 255, 255, 0) 52%),
222
+ linear-gradient(to top, rgba(12, 12, 14, 0.62), rgba(12, 12, 14, 0) 42%);
223
+ }
224
+ .fg-card--decal .fg-label { color: #ffffff; position: relative; z-index: 1; }
225
+
226
+ .fg-label {
227
+ /* Falls back to the theme label color when the card has no item.color;
228
+ otherwise the engine sets --fg-label-on-color per card so the label
229
+ stays readable against that folder's own front-panel brightness. */
230
+ color: var(--fg-label-on-color, var(--fg-label));
231
+ font-weight: 600;
232
+ font-size: 0.95rem;
233
+ letter-spacing: 0.01em;
234
+ }
235
+
236
+ .fg-dots { display: flex; gap: 0.5rem; }
237
+ .fg-dot {
238
+ width: 8px;
239
+ height: 8px;
240
+ border-radius: 50%;
241
+ border: 0;
242
+ padding: 0;
243
+ cursor: pointer;
244
+ background: var(--fg-dot);
245
+ opacity: 0.5;
246
+ transition:
247
+ opacity var(--fg-transition-fade) var(--fg-ease),
248
+ transform var(--fg-transition-fade) var(--fg-ease),
249
+ background var(--fg-transition-fade) var(--fg-ease);
250
+ }
251
+ .fg-dot:hover { opacity: 0.85; }
252
+ .fg-dot.is-active { background: var(--fg-dot-active); opacity: 1; transform: scale(1.35); }
253
+
254
+ .fg-sr-only {
255
+ position: absolute;
256
+ width: 1px;
257
+ height: 1px;
258
+ overflow: hidden;
259
+ clip: rect(0 0 0 0);
260
+ white-space: nowrap;
261
+ }
262
+
263
+ @media (prefers-reduced-motion: reduce) {
264
+ .fg-card, .fg-content, .fg-front, .fg-dot { transition-duration: 1ms; }
265
+ }