@floegence/floe-webapp-core 0.3.2 → 0.4.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.
@@ -7,4 +7,4 @@ export { FileContextMenu, type FileContextMenuProps, type BuiltinContextMenuActi
7
7
  export { Breadcrumb, type BreadcrumbProps } from './Breadcrumb';
8
8
  export { FileBrowserToolbar, type FileBrowserToolbarProps } from './FileBrowserToolbar';
9
9
  export { FolderIcon, FolderOpenIcon, FileIcon, CodeFileIcon, ImageFileIcon, DocumentFileIcon, ConfigFileIcon, StyleFileIcon, getFileIcon, } from './FileIcons';
10
- export type { FileItem, ViewMode, SortField, SortDirection, SortConfig, FileBrowserContextValue, ContextMenuActionType, ContextMenuItem, ContextMenuEvent, ContextMenuCallbacks, } from './types';
10
+ export type { FileItem, ViewMode, SortField, SortDirection, SortConfig, FileBrowserContextValue, ContextMenuActionType, ContextMenuItem, ContextMenuEvent, ContextMenuCallbacks, OptimisticUpdateType, OptimisticRemove, OptimisticUpdate, OptimisticInsert, OptimisticOperation, ScrollPosition, } from './types';
@@ -82,6 +82,49 @@ export interface FilterMatchInfo {
82
82
  /** Matched character indices in the name */
83
83
  matchedIndices: number[];
84
84
  }
85
+ /**
86
+ * Optimistic update operation types
87
+ */
88
+ export type OptimisticUpdateType = 'remove' | 'update' | 'insert';
89
+ /**
90
+ * Optimistic remove operation
91
+ */
92
+ export interface OptimisticRemove {
93
+ type: 'remove';
94
+ /** Paths to remove */
95
+ paths: string[];
96
+ }
97
+ /**
98
+ * Optimistic update operation (rename/move)
99
+ */
100
+ export interface OptimisticUpdate {
101
+ type: 'update';
102
+ /** Original path */
103
+ oldPath: string;
104
+ /** Updated item data */
105
+ updates: Partial<FileItem>;
106
+ }
107
+ /**
108
+ * Optimistic insert operation (duplicate/copy/new)
109
+ */
110
+ export interface OptimisticInsert {
111
+ type: 'insert';
112
+ /** Parent folder path where item will be inserted */
113
+ parentPath: string;
114
+ /** Item to insert */
115
+ item: FileItem;
116
+ }
117
+ /**
118
+ * Union type for all optimistic operations
119
+ */
120
+ export type OptimisticOperation = OptimisticRemove | OptimisticUpdate | OptimisticInsert;
121
+ /**
122
+ * Scroll position state
123
+ */
124
+ export interface ScrollPosition {
125
+ top: number;
126
+ left: number;
127
+ }
85
128
  /**
86
129
  * File browser context value for internal state management
87
130
  */
@@ -114,4 +157,62 @@ export interface FileBrowserContextValue {
114
157
  showContextMenu: (event: ContextMenuEvent) => void;
115
158
  hideContextMenu: () => void;
116
159
  openItem: (item: FileItem) => void;
160
+ /**
161
+ * Optimistically remove items from the file list.
162
+ * Call this before the actual delete operation for instant UI feedback.
163
+ * @param paths - Array of file/folder paths to remove
164
+ */
165
+ optimisticRemove: (paths: string[]) => void;
166
+ /**
167
+ * Optimistically update an item (rename/move).
168
+ * Call this before the actual operation for instant UI feedback.
169
+ * @param oldPath - Original path of the item
170
+ * @param updates - Partial updates to apply (name, path, etc.)
171
+ */
172
+ optimisticUpdate: (oldPath: string, updates: Partial<FileItem>) => void;
173
+ /**
174
+ * Optimistically insert a new item (duplicate/copy/create).
175
+ * Call this before the actual operation for instant UI feedback.
176
+ * @param parentPath - Parent folder path
177
+ * @param item - The new item to insert
178
+ */
179
+ optimisticInsert: (parentPath: string, item: FileItem) => void;
180
+ /**
181
+ * Clear all pending optimistic updates.
182
+ * Call this after successful server confirmation to sync with real data.
183
+ */
184
+ clearOptimisticUpdates: () => void;
185
+ /**
186
+ * Rollback all optimistic updates and restore original state.
187
+ * Call this when an operation fails to revert the UI.
188
+ */
189
+ rollbackOptimisticUpdates: () => void;
190
+ /**
191
+ * Check if there are pending optimistic updates.
192
+ */
193
+ hasOptimisticUpdates: Accessor<boolean>;
194
+ /**
195
+ * Register a scroll container element for position tracking.
196
+ * Pass this as a ref callback to your scrollable container.
197
+ */
198
+ setScrollContainer: (el: HTMLElement | null) => void;
199
+ /**
200
+ * Get the current scroll position of the registered container.
201
+ */
202
+ getScrollPosition: () => ScrollPosition;
203
+ /**
204
+ * Set the scroll position of the registered container.
205
+ * Useful for restoring position after data refresh.
206
+ */
207
+ setScrollPosition: (position: ScrollPosition) => void;
208
+ /**
209
+ * Save current scroll position and return it.
210
+ * Convenience method that combines get + internal save.
211
+ */
212
+ saveScrollPosition: () => ScrollPosition;
213
+ /**
214
+ * Restore the last saved scroll position.
215
+ * Call this after an operation completes to maintain user's view.
216
+ */
217
+ restoreScrollPosition: () => void;
117
218
  }
package/dist/index39.js CHANGED
@@ -1,145 +1,236 @@
1
- import { createComponent as D } from "solid-js/web";
2
- import { createContext as G, createSignal as i, createMemo as A, useContext as H } from "solid-js";
3
- import { deferNonBlocking as F } from "./index68.js";
4
- const Q = G();
5
- function E(o, l) {
6
- if (!l) return [];
7
- const w = o.toLowerCase(), u = l.toLowerCase(), a = [];
8
- let m = 0;
9
- for (const g of u) {
10
- const d = w.indexOf(g, m);
11
- if (d === -1) return null;
12
- a.push(d), m = d + 1;
1
+ import { createComponent as he } from "solid-js/web";
2
+ import { createContext as me, createSignal as a, createMemo as N, useContext as we } from "solid-js";
3
+ import { deferNonBlocking as I } from "./index68.js";
4
+ const V = me();
5
+ function Q(r, p) {
6
+ if (!p) return [];
7
+ const x = r.toLowerCase(), w = p.toLowerCase(), h = [];
8
+ let g = 0;
9
+ for (const S of w) {
10
+ const m = x.indexOf(S, g);
11
+ if (m === -1) return null;
12
+ h.push(m), g = m + 1;
13
13
  }
14
- return a;
14
+ return h;
15
15
  }
16
- function re(o) {
17
- const [l, w] = i(o.initialPath ?? "/"), [u, a] = i(/* @__PURE__ */ new Set()), [m, g] = i(o.initialViewMode ?? "list"), [d, T] = i({
16
+ function Ce(r) {
17
+ const [p, x] = a(r.initialPath ?? "/"), [w, h] = a(/* @__PURE__ */ new Set()), [g, S] = a(r.initialViewMode ?? "list"), [m, j] = a({
18
18
  field: "name",
19
19
  direction: "asc"
20
- }), [y, M] = i(/* @__PURE__ */ new Set(["/"])), [V, q] = i(!1), [L, v] = i(null), [x, I] = i(""), [N, P] = i(!1), B = () => o.files, C = (e) => {
20
+ }), [M, O] = a(/* @__PURE__ */ new Set(["/"])), [R, D] = a(!1), [G, k] = a(null), [v, b] = a(""), [H, B] = a(!1), [z, P] = a([]);
21
+ let u = null, C = {
22
+ top: 0,
23
+ left: 0
24
+ };
25
+ const A = () => r.files, c = (e) => {
21
26
  const t = (e ?? "").trim();
22
27
  return t === "" ? "/" : t;
23
- }, O = A(() => {
24
- const e = /* @__PURE__ */ new Map(), t = (r) => {
25
- var h;
26
- for (const s of r)
27
- s.type === "folder" && (e.set(C(s.path), s.children ?? []), (h = s.children) != null && h.length && t(s.children));
28
- }, n = B();
28
+ }, T = (e) => {
29
+ const t = c(e);
30
+ if (t === "/") return "/";
31
+ const n = t.split("/").filter(Boolean);
32
+ return n.pop(), n.length ? "/" + n.join("/") : "/";
33
+ }, J = (e, t) => {
34
+ const n = z();
35
+ if (n.length === 0) return e;
36
+ let s = [...e];
37
+ const d = c(t);
38
+ for (const o of n)
39
+ switch (o.type) {
40
+ case "remove": {
41
+ const l = new Set(o.paths.map(c));
42
+ s = s.filter((i) => !l.has(c(i.path)));
43
+ break;
44
+ }
45
+ case "update": {
46
+ const l = c(o.oldPath), i = s.findIndex((f) => c(f.path) === l);
47
+ if (i !== -1) {
48
+ const f = o.updates.path ?? s[i].path;
49
+ T(f) === d ? s[i] = {
50
+ ...s[i],
51
+ ...o.updates
52
+ } : s.splice(i, 1);
53
+ } else {
54
+ const f = o.updates.path;
55
+ f && T(f);
56
+ }
57
+ break;
58
+ }
59
+ case "insert": {
60
+ c(o.parentPath) === d && (s.some((i) => c(i.path) === c(o.item.path)) || s.push(o.item));
61
+ break;
62
+ }
63
+ }
64
+ return s;
65
+ }, K = N(() => {
66
+ const e = /* @__PURE__ */ new Map(), t = (s) => {
67
+ var d;
68
+ for (const o of s)
69
+ o.type === "folder" && (e.set(c(o.path), o.children ?? []), (d = o.children) != null && d.length && t(o.children));
70
+ }, n = A();
29
71
  return e.set("/", n), t(n), e;
30
- }), z = A(() => {
31
- const e = C(l());
32
- let t = O().get(e) ?? [];
33
- const n = x().trim();
34
- n && (t = t.filter((s) => E(s.name, n) !== null));
35
- const r = d();
36
- return [...t].sort((s, c) => {
37
- var k, p;
38
- if (s.type !== c.type)
39
- return s.type === "folder" ? -1 : 1;
40
- let f = 0;
41
- switch (r.field) {
72
+ }), U = N(() => {
73
+ const e = c(p());
74
+ let t = K().get(e) ?? [];
75
+ t = J(t, e);
76
+ const n = v().trim();
77
+ n && (t = t.filter((o) => Q(o.name, n) !== null));
78
+ const s = m();
79
+ return [...t].sort((o, l) => {
80
+ var f, F;
81
+ if (o.type !== l.type)
82
+ return o.type === "folder" ? -1 : 1;
83
+ let i = 0;
84
+ switch (s.field) {
42
85
  case "name":
43
- f = s.name.localeCompare(c.name);
86
+ i = o.name.localeCompare(l.name);
44
87
  break;
45
88
  case "size":
46
- f = (s.size ?? 0) - (c.size ?? 0);
89
+ i = (o.size ?? 0) - (l.size ?? 0);
47
90
  break;
48
91
  case "modifiedAt":
49
- f = (((k = s.modifiedAt) == null ? void 0 : k.getTime()) ?? 0) - (((p = c.modifiedAt) == null ? void 0 : p.getTime()) ?? 0);
92
+ i = (((f = o.modifiedAt) == null ? void 0 : f.getTime()) ?? 0) - (((F = l.modifiedAt) == null ? void 0 : F.getTime()) ?? 0);
50
93
  break;
51
94
  case "type":
52
- f = (s.extension ?? "").localeCompare(c.extension ?? "");
95
+ i = (o.extension ?? "").localeCompare(l.extension ?? "");
53
96
  break;
54
97
  }
55
- return r.direction === "asc" ? f : -f;
98
+ return s.direction === "asc" ? i : -i;
56
99
  });
57
- }), S = (e) => {
58
- var r;
59
- const t = C(e);
60
- w(t), a(/* @__PURE__ */ new Set()), I(""), P(!1);
61
- const n = o.onSelect;
62
- F(() => n == null ? void 0 : n([])), (r = o.onNavigate) == null || r.call(o, t);
63
- }, j = () => {
64
- const e = l();
100
+ }), y = (e) => {
101
+ var s;
102
+ const t = c(e);
103
+ x(t), h(/* @__PURE__ */ new Set()), b(""), B(!1);
104
+ const n = r.onSelect;
105
+ I(() => n == null ? void 0 : n([])), (s = r.onNavigate) == null || s.call(r, t);
106
+ }, W = () => {
107
+ const e = p();
65
108
  if (e === "/" || e === "") return;
66
109
  const t = e.split("/").filter(Boolean);
67
- t.pop(), S(t.length ? "/" + t.join("/") : "/");
68
- }, b = (e) => {
69
- e.type === "folder" && (S(e.path), M((t) => {
110
+ t.pop(), y(t.length ? "/" + t.join("/") : "/");
111
+ }, E = (e) => {
112
+ e.type === "folder" && (y(e.path), O((t) => {
70
113
  const n = new Set(t);
71
114
  return n.add(e.path), n;
72
115
  }));
73
- }, U = {
74
- currentPath: l,
75
- setCurrentPath: S,
76
- navigateUp: j,
77
- navigateTo: b,
78
- selectedItems: () => u(),
79
- selectItem: (e, t = !1) => {
80
- const n = u(), r = t ? new Set(n) : /* @__PURE__ */ new Set();
81
- t ? r.has(e) ? r.delete(e) : r.add(e) : (r.clear(), r.add(e)), a(r);
82
- const h = z().filter((c) => r.has(c.id)), s = o.onSelect;
83
- F(() => s == null ? void 0 : s(h));
84
- },
85
- clearSelection: () => {
86
- a(/* @__PURE__ */ new Set());
87
- const e = o.onSelect;
88
- F(() => e == null ? void 0 : e([]));
89
- },
90
- isSelected: (e) => u().has(e),
91
- viewMode: m,
92
- setViewMode: g,
93
- sortConfig: d,
94
- setSortConfig: T,
95
- expandedFolders: y,
96
- toggleFolder: (e) => {
97
- M((t) => {
98
- const n = new Set(t);
99
- return n.has(e) ? n.delete(e) : n.add(e), n;
116
+ }, X = (e, t = !1) => {
117
+ const n = w(), s = t ? new Set(n) : /* @__PURE__ */ new Set();
118
+ t ? s.has(e) ? s.delete(e) : s.add(e) : (s.clear(), s.add(e)), h(s);
119
+ const d = U().filter((l) => s.has(l.id)), o = r.onSelect;
120
+ I(() => o == null ? void 0 : o(d));
121
+ }, Y = () => {
122
+ h(/* @__PURE__ */ new Set());
123
+ const e = r.onSelect;
124
+ I(() => e == null ? void 0 : e([]));
125
+ }, Z = (e) => w().has(e), _ = (e) => {
126
+ O((t) => {
127
+ const n = new Set(t);
128
+ return n.has(e) ? n.delete(e) : n.add(e), n;
129
+ });
130
+ }, $ = (e) => M().has(e), ee = () => D((e) => !e), te = (e) => k(e), ne = () => k(null), oe = (e) => {
131
+ b(e);
132
+ }, se = (e) => {
133
+ const t = v().trim();
134
+ if (!t) return null;
135
+ const n = Q(e, t);
136
+ return n ? {
137
+ matchedIndices: n
138
+ } : null;
139
+ }, re = (e) => {
140
+ var t;
141
+ e.type === "folder" ? E(e) : (t = r.onOpen) == null || t.call(r, e);
142
+ }, ie = (e) => {
143
+ e.length !== 0 && P((t) => [...t, {
144
+ type: "remove",
145
+ paths: e
146
+ }]);
147
+ }, ce = (e, t) => {
148
+ P((n) => [...n, {
149
+ type: "update",
150
+ oldPath: e,
151
+ updates: t
152
+ }]);
153
+ }, le = (e, t) => {
154
+ P((n) => [...n, {
155
+ type: "insert",
156
+ parentPath: e,
157
+ item: t
158
+ }]);
159
+ }, ae = () => {
160
+ P([]);
161
+ }, de = () => {
162
+ P([]);
163
+ }, fe = () => z().length > 0, ue = (e) => {
164
+ u = e;
165
+ }, L = () => u ? {
166
+ top: u.scrollTop,
167
+ left: u.scrollLeft
168
+ } : {
169
+ top: 0,
170
+ left: 0
171
+ }, q = (e) => {
172
+ u && (u.scrollTop = e.top, u.scrollLeft = e.left);
173
+ }, pe = {
174
+ currentPath: p,
175
+ setCurrentPath: y,
176
+ navigateUp: W,
177
+ navigateTo: E,
178
+ selectedItems: () => w(),
179
+ selectItem: X,
180
+ clearSelection: Y,
181
+ isSelected: Z,
182
+ viewMode: g,
183
+ setViewMode: S,
184
+ sortConfig: m,
185
+ setSortConfig: j,
186
+ expandedFolders: M,
187
+ toggleFolder: _,
188
+ isExpanded: $,
189
+ files: A,
190
+ currentFiles: U,
191
+ filterQuery: v,
192
+ setFilterQuery: oe,
193
+ isFilterActive: H,
194
+ setFilterActive: B,
195
+ getFilterMatch: se,
196
+ sidebarCollapsed: R,
197
+ toggleSidebar: ee,
198
+ contextMenu: G,
199
+ showContextMenu: te,
200
+ hideContextMenu: ne,
201
+ openItem: re,
202
+ // Optimistic updates
203
+ optimisticRemove: ie,
204
+ optimisticUpdate: ce,
205
+ optimisticInsert: le,
206
+ clearOptimisticUpdates: ae,
207
+ rollbackOptimisticUpdates: de,
208
+ hasOptimisticUpdates: fe,
209
+ // Scroll position management
210
+ setScrollContainer: ue,
211
+ getScrollPosition: L,
212
+ setScrollPosition: q,
213
+ saveScrollPosition: () => (C = L(), C),
214
+ restoreScrollPosition: () => {
215
+ requestAnimationFrame(() => {
216
+ q(C);
100
217
  });
101
- },
102
- isExpanded: (e) => y().has(e),
103
- files: B,
104
- currentFiles: z,
105
- filterQuery: x,
106
- setFilterQuery: (e) => {
107
- I(e);
108
- },
109
- isFilterActive: N,
110
- setFilterActive: P,
111
- getFilterMatch: (e) => {
112
- const t = x().trim();
113
- if (!t) return null;
114
- const n = E(e, t);
115
- return n ? {
116
- matchedIndices: n
117
- } : null;
118
- },
119
- sidebarCollapsed: V,
120
- toggleSidebar: () => q((e) => !e),
121
- contextMenu: L,
122
- showContextMenu: (e) => v(e),
123
- hideContextMenu: () => v(null),
124
- openItem: (e) => {
125
- var t;
126
- e.type === "folder" ? b(e) : (t = o.onOpen) == null || t.call(o, e);
127
218
  }
128
219
  };
129
- return D(Q.Provider, {
130
- value: U,
220
+ return he(V.Provider, {
221
+ value: pe,
131
222
  get children() {
132
- return o.children;
223
+ return r.children;
133
224
  }
134
225
  });
135
226
  }
136
- function ie() {
137
- const o = H(Q);
138
- if (!o)
227
+ function ye() {
228
+ const r = we(V);
229
+ if (!r)
139
230
  throw new Error("useFileBrowser must be used within a FileBrowserProvider");
140
- return o;
231
+ return r;
141
232
  }
142
233
  export {
143
- re as FileBrowserProvider,
144
- ie as useFileBrowser
234
+ Ce as FileBrowserProvider,
235
+ ye as useFileBrowser
145
236
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/floe-webapp-core",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",