flipfolio 0.1.0 → 0.3.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/README.md +34 -7
- package/dist/{chunk-DT6YOBGV.js → chunk-RFBZ3WPG.js} +274 -75
- package/dist/chunk-RFBZ3WPG.js.map +1 -0
- package/dist/element.cjs +287 -59
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.ts +6 -0
- package/dist/element.js +21 -2
- package/dist/element.js.map +1 -1
- package/dist/folder-gallery.css +66 -24
- package/dist/index.cjs +274 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +3 -1
- package/dist/react.cjs +305 -94
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.js +39 -27
- package/dist/react.js.map +1 -1
- package/dist/vue.cjs +299 -62
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.d.ts +20 -4
- package/dist/vue.js +33 -5
- package/dist/vue.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-DT6YOBGV.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# flipfolio
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/flipfolio)
|
|
4
|
+
[](https://bundlephobia.com/package/flipfolio)
|
|
5
|
+
[](https://github.com/kirthi-b/flipfolio/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
A gallery where every item is a folder: stack them, grid them, spin them.
|
|
4
9
|
|
|
5
10
|
Started as a CSS 3D experiment; it now runs the [kirthi.studio](https://kirthi.studio) homepage. No framework, no dependencies.
|
|
6
11
|
|
|
7
|
-
Live demo: [kirthi.studio/demos/flipfolio](https://kirthi.studio/demos/flipfolio/)
|
|
12
|
+
Live demo: [kirthi.studio/demos/flipfolio](https://kirthi.studio/demos/flipfolio/), or edit it on [StackBlitz](https://stackblitz.com/github/kirthi-b/flipfolio/tree/master/examples/vanilla?file=main.js)
|
|
8
13
|
|
|
9
|
-
> v0.
|
|
14
|
+
> v0.3.0. New this release: a crisp grid view, runtime color and gradient theming, and drag hooks.
|
|
10
15
|
|
|
11
|
-

|
|
12
17
|
|
|
13
18
|
## Quick start
|
|
14
19
|
```sh
|
|
@@ -72,6 +77,9 @@ Folders hold your actual content. These are live project images, loaded with `it
|
|
|
72
77
|

|
|
73
78
|
One items array, three layouts. `setMode()` animates between them at runtime.
|
|
74
79
|
|
|
80
|
+

|
|
81
|
+
Grab the front folder and throw it. Stack mode, `drag: 'fling'`; a short drag springs back.
|
|
82
|
+
|
|
75
83
|

|
|
76
84
|
Interiors take images, markup, DOM nodes, or a `contentRenderer` for anything else.
|
|
77
85
|
|
|
@@ -85,28 +93,47 @@ Per-folder tab colors, tokens for radius and blur, and the silhouette itself is
|
|
|
85
93
|
|---|---|---|---|
|
|
86
94
|
| `items` | `Item[]` | `[]` | `{ label?, color?, src?, content?, decal?, ...data }` |
|
|
87
95
|
| `mode` | `'stack'\|'grid'\|'carousel'` | `'stack'` | 3 modes (a 4th "shelf" is planned) |
|
|
96
|
+
| `grid` | `{ columns?, gap? }` | auto | pin the grid's column count and/or gap (px); default is a width/count heuristic |
|
|
88
97
|
| `peek` | `'hover'\|'always'\|'off'` | `'hover'` | contents slide out of the folder |
|
|
98
|
+
| `drag` | `'fling'\|'off'` | `'fling'` | grab the active folder and throw it (stack mode) |
|
|
89
99
|
| `contentRenderer` | `(card, item, i) => void` | built-in | fills the folder interior with whatever you render |
|
|
90
100
|
| `onSelect` | `(item, i) => void` | (none) | fired on click/Enter of the active folder (no built-in navigation) |
|
|
91
|
-
| `folderPath` | `string` | manila default | SVG path `d` (viewBox `0 0 480 342`) |
|
|
101
|
+
| `folderPath` | `string` | manila default | SVG path `d` (viewBox `0 0 480 342`), or a `FOLDER_PATHS` preset |
|
|
92
102
|
| `loop` | `boolean` | `true` | wrap-around |
|
|
93
103
|
| `scrollNav` | `boolean` | `true` | wheel/trackpad cycles the stack/carousel |
|
|
94
104
|
| `reducedMotion` | `'auto'\|'off'\|'force'` | `'auto'` | drops the 3D tilt when reduced |
|
|
95
105
|
| `defaultActiveIndex` | `number` | `0` | |
|
|
96
106
|
| `label` | `string` | `'Folder gallery'` | `aria-label` for the listbox |
|
|
97
107
|
|
|
98
|
-
Handle: `next()`, `prev()`, `goTo(i)`, `setMode(m)`, `setPeek(p)`, `getActiveIndex()`, `getMode()`, `destroy()`.
|
|
108
|
+
Handle: `next()`, `prev()`, `goTo(i)`, `setMode(m)`, `setPeek(p)`, `setColor(i, hex)`, `setGradient(i, gradient)`, `getActiveIndex()`, `getMode()`, `getPeek()`, `getColor(i)`, `getGradient(i)`, `getItems()`, `destroy()`.
|
|
99
109
|
|
|
100
|
-
Events (CustomEvent on the root, bubbling): `fg-select`, `fg-activechange`, `fg-modechange`, each with `event.detail`.
|
|
110
|
+
Events (CustomEvent on the root, bubbling): `fg-select`, `fg-activechange`, `fg-modechange`, `fg-peekchange` (detail `{ peek }`), `fg-flingstart` and `fg-flingend` (detail `{ index, direction }`, direction `'next' | 'prev'`), each with `event.detail`. React props: `onSelect`, `onActiveChange`, `onModeChange`, `onPeekChange`, `onFlingStart`, `onFlingEnd`. Vue emits: `@select`, `@active-change`, `@mode-change`, `@peek-change`, `@fling-start`, `@fling-end`.
|
|
101
111
|
|
|
102
112
|
## Content
|
|
103
113
|
Each folder renders what you give it: `item.src` for an image, `item.content` for an HTML string or a DOM node, or a `contentRenderer(card, item, index)` for anything richer. Set `item.decal` to an image URL and the photo prints on the folder's front panel; the back and tab keep their color. Contents slide out of the folder on hover; the `peek` option makes that always-on or turns it off.
|
|
104
114
|
|
|
115
|
+
## Silhouette
|
|
116
|
+
`folderPath` takes any SVG path `d` in the `0 0 480 342` viewBox. Three presets ship as `FOLDER_PATHS`: `left` (the default tab), `right` (tab mirrored), and `tray` (no tab). Pass one instead of a raw string:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { createFolderGallery, FOLDER_PATHS } from 'flipfolio';
|
|
120
|
+
createFolderGallery(el, { items, folderPath: FOLDER_PATHS.tray });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Runtime theming
|
|
124
|
+
Each folder's palette derives from `item.color`; `setColor(index, hex)` re-derives the whole thing (SVG back, frosted + solid fronts, auto-contrast label) at runtime, no rebuild. `item.gradient` paints a CSS gradient across the front over the frosted surface, and `setGradient(index, gradient)` sets or clears it live (pass a falsy value to clear). Together they drive swatch pickers and theme switchers:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
gallery.setColor(0, '#3d2e42'); // recolor one folder
|
|
128
|
+
gallery.setGradient(0, 'linear-gradient(135deg, #236363, #6FCDCD, #8B7FB8)');
|
|
129
|
+
gallery.setGradient(0, null); // back to the flat front
|
|
130
|
+
```
|
|
131
|
+
|
|
105
132
|
## Accessibility
|
|
106
133
|
`role="listbox"`/`option`, roving tabindex, arrow-key + Home/End navigation, `aria-live` position announcements, `:focus-visible` rings, and a `prefers-reduced-motion` fallback that drops the 3D tilt.
|
|
107
134
|
|
|
108
135
|
## Theming
|
|
109
|
-
Override the CSS custom properties on `.fg-root`: `--fg-folder-bg`, `--fg-radius`, `--fg-perspective`, `--fg-ease`, `--fg-front`, `--fg-front-solid`, `--fg-active-blur`, `--fg-label`, `--fg-dot`, `--fg-dot-active`. Light is the default; dark applies automatically under `prefers-color-scheme: dark`, and `data-fg-theme="light"` or `"dark"` on `.fg-root` overrides either way.
|
|
136
|
+
Override the CSS custom properties on `.fg-root`: `--fg-folder-bg`, `--fg-radius`, `--fg-perspective`, `--fg-ease`, `--fg-front`, `--fg-front-solid`, `--fg-active-blur`, `--fg-label`, `--fg-dot`, `--fg-dot-active`, `--fg-scene-height`, `--fg-gap`, `--fg-folder-shadow`, `--fg-gradient-opacity`, `--fg-label-size`, `--fg-label-weight`, `--fg-label-tracking`. Light is the default; dark applies automatically under `prefers-color-scheme: dark`, and `data-fg-theme="light"` or `"dark"` on `.fg-root` overrides either way.
|
|
110
137
|
|
|
111
138
|
## Limits
|
|
112
139
|
Client-rendered only: folders paint after JS runs, there is no SSR of the layout itself. Every folder is a live 3D layer, so the sweet spot is 12 items or fewer, not hundreds. Uses `aspect-ratio` and `backdrop-filter`: evergreen browsers only, no IE fallback.
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
-
var __spreadValues = (a, b) => {
|
|
7
|
-
for (var prop in b || (b = {}))
|
|
8
|
-
if (__hasOwnProp.call(b, prop))
|
|
9
|
-
__defNormalProp(a, prop, b[prop]);
|
|
10
|
-
if (__getOwnPropSymbols)
|
|
11
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
-
if (__propIsEnum.call(b, prop))
|
|
13
|
-
__defNormalProp(a, prop, b[prop]);
|
|
14
|
-
}
|
|
15
|
-
return a;
|
|
16
|
-
};
|
|
17
|
-
var __objRest = (source, exclude) => {
|
|
18
|
-
var target = {};
|
|
19
|
-
for (var prop in source)
|
|
20
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
21
|
-
target[prop] = source[prop];
|
|
22
|
-
if (source != null && __getOwnPropSymbols)
|
|
23
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
24
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
25
|
-
target[prop] = source[prop];
|
|
26
|
-
}
|
|
27
|
-
return target;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
1
|
// src/folder-gallery.js
|
|
31
2
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
32
3
|
var DEFAULT_FOLDER_PATH = "M 0 22 C 0 10 10 0 22 0 L 155 0 C 168 0 172 6 176 14 C 180 24 188 32 204 32 L 458 32 C 470 32 480 42 480 54 L 480 320 C 480 332 470 342 458 342 L 22 342 C 10 342 0 332 0 320 Z";
|
|
4
|
+
var FOLDER_PATHS = {
|
|
5
|
+
left: DEFAULT_FOLDER_PATH,
|
|
6
|
+
right: "M 480 22 C 480 10 470 0 458 0 L 325 0 C 312 0 308 6 304 14 C 300 24 292 32 276 32 L 22 32 C 10 32 0 42 0 54 L 0 320 C 0 332 10 342 22 342 L 458 342 C 470 342 480 332 480 320 Z",
|
|
7
|
+
tray: "M 0 22 C 0 10 10 0 22 0 L 458 0 C 470 0 480 10 480 22 L 480 320 C 480 332 470 342 458 342 L 22 342 C 10 342 0 332 0 320 Z"
|
|
8
|
+
};
|
|
33
9
|
var DEFAULTS = {
|
|
34
10
|
mode: "stack",
|
|
35
11
|
// 'stack' | 'grid' | 'carousel'
|
|
@@ -40,19 +16,27 @@ var DEFAULTS = {
|
|
|
40
16
|
// 'auto' | 'off' | 'force'
|
|
41
17
|
peek: "hover",
|
|
42
18
|
// 'hover' | 'always' | 'off'
|
|
19
|
+
drag: "fling",
|
|
20
|
+
// 'fling' | 'off' - grab the active folder and throw it (stack mode)
|
|
43
21
|
defaultActiveIndex: 0,
|
|
44
22
|
label: "Folder gallery"
|
|
45
23
|
};
|
|
46
24
|
var PEEK_MODES = /* @__PURE__ */ new Set(["hover", "always", "off"]);
|
|
25
|
+
var DRAG_MODES = /* @__PURE__ */ new Set(["fling", "off"]);
|
|
26
|
+
var INSTANCES = /* @__PURE__ */ new WeakMap();
|
|
47
27
|
var MODE_WIDTHS = { stack: 480, grid: 720, carousel: 720 };
|
|
48
28
|
var SCROLL_THRESHOLD = 30;
|
|
49
29
|
var SCROLL_COOLDOWN = { stack: 450, carousel: 300 };
|
|
50
30
|
function hexToRgb(hex) {
|
|
51
|
-
|
|
31
|
+
let h = String(hex).trim().replace(/^#/, "");
|
|
32
|
+
if (h.length === 3) h = h.replace(/./g, (c) => c + c);
|
|
33
|
+
if (!/^[0-9a-f]{6}$/i.test(h)) return null;
|
|
52
34
|
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
53
35
|
}
|
|
54
36
|
function folderColors(hex) {
|
|
55
|
-
const
|
|
37
|
+
const rgb = hexToRgb(hex);
|
|
38
|
+
if (!rgb) return null;
|
|
39
|
+
const [r, g, b] = rgb;
|
|
56
40
|
const dn = (v) => Math.max(v - 30, 0);
|
|
57
41
|
const up = (v, o) => Math.min(v + o, 255);
|
|
58
42
|
const fr = up(r, 35);
|
|
@@ -66,6 +50,34 @@ function folderColors(hex) {
|
|
|
66
50
|
label: yiq >= 128 ? "#1c1c1a" : "#ffffff"
|
|
67
51
|
};
|
|
68
52
|
}
|
|
53
|
+
function paintFolder(card, hex) {
|
|
54
|
+
const c = folderColors(hex);
|
|
55
|
+
if (!c) return;
|
|
56
|
+
card.style.setProperty("--fg-folder-bg", hex);
|
|
57
|
+
card.style.setProperty("--fg-front", c.front);
|
|
58
|
+
card.style.setProperty("--fg-front-solid", c.frontSolid);
|
|
59
|
+
card.style.setProperty("--fg-label-on-color", c.label);
|
|
60
|
+
const path = card.querySelector(".fg-folder path");
|
|
61
|
+
if (path) path.setAttribute("fill", c.back);
|
|
62
|
+
}
|
|
63
|
+
function paintGradient(card, gradient) {
|
|
64
|
+
const front = card.querySelector(".fg-front");
|
|
65
|
+
if (!front) return;
|
|
66
|
+
let layer = front.querySelector(".fg-gradient");
|
|
67
|
+
if (!gradient) {
|
|
68
|
+
if (layer) layer.remove();
|
|
69
|
+
card.classList.remove("fg-card--gradient");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!layer) {
|
|
73
|
+
layer = document.createElement("div");
|
|
74
|
+
layer.className = "fg-gradient";
|
|
75
|
+
layer.setAttribute("aria-hidden", "true");
|
|
76
|
+
front.insertBefore(layer, front.firstChild);
|
|
77
|
+
}
|
|
78
|
+
layer.style.background = gradient;
|
|
79
|
+
card.classList.add("fg-card--gradient");
|
|
80
|
+
}
|
|
69
81
|
function defaultContentRenderer(card, item) {
|
|
70
82
|
const wrap = document.createElement("div");
|
|
71
83
|
wrap.className = "fg-content";
|
|
@@ -85,12 +97,17 @@ function defaultContentRenderer(card, item) {
|
|
|
85
97
|
card.appendChild(wrap);
|
|
86
98
|
}
|
|
87
99
|
function createFolderGallery(root, options = {}) {
|
|
88
|
-
if (!root ||
|
|
89
|
-
const
|
|
100
|
+
if (!root || root.nodeType !== 1) throw new Error("createFolderGallery: a root element is required");
|
|
101
|
+
const prior = INSTANCES.get(root);
|
|
102
|
+
if (prior) prior();
|
|
103
|
+
const provided = {};
|
|
104
|
+
for (const k in options) if (options[k] !== void 0) provided[k] = options[k];
|
|
105
|
+
const opts = { ...DEFAULTS, ...provided };
|
|
90
106
|
const items = Array.isArray(opts.items) ? opts.items : [];
|
|
91
107
|
const n = items.length;
|
|
92
108
|
const renderContent = typeof opts.contentRenderer === "function" ? opts.contentRenderer : defaultContentRenderer;
|
|
93
|
-
const
|
|
109
|
+
const reducedQuery = opts.reducedMotion === "auto" && typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
110
|
+
let reduced = opts.reducedMotion === "force" || (reducedQuery ? reducedQuery.matches : false);
|
|
94
111
|
let mode = MODE_WIDTHS[opts.mode] ? opts.mode : "stack";
|
|
95
112
|
let active = Math.min(Math.max(opts.defaultActiveIndex | 0, 0), Math.max(n - 1, 0));
|
|
96
113
|
let scrollCooldown = false;
|
|
@@ -101,9 +118,29 @@ function createFolderGallery(root, options = {}) {
|
|
|
101
118
|
cleanups.push(() => el.removeEventListener(ev, fn, o));
|
|
102
119
|
};
|
|
103
120
|
const emit = (name, detail) => root.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
|
|
121
|
+
let destroyed = false;
|
|
122
|
+
const timers = /* @__PURE__ */ new Set();
|
|
123
|
+
const rafs = /* @__PURE__ */ new Set();
|
|
124
|
+
const later = (fn, ms) => {
|
|
125
|
+
const id = setTimeout(() => {
|
|
126
|
+
timers.delete(id);
|
|
127
|
+
fn();
|
|
128
|
+
}, ms);
|
|
129
|
+
timers.add(id);
|
|
130
|
+
return id;
|
|
131
|
+
};
|
|
132
|
+
const frame = (fn) => {
|
|
133
|
+
const id = requestAnimationFrame(() => {
|
|
134
|
+
rafs.delete(id);
|
|
135
|
+
fn();
|
|
136
|
+
});
|
|
137
|
+
rafs.add(id);
|
|
138
|
+
return id;
|
|
139
|
+
};
|
|
104
140
|
root.classList.add("fg-root");
|
|
105
141
|
root.setAttribute("data-fg-mode", mode);
|
|
106
142
|
root.setAttribute("data-fg-peek", PEEK_MODES.has(opts.peek) ? opts.peek : "hover");
|
|
143
|
+
root.setAttribute("data-fg-drag", DRAG_MODES.has(opts.drag) ? opts.drag : "fling");
|
|
107
144
|
const scene = document.createElement("div");
|
|
108
145
|
scene.className = "fg-scene";
|
|
109
146
|
scene.setAttribute("role", "listbox");
|
|
@@ -118,26 +155,48 @@ function createFolderGallery(root, options = {}) {
|
|
|
118
155
|
live.setAttribute("aria-atomic", "true");
|
|
119
156
|
root.append(scene, dotsEl, live);
|
|
120
157
|
function teardown() {
|
|
158
|
+
if (destroyed) return;
|
|
159
|
+
destroyed = true;
|
|
160
|
+
timers.forEach(clearTimeout);
|
|
161
|
+
timers.clear();
|
|
162
|
+
rafs.forEach(cancelAnimationFrame);
|
|
163
|
+
rafs.clear();
|
|
121
164
|
cleanups.forEach((fn) => fn());
|
|
122
165
|
cleanups.length = 0;
|
|
123
166
|
root.replaceChildren();
|
|
124
167
|
root.classList.remove("fg-root");
|
|
125
168
|
root.removeAttribute("data-fg-mode");
|
|
126
169
|
root.removeAttribute("data-fg-peek");
|
|
170
|
+
root.removeAttribute("data-fg-drag");
|
|
171
|
+
INSTANCES.delete(root);
|
|
127
172
|
}
|
|
128
173
|
if (n === 0) {
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
174
|
+
const noop = () => {
|
|
175
|
+
};
|
|
176
|
+
INSTANCES.set(root, teardown);
|
|
177
|
+
return {
|
|
178
|
+
next: noop,
|
|
179
|
+
prev: noop,
|
|
180
|
+
goTo: noop,
|
|
181
|
+
setMode: noop,
|
|
182
|
+
setPeek: noop,
|
|
183
|
+
setColor: noop,
|
|
184
|
+
setGradient: noop,
|
|
185
|
+
getActiveIndex: () => -1,
|
|
186
|
+
getMode: () => mode,
|
|
187
|
+
getPeek: () => root.getAttribute("data-fg-peek"),
|
|
188
|
+
getColor: () => void 0,
|
|
189
|
+
getGradient: () => void 0,
|
|
190
|
+
getItems: () => [],
|
|
191
|
+
destroy: teardown
|
|
192
|
+
};
|
|
135
193
|
}
|
|
136
194
|
function setCardTransform(card, { x, y, z, rx, ry, s, zIndex, opacity, isActive }) {
|
|
137
|
-
card.
|
|
138
|
-
|
|
195
|
+
if (!card.classList.contains("fg-card--flung")) {
|
|
196
|
+
card.style.transform = `translate3d(${x}px,${y}px,${z}px) rotateX(${rx}deg) rotateY(${ry}deg) scale(${s})`;
|
|
197
|
+
card.style.opacity = String(opacity);
|
|
198
|
+
}
|
|
139
199
|
card.style.zIndex = String(zIndex);
|
|
140
|
-
card.style.opacity = String(opacity);
|
|
141
200
|
card.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
142
201
|
card.classList.toggle("is-active", isActive);
|
|
143
202
|
}
|
|
@@ -147,13 +206,9 @@ function createFolderGallery(root, options = {}) {
|
|
|
147
206
|
card.tabIndex = i === active ? 0 : -1;
|
|
148
207
|
card.setAttribute("role", "option");
|
|
149
208
|
card.setAttribute("aria-label", item.label || `Item ${i + 1}`);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
card.style.setProperty("--fg-front", c.front);
|
|
154
|
-
card.style.setProperty("--fg-front-solid", c.frontSolid);
|
|
155
|
-
card.style.setProperty("--fg-label-on-color", c.label);
|
|
156
|
-
}
|
|
209
|
+
card.setAttribute("aria-setsize", String(n));
|
|
210
|
+
card.setAttribute("aria-posinset", String(i + 1));
|
|
211
|
+
card.setAttribute("data-fg-index", String(i));
|
|
157
212
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
158
213
|
svg.setAttribute("class", "fg-folder");
|
|
159
214
|
svg.setAttribute("viewBox", "0 0 480 342");
|
|
@@ -161,7 +216,7 @@ function createFolderGallery(root, options = {}) {
|
|
|
161
216
|
svg.setAttribute("aria-hidden", "true");
|
|
162
217
|
const path = document.createElementNS(SVG_NS, "path");
|
|
163
218
|
path.setAttribute("d", opts.folderPath);
|
|
164
|
-
path.setAttribute("fill",
|
|
219
|
+
path.setAttribute("fill", "var(--fg-folder-bg)");
|
|
165
220
|
svg.appendChild(path);
|
|
166
221
|
card.appendChild(svg);
|
|
167
222
|
renderContent(card, item, i);
|
|
@@ -184,6 +239,8 @@ function createFolderGallery(root, options = {}) {
|
|
|
184
239
|
front.appendChild(label);
|
|
185
240
|
}
|
|
186
241
|
card.appendChild(front);
|
|
242
|
+
if (item.color) paintFolder(card, item.color);
|
|
243
|
+
if (item.gradient) paintGradient(card, item.gradient);
|
|
187
244
|
scene.appendChild(card);
|
|
188
245
|
return card;
|
|
189
246
|
});
|
|
@@ -197,9 +254,13 @@ function createFolderGallery(root, options = {}) {
|
|
|
197
254
|
dotsEl.appendChild(dot);
|
|
198
255
|
return dot;
|
|
199
256
|
});
|
|
257
|
+
const clearGridWidth = (card) => {
|
|
258
|
+
if (card.style.width) card.style.width = "";
|
|
259
|
+
};
|
|
200
260
|
function layoutStack() {
|
|
201
261
|
scene.style.height = "";
|
|
202
262
|
cardEls.forEach((card, i) => {
|
|
263
|
+
clearGridWidth(card);
|
|
203
264
|
const behind = (i - active + n) % n;
|
|
204
265
|
if (behind === 0) {
|
|
205
266
|
setCardTransform(card, { x: 0, y: 100, z: 20, rx: 0, ry: 0, s: 1, zIndex: n + 1, opacity: 1, isActive: true });
|
|
@@ -215,33 +276,31 @@ function createFolderGallery(root, options = {}) {
|
|
|
215
276
|
}
|
|
216
277
|
function layoutGrid() {
|
|
217
278
|
const sceneW = scene.clientWidth || MODE_WIDTHS.grid;
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const scaledH = cardH * sc;
|
|
225
|
-
const tabScaled = 22 * sc;
|
|
279
|
+
const gap = Number.isFinite(opts.grid && opts.grid.gap) ? opts.grid.gap : 16;
|
|
280
|
+
const cols = Math.max(1, Math.round(
|
|
281
|
+
opts.grid && opts.grid.columns ? opts.grid.columns : sceneW < 520 ? 2 : n <= 4 ? 2 : 3
|
|
282
|
+
));
|
|
283
|
+
const cellW = (sceneW - (cols - 1) * gap) / cols;
|
|
284
|
+
const cellH = cellW * (2 / 3);
|
|
226
285
|
const rows = Math.ceil(n / cols);
|
|
227
286
|
cardEls.forEach((card, i) => {
|
|
228
287
|
const col = i % cols;
|
|
229
288
|
const row = Math.floor(i / cols);
|
|
230
289
|
const itemsInRow = Math.min(cols, n - row * cols);
|
|
231
|
-
const rowW = itemsInRow *
|
|
290
|
+
const rowW = itemsInRow * cellW + (itemsInRow - 1) * gap;
|
|
232
291
|
const rowStartX = (sceneW - rowW) / 2;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
setCardTransform(card, { x: tx, y: ty, z: 0, rx: 0, ry: 0, s: sc, zIndex: i === active ? 2 : 1, opacity: 1, isActive: i === active });
|
|
292
|
+
const x = rowStartX + col * (cellW + gap);
|
|
293
|
+
const y = row * (cellH + gap);
|
|
294
|
+
card.style.width = cellW + "px";
|
|
295
|
+
setCardTransform(card, { x, y, z: 0, rx: 0, ry: 0, s: 1, zIndex: i === active ? 2 : 1, opacity: 1, isActive: i === active });
|
|
238
296
|
});
|
|
239
|
-
scene.style.height = rows *
|
|
297
|
+
scene.style.height = rows * cellH + (rows - 1) * gap + "px";
|
|
240
298
|
}
|
|
241
299
|
function layoutCarousel() {
|
|
242
300
|
const sceneW = scene.clientWidth || MODE_WIDTHS.carousel;
|
|
243
301
|
const cardW = sceneW;
|
|
244
302
|
const cardH = cardW * (2 / 3);
|
|
303
|
+
cardEls.forEach(clearGridWidth);
|
|
245
304
|
const activeSc = 0.62;
|
|
246
305
|
const activeW = cardW * activeSc;
|
|
247
306
|
const sceneH = Math.max(cardH * activeSc + 40, 180);
|
|
@@ -276,29 +335,39 @@ function createFolderGallery(root, options = {}) {
|
|
|
276
335
|
live.textContent = cur && cur.label ? `${active + 1} of ${n}: ${cur.label}` : `${active + 1} of ${n}`;
|
|
277
336
|
}
|
|
278
337
|
function goTo(i) {
|
|
279
|
-
|
|
338
|
+
if (destroyed) return;
|
|
339
|
+
const t = Number.isFinite(+i) ? Math.trunc(+i) : active;
|
|
340
|
+
const next = opts.loop ? (t % n + n) % n : Math.min(Math.max(t, 0), n - 1);
|
|
280
341
|
if (next === active) return;
|
|
281
342
|
active = next;
|
|
282
343
|
applyLayout();
|
|
283
344
|
emit("fg-activechange", { index: active, item: items[active] });
|
|
284
345
|
}
|
|
285
346
|
function select(i) {
|
|
347
|
+
if (destroyed) return;
|
|
286
348
|
emit("fg-select", { index: i, item: items[i] });
|
|
287
349
|
if (typeof opts.onSelect === "function") opts.onSelect(items[i], i);
|
|
288
350
|
}
|
|
289
351
|
function setMode(m) {
|
|
290
|
-
if (m === mode || !MODE_WIDTHS[m]) return;
|
|
352
|
+
if (destroyed || m === mode || !MODE_WIDTHS[m]) return;
|
|
291
353
|
mode = m;
|
|
292
354
|
root.setAttribute("data-fg-mode", mode);
|
|
293
355
|
applyLayout();
|
|
294
356
|
emit("fg-modechange", { mode });
|
|
295
357
|
}
|
|
296
358
|
function setPeek(p) {
|
|
297
|
-
if (!PEEK_MODES.has(p)) return;
|
|
359
|
+
if (destroyed || !PEEK_MODES.has(p) || p === root.getAttribute("data-fg-peek")) return;
|
|
298
360
|
root.setAttribute("data-fg-peek", p);
|
|
361
|
+
emit("fg-peekchange", { peek: p });
|
|
299
362
|
}
|
|
363
|
+
const dragEnabled = opts.drag !== "off";
|
|
364
|
+
let suppressClick = false;
|
|
300
365
|
cardEls.forEach((card, i) => {
|
|
301
366
|
on(card, "click", () => {
|
|
367
|
+
if (suppressClick) {
|
|
368
|
+
suppressClick = false;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
302
371
|
i === active ? select(i) : goTo(i);
|
|
303
372
|
});
|
|
304
373
|
on(card, "keydown", (e) => {
|
|
@@ -364,7 +433,7 @@ function createFolderGallery(root, options = {}) {
|
|
|
364
433
|
scrollAccum = 0;
|
|
365
434
|
scrollCooldown = true;
|
|
366
435
|
goTo(active + dir);
|
|
367
|
-
|
|
436
|
+
later(() => {
|
|
368
437
|
scrollCooldown = false;
|
|
369
438
|
scrollAccum = 0;
|
|
370
439
|
}, SCROLL_COOLDOWN[mode] || 300);
|
|
@@ -378,6 +447,7 @@ function createFolderGallery(root, options = {}) {
|
|
|
378
447
|
touchStartY = e.touches[0].clientY;
|
|
379
448
|
}, { passive: true });
|
|
380
449
|
on(scene, "touchend", (e) => {
|
|
450
|
+
if (dragEnabled && mode === "stack") return;
|
|
381
451
|
const dx = touchStartX - e.changedTouches[0].clientX;
|
|
382
452
|
const dy = touchStartY - e.changedTouches[0].clientY;
|
|
383
453
|
const ax = Math.abs(dx);
|
|
@@ -386,29 +456,158 @@ function createFolderGallery(root, options = {}) {
|
|
|
386
456
|
const dir = ax > ay ? dx > 0 ? 1 : -1 : dy > 0 ? 1 : -1;
|
|
387
457
|
goTo(active + dir);
|
|
388
458
|
}, { passive: true });
|
|
459
|
+
if (dragEnabled) {
|
|
460
|
+
const DRAG_ROT = 0.04;
|
|
461
|
+
const FLING_DIST = 90;
|
|
462
|
+
const FLING_VEL = 0.5;
|
|
463
|
+
const TAP_SLOP = 6;
|
|
464
|
+
let dragCard = null;
|
|
465
|
+
let dragBase = "";
|
|
466
|
+
let startPX = 0, startPY = 0, dragDX = 0, dragDY = 0;
|
|
467
|
+
let lastX = 0, lastY = 0, lastT = 0, velX = 0, velY = 0;
|
|
468
|
+
on(scene, "pointerdown", (e) => {
|
|
469
|
+
if (mode !== "stack") return;
|
|
470
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
471
|
+
const card = e.target && e.target.closest ? e.target.closest(".fg-card") : null;
|
|
472
|
+
if (!card || !card.classList.contains("is-active")) return;
|
|
473
|
+
dragCard = card;
|
|
474
|
+
dragBase = card.style.transform;
|
|
475
|
+
startPX = e.clientX;
|
|
476
|
+
startPY = e.clientY;
|
|
477
|
+
dragDX = 0;
|
|
478
|
+
dragDY = 0;
|
|
479
|
+
lastX = e.clientX;
|
|
480
|
+
lastY = e.clientY;
|
|
481
|
+
lastT = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
482
|
+
velX = 0;
|
|
483
|
+
velY = 0;
|
|
484
|
+
card.classList.add("fg-card--dragging");
|
|
485
|
+
if (card.setPointerCapture && e.pointerId !== void 0) {
|
|
486
|
+
try {
|
|
487
|
+
card.setPointerCapture(e.pointerId);
|
|
488
|
+
} catch (_) {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
on(window, "pointermove", (e) => {
|
|
493
|
+
if (!dragCard) return;
|
|
494
|
+
dragDX = e.clientX - startPX;
|
|
495
|
+
dragDY = e.clientY - startPY;
|
|
496
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
497
|
+
const dt = Math.max(now - lastT, 1);
|
|
498
|
+
velX = (e.clientX - lastX) / dt;
|
|
499
|
+
velY = (e.clientY - lastY) / dt;
|
|
500
|
+
lastX = e.clientX;
|
|
501
|
+
lastY = e.clientY;
|
|
502
|
+
lastT = now;
|
|
503
|
+
dragCard.style.transform = `${dragBase} translate3d(${dragDX}px, ${dragDY}px, 40px) rotate(${(dragDX * DRAG_ROT).toFixed(2)}deg)`;
|
|
504
|
+
});
|
|
505
|
+
const endDrag = () => {
|
|
506
|
+
if (!dragCard) return;
|
|
507
|
+
const card = dragCard;
|
|
508
|
+
dragCard = null;
|
|
509
|
+
card.classList.remove("fg-card--dragging");
|
|
510
|
+
const dist = Math.hypot(dragDX, dragDY);
|
|
511
|
+
if (dist < TAP_SLOP) {
|
|
512
|
+
card.style.transform = dragBase;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
suppressClick = true;
|
|
516
|
+
const horizontal = Math.abs(dragDX) >= Math.abs(dragDY);
|
|
517
|
+
const flung = dist > FLING_DIST || Math.hypot(velX, velY) > FLING_VEL;
|
|
518
|
+
const dir = horizontal ? dragDX > 0 ? -1 : 1 : dragDY > 0 ? -1 : 1;
|
|
519
|
+
const target = active + dir;
|
|
520
|
+
const blocked = !opts.loop && (target < 0 || target > n - 1);
|
|
521
|
+
if (!flung || blocked) {
|
|
522
|
+
card.style.transform = dragBase;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const semantic = dir > 0 ? "next" : "prev";
|
|
526
|
+
emit("fg-flingstart", { index: active, direction: semantic });
|
|
527
|
+
if (reduced) {
|
|
528
|
+
goTo(target);
|
|
529
|
+
emit("fg-flingend", { index: target, direction: semantic });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const offX = horizontal ? Math.sign(dragDX) * Math.max(window.innerWidth * 0.7, 480) : dragDX * 3;
|
|
533
|
+
const offY = horizontal ? dragDY * 3 : Math.sign(dragDY) * Math.max(window.innerHeight * 0.7, 480);
|
|
534
|
+
card.classList.add("fg-card--flung");
|
|
535
|
+
card.style.transform = `${dragBase} translate3d(${offX}px, ${offY}px, 40px) rotate(${Math.sign(dragDX || 1) * 18}deg)`;
|
|
536
|
+
card.style.opacity = "0";
|
|
537
|
+
goTo(target);
|
|
538
|
+
later(() => {
|
|
539
|
+
card.classList.add("fg-card--snap");
|
|
540
|
+
card.classList.remove("fg-card--flung");
|
|
541
|
+
applyLayout();
|
|
542
|
+
const slot = card.style.transform;
|
|
543
|
+
const slotOpacity = card.style.opacity;
|
|
544
|
+
card.style.transform = `${slot} translate3d(0, -90px, 0)`;
|
|
545
|
+
card.style.opacity = "0";
|
|
546
|
+
frame(() => frame(() => {
|
|
547
|
+
card.classList.remove("fg-card--snap");
|
|
548
|
+
card.classList.add("fg-card--entering");
|
|
549
|
+
card.style.transform = slot;
|
|
550
|
+
card.style.opacity = slotOpacity;
|
|
551
|
+
later(() => {
|
|
552
|
+
card.classList.remove("fg-card--entering");
|
|
553
|
+
emit("fg-flingend", { index: target, direction: semantic });
|
|
554
|
+
}, 700);
|
|
555
|
+
}));
|
|
556
|
+
}, 400);
|
|
557
|
+
};
|
|
558
|
+
on(window, "pointerup", endDrag);
|
|
559
|
+
on(window, "pointercancel", endDrag);
|
|
560
|
+
}
|
|
389
561
|
let resizeTimer;
|
|
390
562
|
on(window, "resize", () => {
|
|
391
563
|
clearTimeout(resizeTimer);
|
|
392
|
-
resizeTimer =
|
|
564
|
+
resizeTimer = later(applyLayout, 150);
|
|
393
565
|
});
|
|
566
|
+
if (reducedQuery && reducedQuery.addEventListener) {
|
|
567
|
+
on(reducedQuery, "change", (e) => {
|
|
568
|
+
if (!destroyed) {
|
|
569
|
+
reduced = e.matches;
|
|
570
|
+
applyLayout();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
394
574
|
applyLayout();
|
|
395
|
-
|
|
575
|
+
const handle = {
|
|
396
576
|
next: () => goTo(active + 1),
|
|
397
577
|
prev: () => goTo(active - 1),
|
|
398
578
|
goTo,
|
|
399
579
|
setMode,
|
|
400
580
|
setPeek,
|
|
581
|
+
setColor: (i, hex) => {
|
|
582
|
+
const card = cardEls[i];
|
|
583
|
+
if (card && !destroyed) paintFolder(card, hex);
|
|
584
|
+
},
|
|
585
|
+
setGradient: (i, gradient) => {
|
|
586
|
+
const card = cardEls[i];
|
|
587
|
+
if (card && !destroyed) paintGradient(card, gradient);
|
|
588
|
+
},
|
|
401
589
|
getActiveIndex: () => active,
|
|
402
590
|
getMode: () => mode,
|
|
591
|
+
getPeek: () => root.getAttribute("data-fg-peek"),
|
|
592
|
+
getColor: (i) => {
|
|
593
|
+
const card = cardEls[i];
|
|
594
|
+
return card ? card.style.getPropertyValue("--fg-folder-bg") || void 0 : void 0;
|
|
595
|
+
},
|
|
596
|
+
getGradient: (i) => {
|
|
597
|
+
const layer = cardEls[i] && cardEls[i].querySelector(".fg-gradient");
|
|
598
|
+
return layer ? layer.style.background || void 0 : void 0;
|
|
599
|
+
},
|
|
600
|
+
getItems: () => items.slice(),
|
|
403
601
|
destroy: teardown
|
|
404
602
|
};
|
|
603
|
+
INSTANCES.set(root, teardown);
|
|
604
|
+
return handle;
|
|
405
605
|
}
|
|
406
606
|
var folder_gallery_default = createFolderGallery;
|
|
407
607
|
|
|
408
608
|
export {
|
|
409
|
-
|
|
410
|
-
__objRest,
|
|
609
|
+
FOLDER_PATHS,
|
|
411
610
|
createFolderGallery,
|
|
412
611
|
folder_gallery_default
|
|
413
612
|
};
|
|
414
|
-
//# sourceMappingURL=chunk-
|
|
613
|
+
//# sourceMappingURL=chunk-RFBZ3WPG.js.map
|