@zoobzio/browser 0.0.0-stage → 0.0.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,331 @@
1
+ import { Change as Change$1, Changers as Changers$1, Changes as Changes$1, Listener as Listener$1, Middleware as Middleware$1, Options as Options$1 } from "@zoobzio/kit";
2
+ /**
3
+ * Column data type — drives cell rendering and sort behavior.
4
+ */
5
+ export type ColumnType = "text" | "number" | "date" | "datetime" | "boolean" | "enum" | "currency" | "filesize" | "url" | "image" | "action";
6
+ /**
7
+ * A file column — what the browser renders per file row.
8
+ */
9
+ export interface BrowserColumn<T> {
10
+ key: keyof T;
11
+ label: string;
12
+ type?: ColumnType;
13
+ align?: "left" | "center" | "right";
14
+ sortable?: boolean;
15
+ sortKey?: string;
16
+ enumValues?: string[];
17
+ }
18
+ export type SortDirection = "asc" | "desc";
19
+ /**
20
+ * The serializable half of a file/folder/bulk action: how it renders. The
21
+ * behavior half is a handler registered under the same key in `Actions`.
22
+ */
23
+ export interface ActionDescriptor {
24
+ label: string;
25
+ }
26
+ /**
27
+ * File action — the live shape the service exposes: descriptor joined with
28
+ * its handler, under the config key. `runAction(key, row)` dispatches by
29
+ * key.
30
+ */
31
+ export interface FileAction<T> {
32
+ key: string;
33
+ label: string;
34
+ action: (row: T) => void;
35
+ }
36
+ /**
37
+ * Bulk action over the selected files — the live shape the service exposes:
38
+ * descriptor joined with its handler, under the config key.
39
+ * `runBulkAction(key)` dispatches by key over the service's selection.
40
+ */
41
+ export interface BulkAction {
42
+ key: string;
43
+ label: string;
44
+ action: (selected: Set<string>) => void;
45
+ }
46
+ /**
47
+ * A folder row — fixed on purpose: consumers map their folder data into it,
48
+ * and folder rendering customizes through the view. Files stay generic;
49
+ * folders do not.
50
+ */
51
+ export type BrowserFolder = {
52
+ key: string;
53
+ label: string;
54
+ count?: number;
55
+ };
56
+ /**
57
+ * One step of the navigation trail. State keeps the full crumb (key and
58
+ * label) so breadcrumb labels survive navigation without a refetch; fetch
59
+ * params and events carry keys only.
60
+ */
61
+ export type BrowserCrumb = {
62
+ key: string;
63
+ label: string;
64
+ };
65
+ /**
66
+ * Parameters passed to the fetch action. The browser owns navigation and
67
+ * file sort: `path` is the folder-key trail from the root (`[]` = root).
68
+ * Sorting applies to files only — folders always render alphabetically,
69
+ * enforced by the service regardless of fetch order.
70
+ */
71
+ export interface BrowserFetchParams {
72
+ path: string[];
73
+ sortField: string | null;
74
+ sortDirection: SortDirection;
75
+ }
76
+ export interface BrowserFetchResult<T> {
77
+ folders: BrowserFolder[];
78
+ files: T[];
79
+ }
80
+ /**
81
+ * Folder action — rendered per-folder in an actions menu. The live shape
82
+ * the service exposes: descriptor joined with its handler, under the config
83
+ * key. `runFolderAction(key, folder)` dispatches by key.
84
+ */
85
+ export type FolderAction = {
86
+ key: string;
87
+ label: string;
88
+ action: (folder: BrowserFolder) => void;
89
+ };
90
+ /**
91
+ * Config the consumer provides — pure serializable data. Actions are
92
+ * descriptor records: the key is the action's identity, shared with the
93
+ * handler record in `Actions`. `actions`/`bulkActions` cover files
94
+ * (selection is files-only); `folderActions` covers folder rows.
95
+ */
96
+ export type Config<T> = {
97
+ columns: BrowserColumn<T>[];
98
+ fileKey: keyof T;
99
+ /**
100
+ * The label of the root crumb. Defaults to `Home`.
101
+ */
102
+ rootLabel?: string;
103
+ actions?: Record<string, ActionDescriptor>;
104
+ folderActions?: Record<string, ActionDescriptor>;
105
+ bulkActions?: Record<string, ActionDescriptor>;
106
+ /**
107
+ * The BCP 47 locale cells format in. Defaults to `en-US`.
108
+ */
109
+ locale?: string;
110
+ /**
111
+ * The ISO 4217 code currency cells format in. Defaults to `USD`.
112
+ */
113
+ currency?: string;
114
+ };
115
+ /**
116
+ * The state container the service reads and writes: the listing, the load
117
+ * flags, the trail, the file sort, and the selection. The container can be
118
+ * a plain object or a reactive proxy. The service replaces each member as a
119
+ * whole, so a proxy tracks every change.
120
+ */
121
+ export type State<T> = {
122
+ folders: BrowserFolder[];
123
+ files: T[];
124
+ loading: boolean;
125
+ initialized: boolean;
126
+ path: BrowserCrumb[];
127
+ sortField: string | null;
128
+ sortDirection: SortDirection;
129
+ selected: Set<string>;
130
+ };
131
+ export type StateKey = keyof State<unknown>;
132
+ /**
133
+ * The kit's state mechanisms bound to this service's container: per-field
134
+ * middleware, the read/write options, and the change objects.
135
+ */
136
+ export type Middleware<T> = Middleware$1<State<T>>;
137
+ export type Options<T> = Options$1<State<T>>;
138
+ export type Changes<T> = Changes$1<State<T>>;
139
+ export type Change<T, K extends StateKey = StateKey> = Change$1<State<T>, K>;
140
+ export type Changers<T> = Changers$1<State<T>>;
141
+ /**
142
+ * The consumer-supplied behavior: the fetch mechanism plus handlers keyed
143
+ * like the config's descriptor records.
144
+ */
145
+ export type Actions<T> = {
146
+ fetch: (params: BrowserFetchParams, service: Service<T>) => Promise<BrowserFetchResult<T>>;
147
+ actions?: Record<string, (row: T) => void>;
148
+ folderActions?: Record<string, (folder: BrowserFolder) => void>;
149
+ bulkActions?: Record<string, (selected: Set<string>) => void>;
150
+ };
151
+ /**
152
+ * The events of a browser service, keyed by name. Each value is the
153
+ * argument list of a listener. The service emits after the write lands, so
154
+ * a listener reads the new state.
155
+ *
156
+ * - `change`: any field of the container was written.
157
+ * - `updated`: a fetch settled, success or failure. `path` is the key trail,
158
+ * `folders`/`files` the section lengths.
159
+ * - `navigated`: the trail changed through `open` or `navigate`.
160
+ */
161
+ export type Events<T> = {
162
+ change: [change: Change<T>];
163
+ updated: [event: {
164
+ id: string;
165
+ path: string[];
166
+ folders: number;
167
+ files: number;
168
+ }];
169
+ navigated: [event: {
170
+ id: string;
171
+ path: string[];
172
+ }];
173
+ };
174
+ export type Event<T> = keyof Events<T>;
175
+ export type Listener<T, E extends Event<T>> = Listener$1<Events<T>, E>;
176
+ /**
177
+ * The imperative contract: unwrapped state + derived + the full method
178
+ * surface. `BrowserService<T>` declares `implements` against this, and the
179
+ * fetch action receives it.
180
+ */
181
+ export type Service<T> = {
182
+ readonly id: string;
183
+ readonly config: Config<T>;
184
+ readonly columns: BrowserColumn<T>[];
185
+ readonly fileKey: keyof T;
186
+ readonly actions: FileAction<T>[];
187
+ readonly folderActions: FolderAction[];
188
+ readonly bulkActions: BulkAction[];
189
+ readonly rootLabel: string;
190
+ readonly locale: string;
191
+ readonly currency: string;
192
+ readonly folders: BrowserFolder[];
193
+ readonly files: T[];
194
+ readonly loading: boolean;
195
+ readonly initialized: boolean;
196
+ readonly path: BrowserCrumb[];
197
+ readonly sortField: string | null;
198
+ readonly sortDirection: SortDirection;
199
+ readonly selected: Set<string>;
200
+ readonly pathKeys: string[];
201
+ readonly depth: number;
202
+ /**
203
+ * The breadcrumb trail: the root crumb, then one crumb per trail step.
204
+ * Each crumb's key is its joined path prefix, unique even when one folder
205
+ * key recurs at several depths; its index is what `navigate` takes.
206
+ */
207
+ readonly trail: BrowserCrumb[];
208
+ readonly isAllSelected: boolean;
209
+ readonly isIndeterminate: boolean;
210
+ readonly selectAllState: boolean | "indeterminate";
211
+ readonly colSpan: number;
212
+ readonly hasSelection: boolean;
213
+ readonly isSelectable: boolean;
214
+ readonly hasActions: boolean;
215
+ readonly hasFolderActions: boolean;
216
+ /**
217
+ * Whether the current folder has settled with neither folders nor files.
218
+ * Gated on `initialized` and `loading`, so a view shows the listing, not
219
+ * the empty state, until the first fetch resolves.
220
+ */
221
+ readonly isEmpty: boolean;
222
+ open(folder: BrowserFolder): void;
223
+ navigate(index: number): void;
224
+ sortBy(field: string): void;
225
+ sortFieldFor(col: BrowserColumn<T>): string;
226
+ isSorted(col: BrowserColumn<T>): boolean;
227
+ keyOf(row: T): string;
228
+ toggleRow(key: string): void;
229
+ toggleAll(): void;
230
+ clearSelection(): void;
231
+ isRowSelected(row: T): boolean;
232
+ runAction(key: string, row: T): void;
233
+ runFolderAction(key: string, folder: BrowserFolder): void;
234
+ runBulkAction(key: string): void;
235
+ init(): Promise<boolean>;
236
+ fetch(): Promise<void>;
237
+ /**
238
+ * Subscribes a listener to an event. Listeners run in subscription order.
239
+ * An error in a listener propagates to the caller of the write. Returns a
240
+ * function that removes the listener.
241
+ */
242
+ on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
243
+ };
244
+ /** Default file sort direction. */
245
+ export declare const BROWSER_DEFAULT_SORT_DIRECTION: SortDirection;
246
+ /** The key of the root crumb: the trail's first entry, above every folder. */
247
+ export declare const BROWSER_ROOT_KEY = "";
248
+ /** The label of the root crumb when the config names none. */
249
+ export declare const BROWSER_DEFAULT_ROOT_LABEL = "Home";
250
+ /** The locale cells format in when the config names none. */
251
+ export declare const BROWSER_DEFAULT_LOCALE = "en-US";
252
+ /** The currency code currency cells format in when the config names none. */
253
+ export declare const BROWSER_DEFAULT_CURRENCY = "USD";
254
+ /**
255
+ * The fields of the container, for key-driven iteration independent of a
256
+ * container's runtime keys.
257
+ */
258
+ export declare const BROWSER_STATE_FIELDS: Record<StateKey, null>;
259
+ /**
260
+ * Builds the initial state container. The result is a plain object. Wrap it
261
+ * in a reactive proxy to track reads and writes. The trail starts at the
262
+ * root and sorting starts unset, so no config is needed.
263
+ */
264
+ export declare const createBrowserState: <T>() => State<T>;
265
+ /**
266
+ * The headless browser machine. Owns the folder trail, the file sort, and
267
+ * the file selection; delegates data loading to `wiring.fetch` and joins
268
+ * the config's action descriptors to the handlers in `wiring`.
269
+ *
270
+ * The service reads and writes the `state` container through the `options`
271
+ * middleware. The container can be a plain object or a reactive proxy; the
272
+ * service replaces each member as a whole. Each write emits `change`, and
273
+ * the domain methods emit `updated` and `navigated`. `on` subscribes.
274
+ */
275
+ export declare class BrowserService<T> implements Service<T> {
276
+ readonly id: string;
277
+ readonly config: Config<T>;
278
+ private readonly wiring;
279
+ readonly columns: BrowserColumn<T>[];
280
+ readonly fileKey: keyof T;
281
+ readonly actions: Service<T>["actions"];
282
+ readonly folderActions: Service<T>["folderActions"];
283
+ readonly bulkActions: Service<T>["bulkActions"];
284
+ private readonly state;
285
+ private readonly emitter;
286
+ constructor(id: string, config: Config<T>, state: State<T>, wiring: Actions<T>, options?: Options<T>);
287
+ on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
288
+ get folders(): BrowserFolder[];
289
+ get files(): T[];
290
+ get loading(): boolean;
291
+ get initialized(): boolean;
292
+ get path(): BrowserCrumb[];
293
+ get sortField(): string | null;
294
+ get sortDirection(): SortDirection;
295
+ get selected(): Set<string>;
296
+ get rootLabel(): string;
297
+ get locale(): string;
298
+ get currency(): string;
299
+ get pathKeys(): string[];
300
+ get depth(): number;
301
+ get trail(): BrowserCrumb[];
302
+ get isAllSelected(): boolean;
303
+ get isIndeterminate(): boolean;
304
+ get selectAllState(): boolean | "indeterminate";
305
+ get colSpan(): number;
306
+ get hasSelection(): boolean;
307
+ get isSelectable(): boolean;
308
+ get hasActions(): boolean;
309
+ get hasFolderActions(): boolean;
310
+ get isEmpty(): boolean;
311
+ keyOf(row: T): string;
312
+ /** Descends into a folder: push its crumb, drop the selection, refetch. */
313
+ open(folder: BrowserFolder): void;
314
+ /**
315
+ * Jumps to a trail position: `0` is the root, `depth` is where the
316
+ * browser already stands (a no-op, like clicking the current crumb).
317
+ */
318
+ navigate(index: number): void;
319
+ sortBy(field: string): void;
320
+ sortFieldFor(col: BrowserColumn<T>): string;
321
+ isSorted(col: BrowserColumn<T>): boolean;
322
+ toggleRow(key: string): void;
323
+ toggleAll(): void;
324
+ clearSelection(): void;
325
+ isRowSelected(row: T): boolean;
326
+ runAction(key: string, row: T): void;
327
+ runFolderAction(key: string, folder: BrowserFolder): void;
328
+ runBulkAction(key: string): void;
329
+ init(): Promise<boolean>;
330
+ fetch(): Promise<void>;
331
+ }
@@ -0,0 +1,236 @@
1
+ import { entries } from "objectively";
2
+ import { makeEmitter, makeProxy } from "@zoobzio/kit";
3
+ const BROWSER_DEFAULT_SORT_DIRECTION = "asc";
4
+ const BROWSER_ROOT_KEY = "";
5
+ const BROWSER_DEFAULT_ROOT_LABEL = "Home";
6
+ const BROWSER_DEFAULT_LOCALE = "en-US";
7
+ const BROWSER_DEFAULT_CURRENCY = "USD";
8
+ const BROWSER_STATE_FIELDS = {
9
+ folders: null,
10
+ files: null,
11
+ loading: null,
12
+ initialized: null,
13
+ path: null,
14
+ sortField: null,
15
+ sortDirection: null,
16
+ selected: null
17
+ };
18
+ const createBrowserState = () => ({
19
+ folders: [],
20
+ files: [],
21
+ loading: false,
22
+ initialized: false,
23
+ path: [],
24
+ sortField: null,
25
+ sortDirection: "asc",
26
+ selected: /* @__PURE__ */ new Set()
27
+ });
28
+ const sortFolders = (folders) => [...folders].sort((a, b) => a.label.localeCompare(b.label));
29
+ var BrowserService = class {
30
+ id;
31
+ config;
32
+ wiring;
33
+ columns;
34
+ fileKey;
35
+ actions;
36
+ folderActions;
37
+ bulkActions;
38
+ state;
39
+ emitter = makeEmitter();
40
+ constructor(id, config, state, wiring, options = {}) {
41
+ this.id = id;
42
+ this.config = config;
43
+ this.wiring = wiring;
44
+ this.state = makeProxy(state, BROWSER_STATE_FIELDS, options, this.emitter);
45
+ this.columns = config.columns;
46
+ this.fileKey = config.fileKey;
47
+ this.actions = entries(config.actions ?? {}).map(([key, descriptor]) => ({
48
+ ...descriptor,
49
+ key,
50
+ action: (row) => this.runAction(key, row)
51
+ }));
52
+ this.folderActions = entries(config.folderActions ?? {}).map(([key, descriptor]) => ({
53
+ ...descriptor,
54
+ key,
55
+ action: (folder) => this.runFolderAction(key, folder)
56
+ }));
57
+ this.bulkActions = entries(config.bulkActions ?? {}).map(([key, descriptor]) => ({
58
+ ...descriptor,
59
+ key,
60
+ action: (selected) => wiring.bulkActions?.[key]?.(selected)
61
+ }));
62
+ }
63
+ on(event, listener) {
64
+ return this.emitter.on(event, listener);
65
+ }
66
+ get folders() {
67
+ return this.state.folders;
68
+ }
69
+ get files() {
70
+ return this.state.files;
71
+ }
72
+ get loading() {
73
+ return this.state.loading;
74
+ }
75
+ get initialized() {
76
+ return this.state.initialized;
77
+ }
78
+ get path() {
79
+ return this.state.path;
80
+ }
81
+ get sortField() {
82
+ return this.state.sortField;
83
+ }
84
+ get sortDirection() {
85
+ return this.state.sortDirection;
86
+ }
87
+ get selected() {
88
+ return this.state.selected;
89
+ }
90
+ get rootLabel() {
91
+ return this.config.rootLabel ?? "Home";
92
+ }
93
+ get locale() {
94
+ return this.config.locale ?? "en-US";
95
+ }
96
+ get currency() {
97
+ return this.config.currency ?? "USD";
98
+ }
99
+ get pathKeys() {
100
+ return this.path.map((crumb) => crumb.key);
101
+ }
102
+ get depth() {
103
+ return this.path.length;
104
+ }
105
+ get trail() {
106
+ const keys = this.pathKeys;
107
+ return [{
108
+ key: "",
109
+ label: this.rootLabel
110
+ }, ...this.path.map((crumb, index) => ({
111
+ key: keys.slice(0, index + 1).join("/"),
112
+ label: crumb.label
113
+ }))];
114
+ }
115
+ get isAllSelected() {
116
+ if (!this.files.length) return false;
117
+ return this.files.every((row) => this.selected.has(this.keyOf(row)));
118
+ }
119
+ get isIndeterminate() {
120
+ if (!this.selected.size) return false;
121
+ return !this.isAllSelected;
122
+ }
123
+ get selectAllState() {
124
+ if (this.isIndeterminate) return "indeterminate";
125
+ return this.isAllSelected;
126
+ }
127
+ get colSpan() {
128
+ return this.columns.length + (this.hasActions || this.hasFolderActions ? 1 : 0) + (this.isSelectable ? 1 : 0);
129
+ }
130
+ get hasSelection() {
131
+ return this.selected.size > 0;
132
+ }
133
+ get isSelectable() {
134
+ return this.bulkActions.length > 0;
135
+ }
136
+ get hasActions() {
137
+ return this.actions.length > 0;
138
+ }
139
+ get hasFolderActions() {
140
+ return this.folderActions.length > 0;
141
+ }
142
+ get isEmpty() {
143
+ return this.initialized && !this.loading && !this.folders.length && !this.files.length;
144
+ }
145
+ keyOf(row) {
146
+ return String(row[this.config.fileKey]);
147
+ }
148
+ open(folder) {
149
+ this.state.path = [...this.path, {
150
+ key: folder.key,
151
+ label: folder.label
152
+ }];
153
+ this.state.selected = /* @__PURE__ */ new Set();
154
+ this.fetch();
155
+ this.emitter.emit("navigated", {
156
+ id: this.id,
157
+ path: this.pathKeys
158
+ });
159
+ }
160
+ navigate(index) {
161
+ if (index < 0 || index >= this.depth) return;
162
+ this.state.path = this.path.slice(0, index);
163
+ this.state.selected = /* @__PURE__ */ new Set();
164
+ this.fetch();
165
+ this.emitter.emit("navigated", {
166
+ id: this.id,
167
+ path: this.pathKeys
168
+ });
169
+ }
170
+ sortBy(field) {
171
+ if (this.sortField === field) this.state.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
172
+ else {
173
+ this.state.sortField = field;
174
+ this.state.sortDirection = "asc";
175
+ }
176
+ this.fetch();
177
+ }
178
+ sortFieldFor(col) {
179
+ return col.sortKey ?? String(col.key);
180
+ }
181
+ isSorted(col) {
182
+ return this.sortField === this.sortFieldFor(col);
183
+ }
184
+ toggleRow(key) {
185
+ const next = new Set(this.selected);
186
+ if (next.has(key)) next.delete(key);
187
+ else next.add(key);
188
+ this.state.selected = next;
189
+ }
190
+ toggleAll() {
191
+ if (this.isAllSelected) this.state.selected = /* @__PURE__ */ new Set();
192
+ else this.state.selected = new Set(this.files.map((row) => this.keyOf(row)));
193
+ }
194
+ clearSelection() {
195
+ this.state.selected = /* @__PURE__ */ new Set();
196
+ }
197
+ isRowSelected(row) {
198
+ return this.selected.has(this.keyOf(row));
199
+ }
200
+ runAction(key, row) {
201
+ this.wiring.actions?.[key]?.(row);
202
+ }
203
+ runFolderAction(key, folder) {
204
+ this.wiring.folderActions?.[key]?.(folder);
205
+ }
206
+ runBulkAction(key) {
207
+ this.wiring.bulkActions?.[key]?.(this.selected);
208
+ }
209
+ async init() {
210
+ if (this.initialized) return true;
211
+ this.state.initialized = true;
212
+ await this.fetch();
213
+ return true;
214
+ }
215
+ async fetch() {
216
+ this.state.loading = true;
217
+ try {
218
+ const result = await this.wiring.fetch({
219
+ path: this.pathKeys,
220
+ sortField: this.sortField,
221
+ sortDirection: this.sortDirection
222
+ }, this);
223
+ this.state.folders = sortFolders(result.folders);
224
+ this.state.files = result.files;
225
+ } finally {
226
+ this.state.loading = false;
227
+ this.emitter.emit("updated", {
228
+ id: this.id,
229
+ path: this.pathKeys,
230
+ folders: this.folders.length,
231
+ files: this.files.length
232
+ });
233
+ }
234
+ }
235
+ };
236
+ export { BROWSER_DEFAULT_CURRENCY, BROWSER_DEFAULT_LOCALE, BROWSER_DEFAULT_ROOT_LABEL, BROWSER_DEFAULT_SORT_DIRECTION, BROWSER_ROOT_KEY, BROWSER_STATE_FIELDS, BrowserService, createBrowserState };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alexander Thorwaldson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,87 @@
1
- # Temporary Holding Version
1
+ # @zoobzio/browser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Headless data browser service. Pure TypeScript, no framework dependency.
4
+
5
+ Owns: the folder trail (open a folder, jump to a crumb), the file sort,
6
+ the file selection, and the join of action descriptors to their handlers.
7
+ Delegates: data loading (`fetch`) and state storage (the container).
8
+ Folders always come back alphabetical; file order is the fetch's.
9
+
10
+ The package defines its own column and action shapes (`BrowserColumn`,
11
+ `ActionDescriptor`, `FileAction`, `FolderAction`, `BulkAction`,
12
+ `SortDirection`); it depends on no other package.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { BrowserService, createBrowserState } from "@zoobzio/browser";
18
+
19
+ const config = {
20
+ columns: [{ key: "name", label: "Name", sortable: true }],
21
+ fileKey: "id",
22
+ actions: { download: { label: "Download" } },
23
+ };
24
+
25
+ const browser = new BrowserService("assets", config, createBrowserState(), {
26
+ fetch: async (params) => api.assets(params), // { folders, files }
27
+ actions: { download: (file) => save(file) },
28
+ });
29
+
30
+ await browser.init();
31
+ browser.open({ key: "media", label: "Media" }); // descend, refetch
32
+ browser.navigate(0); // back to the root
33
+ browser.sortBy("name"); // asc, then desc on repeat
34
+ browser.toggleRow("1");
35
+ browser.runAction("download", file); // dispatch a file action by key
36
+ browser.runBulkAction("purge"); // a bulk action over the selection
37
+ ```
38
+
39
+ ## The trail
40
+
41
+ `trail` is the breadcrumb from the root: the root crumb (`rootLabel`,
42
+ default `Home`) followed by one crumb per step of `path`. Each crumb's key
43
+ is its joined path prefix, so keys stay unique when one folder key recurs
44
+ at several depths, and a crumb's index in `trail` is what `navigate` takes.
45
+
46
+ ```ts
47
+ browser.trail; // [{ key: "", label: "Home" }, { key: "media", label: "Media" }]
48
+ browser.navigate(browser.trail.findIndex((c) => c.key === "media"));
49
+ ```
50
+
51
+ ## The state container
52
+
53
+ The service reads and writes the `state` container: the listing, the load
54
+ flags, the trail, the file sort, and the selection. The container can be a
55
+ plain object or a reactive proxy. The service replaces each member as a
56
+ whole, so a proxy tracks every change.
57
+
58
+ ```ts
59
+ createBrowserState(); // plain: tests, Node
60
+ reactive(createBrowserState()); // Vue
61
+ useState(`browser-${id}`, () => createBrowserState()).value; // Nuxt, shared by id
62
+ ```
63
+
64
+ ## Middleware
65
+
66
+ `options.get.state.<field>` and `options.set.state.<field>` receive the value
67
+ of the field and return the value the service uses. A missing slot passes the
68
+ value on.
69
+
70
+ ```ts
71
+ new BrowserService(id, config, state, wiring, {
72
+ set: { state: { sortField: (field) => field?.toLowerCase() ?? null } },
73
+ });
74
+ ```
75
+
76
+ ## Events
77
+
78
+ `on(event, listener)` subscribes. Listeners run in subscription order, after
79
+ the write lands. The return value removes the listener.
80
+
81
+ - `change`: a field of the container was written. `{ field, before, after }`, narrowed by `field`.
82
+ - `updated`: a fetch settled, success or failure. `{ id, path, folders, files }` — the key trail and section lengths.
83
+ - `navigated`: the trail changed through `open` or `navigate`. `{ id, path }`.
84
+
85
+ ```ts
86
+ const off = browser.on("navigated", ({ path }) => history.push(path));
87
+ ```
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@zoobzio/browser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "Headless data browser service: folder trail navigation, file sort, selection, action joins. No framework dependency.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Alex Thorwaldson",
8
+ "email": "alex@zoobz.io",
9
+ "url": "https://zoobz.io"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/zoobzio/foundation.git",
14
+ "directory": "packages/browser"
15
+ },
16
+ "files": [
17
+ ".dist"
18
+ ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "main": "./.dist/index.mjs",
22
+ "types": "./.dist/index.d.mts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./.dist/index.d.mts",
26
+ "import": "./.dist/index.mjs"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dependencies": {
31
+ "objectively": "^0.3.0",
32
+ "@zoobzio/kit": "0.0.0"
33
+ },
34
+ "scripts": {
35
+ "build": "obuild",
36
+ "stub": "obuild --stub",
37
+ "lint": "oxlint",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run"
40
+ }
6
41
  }