@sepale/components 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -3
  3. package/dist/components/Accordion.svelte +48 -0
  4. package/dist/components/Accordion.svelte.d.ts +24 -0
  5. package/dist/components/Alert.svelte +107 -0
  6. package/dist/components/Alert.svelte.d.ts +40 -0
  7. package/dist/components/Avatar.svelte +75 -0
  8. package/dist/components/Avatar.svelte.d.ts +37 -0
  9. package/dist/components/Badge.svelte +46 -0
  10. package/dist/components/Badge.svelte.d.ts +22 -0
  11. package/dist/components/Breadcrumbs.svelte +36 -0
  12. package/dist/components/Breadcrumbs.svelte.d.ts +18 -0
  13. package/dist/components/Bubble.svelte +43 -0
  14. package/dist/components/Bubble.svelte.d.ts +23 -0
  15. package/dist/components/Button.svelte +132 -0
  16. package/dist/components/Button.svelte.d.ts +40 -0
  17. package/dist/components/Calendar.svelte +193 -0
  18. package/dist/components/Calendar.svelte.d.ts +60 -0
  19. package/dist/components/Card.svelte +69 -0
  20. package/dist/components/Card.svelte.d.ts +37 -0
  21. package/dist/components/Carousel.svelte +550 -0
  22. package/dist/components/Carousel.svelte.d.ts +124 -0
  23. package/dist/components/Checkbox.svelte +48 -0
  24. package/dist/components/Checkbox.svelte.d.ts +26 -0
  25. package/dist/components/Chip.svelte +60 -0
  26. package/dist/components/Chip.svelte.d.ts +26 -0
  27. package/dist/components/ChipsInput.svelte +158 -0
  28. package/dist/components/ChipsInput.svelte.d.ts +64 -0
  29. package/dist/components/Collapse.svelte +101 -0
  30. package/dist/components/Collapse.svelte.d.ts +46 -0
  31. package/dist/components/ColorPicker.svelte +135 -0
  32. package/dist/components/ColorPicker.svelte.d.ts +44 -0
  33. package/dist/components/Combobox.svelte +496 -0
  34. package/dist/components/Combobox.svelte.d.ts +96 -0
  35. package/dist/components/DataControl.svelte +754 -0
  36. package/dist/components/DataControl.svelte.d.ts +143 -0
  37. package/dist/components/DataHandler.svelte +67 -0
  38. package/dist/components/DataHandler.svelte.d.ts +44 -0
  39. package/dist/components/DatePicker.svelte +185 -0
  40. package/dist/components/DatePicker.svelte.d.ts +56 -0
  41. package/dist/components/Dialog.svelte +194 -0
  42. package/dist/components/Dialog.svelte.d.ts +51 -0
  43. package/dist/components/Divider.svelte +54 -0
  44. package/dist/components/Divider.svelte.d.ts +27 -0
  45. package/dist/components/Dock.svelte +34 -0
  46. package/dist/components/Dock.svelte.d.ts +18 -0
  47. package/dist/components/Drawer.svelte +174 -0
  48. package/dist/components/Drawer.svelte.d.ts +51 -0
  49. package/dist/components/Dropdown.svelte +183 -0
  50. package/dist/components/Dropdown.svelte.d.ts +52 -0
  51. package/dist/components/Editable.svelte +292 -0
  52. package/dist/components/Editable.svelte.d.ts +113 -0
  53. package/dist/components/EmptyState.svelte +64 -0
  54. package/dist/components/EmptyState.svelte.d.ts +29 -0
  55. package/dist/components/Fieldset.svelte +42 -0
  56. package/dist/components/Fieldset.svelte.d.ts +20 -0
  57. package/dist/components/FileDropzone.svelte +154 -0
  58. package/dist/components/FileDropzone.svelte.d.ts +45 -0
  59. package/dist/components/FileExplorer.svelte +643 -0
  60. package/dist/components/FileExplorer.svelte.d.ts +114 -0
  61. package/dist/components/FileInput.svelte +125 -0
  62. package/dist/components/FileInput.svelte.d.ts +51 -0
  63. package/dist/components/FloatingLabel.svelte +38 -0
  64. package/dist/components/FloatingLabel.svelte.d.ts +24 -0
  65. package/dist/components/Footer.svelte +68 -0
  66. package/dist/components/Footer.svelte.d.ts +37 -0
  67. package/dist/components/Grid.svelte +122 -0
  68. package/dist/components/Grid.svelte.d.ts +72 -0
  69. package/dist/components/Hero.svelte +62 -0
  70. package/dist/components/Hero.svelte.d.ts +33 -0
  71. package/dist/components/Input.svelte +184 -0
  72. package/dist/components/Input.svelte.d.ts +65 -0
  73. package/dist/components/Join.svelte +38 -0
  74. package/dist/components/Join.svelte.d.ts +21 -0
  75. package/dist/components/Kanban.svelte +284 -0
  76. package/dist/components/Kanban.svelte.d.ts +93 -0
  77. package/dist/components/Label.svelte +32 -0
  78. package/dist/components/Label.svelte.d.ts +21 -0
  79. package/dist/components/LaptopFrame.svelte +44 -0
  80. package/dist/components/LaptopFrame.svelte.d.ts +21 -0
  81. package/dist/components/List.svelte +95 -0
  82. package/dist/components/List.svelte.d.ts +69 -0
  83. package/dist/components/Loading.svelte +37 -0
  84. package/dist/components/Loading.svelte.d.ts +17 -0
  85. package/dist/components/Menu.svelte +144 -0
  86. package/dist/components/Menu.svelte.d.ts +53 -0
  87. package/dist/components/MobileFrame.svelte +41 -0
  88. package/dist/components/MobileFrame.svelte.d.ts +21 -0
  89. package/dist/components/Navbar.svelte +49 -0
  90. package/dist/components/Navbar.svelte.d.ts +25 -0
  91. package/dist/components/NumberInput.svelte +255 -0
  92. package/dist/components/NumberInput.svelte.d.ts +68 -0
  93. package/dist/components/Pagination.svelte +169 -0
  94. package/dist/components/Pagination.svelte.d.ts +50 -0
  95. package/dist/components/Progress.svelte +65 -0
  96. package/dist/components/Progress.svelte.d.ts +28 -0
  97. package/dist/components/RadialProgress.svelte +48 -0
  98. package/dist/components/RadialProgress.svelte.d.ts +20 -0
  99. package/dist/components/Radio.svelte +50 -0
  100. package/dist/components/Radio.svelte.d.ts +28 -0
  101. package/dist/components/Range.svelte +53 -0
  102. package/dist/components/Range.svelte.d.ts +26 -0
  103. package/dist/components/Rating.svelte +90 -0
  104. package/dist/components/Rating.svelte.d.ts +41 -0
  105. package/dist/components/Select.svelte +338 -0
  106. package/dist/components/Select.svelte.d.ts +74 -0
  107. package/dist/components/Sidebar.svelte +100 -0
  108. package/dist/components/Sidebar.svelte.d.ts +54 -0
  109. package/dist/components/Skeleton.svelte +40 -0
  110. package/dist/components/Skeleton.svelte.d.ts +18 -0
  111. package/dist/components/Stat.svelte +71 -0
  112. package/dist/components/Stat.svelte.d.ts +35 -0
  113. package/dist/components/Stats.svelte +38 -0
  114. package/dist/components/Stats.svelte.d.ts +22 -0
  115. package/dist/components/Steps.svelte +204 -0
  116. package/dist/components/Steps.svelte.d.ts +66 -0
  117. package/dist/components/Subtitle.svelte +44 -0
  118. package/dist/components/Subtitle.svelte.d.ts +26 -0
  119. package/dist/components/Swatch.svelte +61 -0
  120. package/dist/components/Swatch.svelte.d.ts +27 -0
  121. package/dist/components/Switch.svelte +105 -0
  122. package/dist/components/Switch.svelte.d.ts +44 -0
  123. package/dist/components/Table.svelte +243 -0
  124. package/dist/components/Table.svelte.d.ts +121 -0
  125. package/dist/components/Tabs.svelte +142 -0
  126. package/dist/components/Tabs.svelte.d.ts +80 -0
  127. package/dist/components/Text.svelte +43 -0
  128. package/dist/components/Text.svelte.d.ts +25 -0
  129. package/dist/components/Textarea.svelte +76 -0
  130. package/dist/components/Textarea.svelte.d.ts +39 -0
  131. package/dist/components/Title.svelte +46 -0
  132. package/dist/components/Title.svelte.d.ts +29 -0
  133. package/dist/components/Toast.svelte +154 -0
  134. package/dist/components/Toast.svelte.d.ts +49 -0
  135. package/dist/components/Toggle.svelte +48 -0
  136. package/dist/components/Toggle.svelte.d.ts +25 -0
  137. package/dist/components/Tooltip.svelte +70 -0
  138. package/dist/components/Tooltip.svelte.d.ts +34 -0
  139. package/dist/components/WindowFrame.svelte +53 -0
  140. package/dist/components/WindowFrame.svelte.d.ts +24 -0
  141. package/dist/components/accordion-context.d.ts +8 -0
  142. package/dist/components/accordion-context.js +25 -0
  143. package/dist/components/color.d.ts +39 -0
  144. package/dist/components/color.js +92 -0
  145. package/dist/components/data-handler.svelte.d.ts +362 -0
  146. package/dist/components/data-handler.svelte.js +690 -0
  147. package/dist/components/file-icons.d.ts +25 -0
  148. package/dist/components/file-icons.js +46 -0
  149. package/dist/family-types.d.ts +45 -0
  150. package/dist/family-types.js +73 -0
  151. package/dist/generated/classes.d.ts +17 -0
  152. package/dist/generated/classes.js +2511 -0
  153. package/dist/generated/components.d.ts +39 -0
  154. package/dist/generated/components.js +28666 -0
  155. package/dist/generated/registry.d.ts +35 -0
  156. package/dist/generated/registry.js +2658 -0
  157. package/dist/index.d.ts +73 -0
  158. package/dist/index.js +99 -0
  159. package/dist/sepale-types.d.ts +8 -0
  160. package/dist/sepale-types.js +200 -0
  161. package/dist/theme.d.ts +123 -0
  162. package/dist/theme.js +226 -0
  163. package/dist/variants.d.ts +124 -0
  164. package/dist/variants.js +70 -0
  165. package/dist/vendor/air-datepicker/LICENSE.md +21 -0
  166. package/dist/vendor/air-datepicker/README.md +19 -0
  167. package/dist/vendor/air-datepicker/buttonPresets.d.ts +13 -0
  168. package/dist/vendor/air-datepicker/buttonPresets.js +12 -0
  169. package/dist/vendor/air-datepicker/consts.d.ts +18 -0
  170. package/dist/vendor/air-datepicker/consts.js +35 -0
  171. package/dist/vendor/air-datepicker/datepicker.d.ts +289 -0
  172. package/dist/vendor/air-datepicker/datepicker.js +1435 -0
  173. package/dist/vendor/air-datepicker/datepickerBody.d.ts +46 -0
  174. package/dist/vendor/air-datepicker/datepickerBody.js +386 -0
  175. package/dist/vendor/air-datepicker/datepickerButtons.d.ts +25 -0
  176. package/dist/vendor/air-datepicker/datepickerButtons.js +85 -0
  177. package/dist/vendor/air-datepicker/datepickerCell.d.ts +35 -0
  178. package/dist/vendor/air-datepicker/datepickerCell.js +297 -0
  179. package/dist/vendor/air-datepicker/datepickerKeyboard.d.ts +26 -0
  180. package/dist/vendor/air-datepicker/datepickerKeyboard.js +207 -0
  181. package/dist/vendor/air-datepicker/datepickerNav.d.ts +31 -0
  182. package/dist/vendor/air-datepicker/datepickerNav.js +200 -0
  183. package/dist/vendor/air-datepicker/datepickerTime.d.ts +60 -0
  184. package/dist/vendor/air-datepicker/datepickerTime.js +276 -0
  185. package/dist/vendor/air-datepicker/defaults.d.ts +65 -0
  186. package/dist/vendor/air-datepicker/defaults.js +79 -0
  187. package/dist/vendor/air-datepicker/index.d.ts +168 -0
  188. package/dist/vendor/air-datepicker/index.js +3 -0
  189. package/dist/vendor/air-datepicker/locale/ar.d.ts +4 -0
  190. package/dist/vendor/air-datepicker/locale/ar.js +14 -0
  191. package/dist/vendor/air-datepicker/locale/bg.d.ts +4 -0
  192. package/dist/vendor/air-datepicker/locale/bg.js +14 -0
  193. package/dist/vendor/air-datepicker/locale/ca.d.ts +4 -0
  194. package/dist/vendor/air-datepicker/locale/ca.js +14 -0
  195. package/dist/vendor/air-datepicker/locale/cs.d.ts +4 -0
  196. package/dist/vendor/air-datepicker/locale/cs.js +14 -0
  197. package/dist/vendor/air-datepicker/locale/da.d.ts +4 -0
  198. package/dist/vendor/air-datepicker/locale/da.js +14 -0
  199. package/dist/vendor/air-datepicker/locale/de.d.ts +4 -0
  200. package/dist/vendor/air-datepicker/locale/de.js +14 -0
  201. package/dist/vendor/air-datepicker/locale/el.d.ts +4 -0
  202. package/dist/vendor/air-datepicker/locale/el.js +14 -0
  203. package/dist/vendor/air-datepicker/locale/en.d.ts +4 -0
  204. package/dist/vendor/air-datepicker/locale/en.js +14 -0
  205. package/dist/vendor/air-datepicker/locale/es.d.ts +4 -0
  206. package/dist/vendor/air-datepicker/locale/es.js +14 -0
  207. package/dist/vendor/air-datepicker/locale/eu.d.ts +4 -0
  208. package/dist/vendor/air-datepicker/locale/eu.js +14 -0
  209. package/dist/vendor/air-datepicker/locale/fi.d.ts +4 -0
  210. package/dist/vendor/air-datepicker/locale/fi.js +14 -0
  211. package/dist/vendor/air-datepicker/locale/fr.d.ts +4 -0
  212. package/dist/vendor/air-datepicker/locale/fr.js +14 -0
  213. package/dist/vendor/air-datepicker/locale/hr.d.ts +4 -0
  214. package/dist/vendor/air-datepicker/locale/hr.js +14 -0
  215. package/dist/vendor/air-datepicker/locale/hu.d.ts +4 -0
  216. package/dist/vendor/air-datepicker/locale/hu.js +14 -0
  217. package/dist/vendor/air-datepicker/locale/id.d.ts +4 -0
  218. package/dist/vendor/air-datepicker/locale/id.js +14 -0
  219. package/dist/vendor/air-datepicker/locale/it.d.ts +4 -0
  220. package/dist/vendor/air-datepicker/locale/it.js +14 -0
  221. package/dist/vendor/air-datepicker/locale/ja.d.ts +4 -0
  222. package/dist/vendor/air-datepicker/locale/ja.js +14 -0
  223. package/dist/vendor/air-datepicker/locale/ko.d.ts +4 -0
  224. package/dist/vendor/air-datepicker/locale/ko.js +14 -0
  225. package/dist/vendor/air-datepicker/locale/nb.d.ts +4 -0
  226. package/dist/vendor/air-datepicker/locale/nb.js +14 -0
  227. package/dist/vendor/air-datepicker/locale/nl.d.ts +4 -0
  228. package/dist/vendor/air-datepicker/locale/nl.js +14 -0
  229. package/dist/vendor/air-datepicker/locale/pl.d.ts +4 -0
  230. package/dist/vendor/air-datepicker/locale/pl.js +14 -0
  231. package/dist/vendor/air-datepicker/locale/pt-BR.d.ts +4 -0
  232. package/dist/vendor/air-datepicker/locale/pt-BR.js +14 -0
  233. package/dist/vendor/air-datepicker/locale/pt.d.ts +4 -0
  234. package/dist/vendor/air-datepicker/locale/pt.js +14 -0
  235. package/dist/vendor/air-datepicker/locale/ro.d.ts +4 -0
  236. package/dist/vendor/air-datepicker/locale/ro.js +14 -0
  237. package/dist/vendor/air-datepicker/locale/ru.d.ts +4 -0
  238. package/dist/vendor/air-datepicker/locale/ru.js +14 -0
  239. package/dist/vendor/air-datepicker/locale/si.d.ts +4 -0
  240. package/dist/vendor/air-datepicker/locale/si.js +14 -0
  241. package/dist/vendor/air-datepicker/locale/sk.d.ts +4 -0
  242. package/dist/vendor/air-datepicker/locale/sk.js +14 -0
  243. package/dist/vendor/air-datepicker/locale/sl.d.ts +4 -0
  244. package/dist/vendor/air-datepicker/locale/sl.js +14 -0
  245. package/dist/vendor/air-datepicker/locale/sv.d.ts +4 -0
  246. package/dist/vendor/air-datepicker/locale/sv.js +14 -0
  247. package/dist/vendor/air-datepicker/locale/th.d.ts +4 -0
  248. package/dist/vendor/air-datepicker/locale/th.js +14 -0
  249. package/dist/vendor/air-datepicker/locale/tr.d.ts +4 -0
  250. package/dist/vendor/air-datepicker/locale/tr.js +16 -0
  251. package/dist/vendor/air-datepicker/locale/uk.d.ts +4 -0
  252. package/dist/vendor/air-datepicker/locale/uk.js +14 -0
  253. package/dist/vendor/air-datepicker/locale/zh.d.ts +4 -0
  254. package/dist/vendor/air-datepicker/locale/zh.js +14 -0
  255. package/dist/vendor/air-datepicker/typedefs.d.ts +3 -0
  256. package/dist/vendor/air-datepicker/typedefs.js +15 -0
  257. package/dist/vendor/air-datepicker/utils.d.ts +155 -0
  258. package/dist/vendor/air-datepicker/utils.js +395 -0
  259. package/dist/vendor/air-datepicker/withEvents.d.ts +1 -0
  260. package/dist/vendor/air-datepicker/withEvents.js +39 -0
  261. package/package.json +44 -3
@@ -0,0 +1,643 @@
1
+ <script module lang="ts">
2
+ /** What .file-explorer is made of, classified. The props follow the
3
+ families the project registers (variants.ts, ADR 0015). */
4
+ export const PREFIX = 'file-explorer';
5
+
6
+ export const AXES = {
7
+ size: ['xs', 'sm', 'md', 'lg', 'xl']
8
+ } as const;
9
+
10
+ export const FLAGS = {} as const;
11
+ export const PARTS = [
12
+ 'toolbar',
13
+ 'path',
14
+ 'actions',
15
+ 'views',
16
+ 'view',
17
+ 'main',
18
+ 'tree',
19
+ 'tree-item',
20
+ 'twisty',
21
+ 'content',
22
+ 'table',
23
+ 'grid',
24
+ 'item',
25
+ 'entry',
26
+ 'icon',
27
+ 'thumb',
28
+ 'name',
29
+ 'meta',
30
+ 'empty',
31
+ 'details',
32
+ 'menu'
33
+ ] as const;
34
+
35
+ /** A file or a folder. Anything else on it is yours: it travels to the snippets. */
36
+ export interface FileNode {
37
+ id: string | number;
38
+ name: string;
39
+ kind: 'folder' | 'file';
40
+ /** Bytes. */
41
+ size?: number;
42
+ modified?: Date;
43
+ /** A static tree: a folder's content, known up front. */
44
+ children?: FileNode[];
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ /** What a drop brings: nodes dragged inside the explorer, or files from the system. */
49
+ export type FileDrop = FileNode[] | File[];
50
+ </script>
51
+
52
+ <script lang="ts">
53
+ /**
54
+ * Files and folders in a hierarchy, Drive-like: the folder tree on the
55
+ * left, the current folder as a list or a grid, the details of the
56
+ * selection on the right, under a toolbar with the path and your actions.
57
+ *
58
+ * Folders load lazily through `load(folder)` — `null` is the top — or
59
+ * come from a static `root`. Selection: click, Ctrl/⌘ to add, Shift for a
60
+ * range; the keyboard walks the content (arrows, Enter, Backspace to go
61
+ * up). Double-click or Enter opens a folder, or calls `onOpen` on a file.
62
+ * Right-click shows the `menu` snippet.
63
+ *
64
+ * Drag and drop moves nothing by itself: `onDrag` may refuse a drag,
65
+ * `onDrop(items, target)` says what was dropped where — nodes from inside,
66
+ * or `File`s from the system — and the app does the work, then calls
67
+ * `refresh()`.
68
+ */
69
+ import { untrack, type Snippet } from 'svelte';
70
+ import type { HTMLAttributes } from 'svelte/elements';
71
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
72
+ import Breadcrumbs from './Breadcrumbs.svelte';
73
+ import { FILE_ICONS, IMAGE_EXTENSIONS, PAGE, extensionOf, type FileIcon } from './file-icons.js';
74
+ import { applyAxes, cx, type AxisProps } from '../variants.js';
75
+
76
+ type Folder = FileNode | null;
77
+ type SortKey = 'name' | 'modified' | 'size';
78
+
79
+ interface Props extends AxisProps<typeof AXES, typeof FLAGS, typeof PREFIX>, HTMLAttributes<HTMLDivElement> {
80
+ /** A static tree, from its top folder. */
81
+ root?: FileNode;
82
+ /** A folder's content, on demand. `null` is the top. Wins over `root`'s children. */
83
+ load?: (folder: Folder) => FileNode[] | Promise<FileNode[]>;
84
+ /** The top's name in the path and the tree, when there is no `root`. */
85
+ rootLabel?: string;
86
+ /** Show the folder tree. */
87
+ tree?: boolean;
88
+ /** The view it starts in. */
89
+ view?: 'list' | 'grid';
90
+ /** Your actions, in the toolbar. */
91
+ toolbar?: Snippet<[{ folder: Folder; selection: FileNode[] }]>;
92
+ /** The details pane, shown while something is selected. */
93
+ details?: Snippet<[FileNode[]]>;
94
+ /** The context menu's content. `close` closes it. */
95
+ menu?: Snippet<[{ items: FileNode[]; close: () => void }]>;
96
+ /** A node's icon, drawn by you. Wins over `icons` and `preview`. */
97
+ icon?: Snippet<[FileNode]>;
98
+ /**
99
+ * Icons by extension (`{ pdf: { glyph, color } }`), merged over the
100
+ * default table. `false` turns the table off: every file is a page.
101
+ */
102
+ icons?: Record<string, FileIcon> | false;
103
+ /**
104
+ * A picture for a file, or nothing. Defaults to the node's `thumbnail`,
105
+ * else its `url` when the extension is an image's. `false`: never.
106
+ */
107
+ preview?: ((node: FileNode) => string | undefined) | false;
108
+ /** Drawn in an empty folder. */
109
+ empty?: Snippet<[Folder]>;
110
+ /** A file was opened (double-click, Enter). */
111
+ onOpen?: (file: FileNode) => void;
112
+ /** The selection changed. */
113
+ onSelect?: (items: FileNode[]) => void;
114
+ /** Another folder is shown. */
115
+ onNavigate?: (folder: Folder) => void;
116
+ /** A drag starts. Return `false` to refuse it. */
117
+ onDrag?: (items: FileNode[], event: DragEvent) => boolean | void;
118
+ /** Something was dropped on a folder (`null`: the top). */
119
+ onDrop?: (items: FileDrop, target: Folder, event: DragEvent) => void;
120
+ /** The texts it writes. */
121
+ labels?: Partial<typeof LABELS>;
122
+ formatSize?: (bytes: number) => string;
123
+ formatDate?: (date: Date) => string;
124
+ class?: string;
125
+ }
126
+
127
+ const LABELS = {
128
+ name: 'Name',
129
+ modified: 'Modified',
130
+ size: 'Size',
131
+ list: 'List view',
132
+ grid: 'Grid view',
133
+ empty: 'This folder is empty',
134
+ tree: 'Folders',
135
+ path: 'Path',
136
+ loading: 'Loading…'
137
+ };
138
+
139
+ // The axes are deliberately NOT destructured: they travel in `axes` and are
140
+ // resolved by name, so an axis a family drops changes nothing down here.
141
+ let {
142
+ root,
143
+ load: source,
144
+ rootLabel = 'Files',
145
+ tree = true,
146
+ view: initialView = 'list',
147
+ toolbar,
148
+ details,
149
+ menu,
150
+ icon,
151
+ icons = {},
152
+ preview = defaultPreview,
153
+ empty,
154
+ onOpen,
155
+ onSelect,
156
+ onNavigate,
157
+ onDrag,
158
+ onDrop,
159
+ labels = {},
160
+ formatSize = defaultSize,
161
+ formatDate = (d: Date) => d.toLocaleDateString(),
162
+ class: klass = '',
163
+ ...axes
164
+ }: Props = $props();
165
+
166
+ let applied = $derived(applyAxes(PREFIX, AXES, FLAGS, axes));
167
+ let classes = $derived(cx(PREFIX, applied.classes, klass));
168
+ let t = $derived({ ...LABELS, ...labels });
169
+
170
+ let table = $derived(icons === false ? {} : { ...FILE_ICONS, ...icons });
171
+
172
+ function defaultPreview(node: FileNode): string | undefined {
173
+ if (typeof node.thumbnail === 'string') return node.thumbnail;
174
+ if (typeof node.url === 'string' && IMAGE_EXTENSIONS.has(extensionOf(node.name))) return node.url;
175
+ return undefined;
176
+ }
177
+
178
+ function defaultSize(n: number): string {
179
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
180
+ let i = 0;
181
+ while (n >= 1024 && i < units.length - 1) {
182
+ n /= 1024;
183
+ i++;
184
+ }
185
+ return `${i ? n.toFixed(n < 10 ? 1 : 0) : n} ${units[i]}`;
186
+ }
187
+
188
+ /* ── Loading ──────────────────────────────────────────────────────── */
189
+
190
+ const TOP = '\u0000top';
191
+ const keyOf = (f: Folder) => (f ? f.id : TOP);
192
+ let top: Folder = $derived(root ?? null);
193
+
194
+ /** Folder key → its content, once loaded. */
195
+ const cache = new SvelteMap<string | number, FileNode[]>();
196
+ const loading = new SvelteSet<string | number>();
197
+
198
+ async function fetchFolder(folder: Folder, force = false) {
199
+ const k = keyOf(folder);
200
+ if ((cache.has(k) && !force) || loading.has(k)) return;
201
+ if (!source && folder?.children) return void cache.set(k, folder.children);
202
+ if (!source) return void cache.set(k, []);
203
+ loading.add(k);
204
+ try {
205
+ cache.set(k, (await source(folder)) ?? []);
206
+ } catch {
207
+ cache.set(k, []);
208
+ } finally {
209
+ loading.delete(k);
210
+ }
211
+ }
212
+
213
+ /** Reload a folder (the current one by default) after the app changed it. */
214
+ export function refresh(folder?: Folder) {
215
+ void fetchFolder(folder === undefined ? current : folder, true);
216
+ }
217
+
218
+ /* ── Where we are ─────────────────────────────────────────────────── */
219
+
220
+ /** The folders from the top down to the current one (the top excluded). */
221
+ let path: FileNode[] = $state([]);
222
+ let current: Folder = $derived(path.length ? path[path.length - 1]! : top);
223
+
224
+ const expanded = new SvelteSet<string | number>();
225
+
226
+ $effect(() => {
227
+ void fetchFolder(current);
228
+ });
229
+
230
+ /** Show a folder, given the folders leading to it from the top. */
231
+ export function navigate(chain: FileNode[]) {
232
+ path = chain.filter((f) => f !== top);
233
+ for (const f of path) expanded.add(f.id);
234
+ clearSelection();
235
+ focusAt = -1;
236
+ onNavigate?.(current);
237
+ }
238
+
239
+ function up() {
240
+ if (path.length) navigate(path.slice(0, -1));
241
+ }
242
+
243
+ function toggleBranch(folder: FileNode) {
244
+ if (expanded.has(folder.id)) expanded.delete(folder.id);
245
+ else {
246
+ expanded.add(folder.id);
247
+ void fetchFolder(folder);
248
+ }
249
+ }
250
+
251
+ /* ── The content, sorted ──────────────────────────────────────────── */
252
+
253
+ /* Where it starts, read once: afterwards the toggle owns it. */
254
+ let view = $state<'list' | 'grid'>(untrack(() => initialView));
255
+ let sort = $state<{ key: SortKey; dir: 1 | -1 }>({ key: 'name', dir: 1 });
256
+
257
+ let items = $derived.by(() => {
258
+ const list = [...(cache.get(keyOf(current)) ?? [])];
259
+ const { key, dir } = sort;
260
+ const val = (n: FileNode) => (key === 'name' ? n.name.toLowerCase() : key === 'size' ? (n.size ?? -1) : (n.modified?.getTime() ?? 0));
261
+ return list.sort((a, b) => {
262
+ if (a.kind !== b.kind) return a.kind === 'folder' ? -1 : 1;
263
+ const x = val(a);
264
+ const y = val(b);
265
+ return (x < y ? -1 : x > y ? 1 : 0) * dir;
266
+ });
267
+ });
268
+
269
+ function sortBy(key: SortKey) {
270
+ sort = sort.key === key ? { key, dir: sort.dir === 1 ? -1 : 1 } : { key, dir: 1 };
271
+ }
272
+
273
+ /* ── Selection ────────────────────────────────────────────────────── */
274
+
275
+ const selected = new SvelteSet<string | number>();
276
+ let anchor = -1;
277
+ let focusAt = $state(-1);
278
+
279
+ let selection = $derived(items.filter((n) => selected.has(n.id)));
280
+
281
+ function changed() {
282
+ onSelect?.(items.filter((n) => selected.has(n.id)));
283
+ }
284
+
285
+ function clearSelection() {
286
+ if (!selected.size) return;
287
+ selected.clear();
288
+ changed();
289
+ }
290
+
291
+ export function getSelection(): FileNode[] {
292
+ return selection;
293
+ }
294
+
295
+ function selectAt(i: number, e: { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }) {
296
+ const node = items[i];
297
+ if (!node) return;
298
+ if (e.shiftKey && anchor >= 0) {
299
+ if (!(e.ctrlKey || e.metaKey)) selected.clear();
300
+ const [a, b] = anchor < i ? [anchor, i] : [i, anchor];
301
+ for (let n = a; n <= b; n++) selected.add(items[n]!.id);
302
+ } else if (e.ctrlKey || e.metaKey) {
303
+ if (selected.has(node.id)) selected.delete(node.id);
304
+ else selected.add(node.id);
305
+ anchor = i;
306
+ } else {
307
+ selected.clear();
308
+ selected.add(node.id);
309
+ anchor = i;
310
+ }
311
+ focusAt = i;
312
+ changed();
313
+ }
314
+
315
+ function open(node: FileNode) {
316
+ if (node.kind === 'folder') navigate([...path, node]);
317
+ else onOpen?.(node);
318
+ }
319
+
320
+ /* ── Keyboard ─────────────────────────────────────────────────────── */
321
+
322
+ let content: HTMLDivElement | undefined = $state();
323
+
324
+ function columns(): number {
325
+ if (view !== 'grid' || !content) return 1;
326
+ const grid = content.querySelector('.file-explorer-grid');
327
+ return grid ? getComputedStyle(grid).gridTemplateColumns.split(' ').length : 1;
328
+ }
329
+
330
+ function keydown(e: KeyboardEvent) {
331
+ const n = items.length;
332
+ const step = { ArrowDown: columns(), ArrowUp: -columns(), ArrowRight: view === 'grid' ? 1 : 0, ArrowLeft: view === 'grid' ? -1 : 0 }[e.key];
333
+ if (step) {
334
+ e.preventDefault();
335
+ const next = Math.min(n - 1, Math.max(0, (focusAt < 0 ? -1 : focusAt) + step));
336
+ selectAt(next, { ctrlKey: false, metaKey: false, shiftKey: e.shiftKey });
337
+ content?.querySelector(`[data-index="${next}"]`)?.scrollIntoView({ block: 'nearest' });
338
+ } else if (e.key === 'Enter' && focusAt >= 0) {
339
+ e.preventDefault();
340
+ open(items[focusAt]!);
341
+ } else if (e.key === 'Backspace') {
342
+ e.preventDefault();
343
+ up();
344
+ } else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
345
+ e.preventDefault();
346
+ for (const node of items) selected.add(node.id);
347
+ changed();
348
+ } else if (e.key === 'Escape') {
349
+ clearSelection();
350
+ }
351
+ }
352
+
353
+ /* ── Context menu ─────────────────────────────────────────────────── */
354
+
355
+ let menuAt = $state<{ x: number; y: number } | null>(null);
356
+ let menuItems: FileNode[] = $state([]);
357
+ let menuBox: HTMLDivElement | undefined = $state();
358
+
359
+ function contextmenu(e: MouseEvent, i: number) {
360
+ if (!menu) return;
361
+ e.preventDefault();
362
+ if (!selected.has(items[i]!.id)) selectAt(i, { ctrlKey: false, metaKey: false, shiftKey: false });
363
+ menuItems = items.filter((n) => selected.has(n.id));
364
+ menuAt = { x: e.clientX, y: e.clientY };
365
+ }
366
+
367
+ const closeMenu = () => (menuAt = null);
368
+
369
+ $effect(() => {
370
+ if (!menuAt) return;
371
+ const away = (e: Event) => {
372
+ if (!menuBox?.contains(e.target as Node)) closeMenu();
373
+ };
374
+ const esc = (e: KeyboardEvent) => e.key === 'Escape' && closeMenu();
375
+ window.addEventListener('pointerdown', away, true);
376
+ window.addEventListener('keydown', esc);
377
+ window.addEventListener('scroll', closeMenu, true);
378
+ return () => {
379
+ window.removeEventListener('pointerdown', away, true);
380
+ window.removeEventListener('keydown', esc);
381
+ window.removeEventListener('scroll', closeMenu, true);
382
+ };
383
+ });
384
+
385
+ /* Kept inside the viewport once its size is known. */
386
+ let menuStyle = $derived.by(() => {
387
+ if (!menuAt) return '';
388
+ const w = menuBox?.offsetWidth ?? 0;
389
+ const h = menuBox?.offsetHeight ?? 0;
390
+ const x = Math.min(menuAt.x, (typeof innerWidth === 'number' ? innerWidth : 1e4) - w - 4);
391
+ const y = Math.min(menuAt.y, (typeof innerHeight === 'number' ? innerHeight : 1e4) - h - 4);
392
+ return `left: ${Math.max(4, x)}px; top: ${Math.max(4, y)}px;`;
393
+ });
394
+
395
+ /* ── Drag and drop ────────────────────────────────────────────────── */
396
+
397
+ let dragging: FileNode[] | null = null;
398
+ let dropKey = $state<string | number | null>(null);
399
+
400
+ function dragstart(e: DragEvent, i: number) {
401
+ if (!selected.has(items[i]!.id)) selectAt(i, { ctrlKey: false, metaKey: false, shiftKey: false });
402
+ const moving = items.filter((n) => selected.has(n.id));
403
+ if (onDrag?.(moving, e) === false) return e.preventDefault();
404
+ dragging = moving;
405
+ e.dataTransfer?.setData('text/plain', moving.map((n) => n.name).join('\n'));
406
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
407
+ }
408
+
409
+ function dragend() {
410
+ dragging = null;
411
+ dropKey = null;
412
+ }
413
+
414
+ function accepts(target: Folder, e: DragEvent): boolean {
415
+ if (!onDrop) return false;
416
+ if (dragging) return !dragging.some((n) => n.id === target?.id) && !(target === current && dragging.every((n) => items.includes(n)));
417
+ return !!e.dataTransfer?.types.includes('Files');
418
+ }
419
+
420
+ function dragover(e: DragEvent, target: Folder) {
421
+ if (!accepts(target, e)) return;
422
+ e.preventDefault();
423
+ e.stopPropagation();
424
+ dropKey = keyOf(target);
425
+ }
426
+
427
+ function dragleave(e: DragEvent, target: Folder) {
428
+ if (dropKey === keyOf(target) && !(e.currentTarget as Element).contains(e.relatedTarget as Node)) dropKey = null;
429
+ }
430
+
431
+ function drop(e: DragEvent, target: Folder) {
432
+ if (!accepts(target, e)) return;
433
+ e.preventDefault();
434
+ e.stopPropagation();
435
+ const what: FileDrop = dragging ?? [...(e.dataTransfer?.files ?? [])];
436
+ dragging = null;
437
+ dropKey = null;
438
+ onDrop?.(what, target, e);
439
+ }
440
+
441
+ /** The handlers that make an element a drop target for `target`. */
442
+ const target = (f: Folder) => ({
443
+ ondragover: (e: DragEvent) => dragover(e, f),
444
+ ondragleave: (e: DragEvent) => dragleave(e, f),
445
+ ondrop: (e: DragEvent) => drop(e, f),
446
+ 'data-drop': dropKey !== null && dropKey === keyOf(f) ? '' : undefined
447
+ });
448
+
449
+ let showDetails = $derived(!!details && selection.length > 0);
450
+ </script>
451
+
452
+ {#snippet nodeIcon(node: FileNode)}
453
+ {@const picture = !icon && node.kind === 'file' && preview ? preview(node) : undefined}
454
+ {@const fileIcon = node.kind === 'file' ? table[extensionOf(node.name)] : undefined}
455
+ <span
456
+ class="file-explorer-icon"
457
+ data-kind={node.kind}
458
+ data-preview={picture ? '' : undefined}
459
+ style:--file-explorer-icon-color={fileIcon?.color}
460
+ aria-hidden="true"
461
+ >
462
+ {#if icon}
463
+ {@render icon(node)}
464
+ {:else if picture}
465
+ <img class="file-explorer-thumb" src={picture} alt="" loading="lazy" decoding="async" />
466
+ {:else if fileIcon}
467
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">{@html (fileIcon.bare ? '' : PAGE) + fileIcon.glyph}</svg>
468
+ {:else if node.kind === 'folder'}
469
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 6.5A1.5 1.5 0 0 1 4.5 5h4.38a1.5 1.5 0 0 1 1.06.44L11.5 7h8a1.5 1.5 0 0 1 1.5 1.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z" /></svg>
470
+ {:else}
471
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M6 3h8l4 4v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z" /><path d="M14 3v4h4" /></svg>
472
+ {/if}
473
+ </span>
474
+ {/snippet}
475
+
476
+ {#snippet branch(nodes: FileNode[], chain: FileNode[])}
477
+ <ul role="group">
478
+ {#each nodes.filter((n) => n.kind === 'folder') as folder (folder.id)}
479
+ {@const open_ = expanded.has(folder.id)}
480
+ {@const known = cache.get(folder.id)}
481
+ {@const leaf = known !== undefined && !known.some((n) => n.kind === 'folder')}
482
+ <li role="treeitem" aria-expanded={leaf ? undefined : open_} aria-selected={current === folder}>
483
+ <button
484
+ type="button"
485
+ class="file-explorer-tree-item"
486
+ aria-current={current === folder}
487
+ onclick={(e) => {
488
+ // The chevron expands; the rest of the row shows the folder.
489
+ if ((e.target as Element).closest('.file-explorer-twisty')) toggleBranch(folder);
490
+ else navigate([...chain, folder]);
491
+ }}
492
+ {...target(folder)}
493
+ >
494
+ <span class="file-explorer-twisty" data-leaf={leaf ? '' : undefined}></span>
495
+ {@render nodeIcon(folder)}
496
+ <span>{folder.name}</span>
497
+ </button>
498
+ {#if open_ && known}
499
+ {@render branch(known, [...chain, folder])}
500
+ {/if}
501
+ </li>
502
+ {/each}
503
+ </ul>
504
+ {/snippet}
505
+
506
+ <div {...applied.rest} class={classes}>
507
+ <div class="file-explorer-toolbar">
508
+ <Breadcrumbs class="file-explorer-path" aria-label={t.path}>
509
+ <li>
510
+ <button type="button" onclick={() => navigate([])} {...target(top)}>{top?.name ?? rootLabel}</button>
511
+ </li>
512
+ {#each path as f, i (f.id)}
513
+ <li><button type="button" onclick={() => navigate(path.slice(0, i + 1))} {...target(f)}>{f.name}</button></li>
514
+ {/each}
515
+ </Breadcrumbs>
516
+ {#if toolbar}<div class="file-explorer-actions">{@render toolbar({ folder: current, selection })}</div>{/if}
517
+ <div class="file-explorer-views" role="group">
518
+ <button type="button" class="file-explorer-view" aria-pressed={view === 'list'} aria-label={t.list} title={t.list} onclick={() => (view = 'list')}>
519
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01" /></svg>
520
+ </button>
521
+ <button type="button" class="file-explorer-view" aria-pressed={view === 'grid'} aria-label={t.grid} title={t.grid} onclick={() => (view = 'grid')}>
522
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" /></svg>
523
+ </button>
524
+ </div>
525
+ </div>
526
+
527
+ <div class="file-explorer-main">
528
+ {#if tree}
529
+ <nav class="file-explorer-tree" aria-label={t.tree}>
530
+ <ul role="tree" aria-label={t.tree}>
531
+ <li role="treeitem" aria-expanded="true" aria-selected={current === top}>
532
+ <button type="button" class="file-explorer-tree-item" aria-current={current === top} onclick={() => navigate([])} {...target(top)}>
533
+ <span class="file-explorer-twisty" data-leaf=""></span>
534
+ {@render nodeIcon({ id: TOP, name: top?.name ?? rootLabel, kind: 'folder' })}
535
+ <span>{top?.name ?? rootLabel}</span>
536
+ </button>
537
+ {@render branch(cache.get(keyOf(top)) ?? [], [])}
538
+ </li>
539
+ </ul>
540
+ </nav>
541
+ {/if}
542
+
543
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
544
+ <div
545
+ bind:this={content}
546
+ class="file-explorer-content"
547
+ role="listbox"
548
+ aria-multiselectable="true"
549
+ aria-label={current?.name ?? rootLabel}
550
+ tabindex="0"
551
+ aria-activedescendant={focusAt >= 0 ? `${PREFIX}-${String(items[focusAt]?.id)}` : undefined}
552
+ onkeydown={keydown}
553
+ onclick={(e) => {
554
+ if (e.target === e.currentTarget || (e.target as Element).classList.contains('file-explorer-grid')) clearSelection();
555
+ }}
556
+ {...target(current)}
557
+ >
558
+ {#if loading.has(keyOf(current)) && !cache.has(keyOf(current))}
559
+ <div class="file-explorer-empty">{t.loading}</div>
560
+ {:else if !items.length}
561
+ <div class="file-explorer-empty">
562
+ {#if empty}{@render empty(current)}{:else}{t.empty}{/if}
563
+ </div>
564
+ {:else if view === 'list'}
565
+ <table class="file-explorer-table" role="presentation">
566
+ <thead>
567
+ <tr>
568
+ {#each [['name', t.name], ['modified', t.modified], ['size', t.size]] as [k, text] (k)}
569
+ <th aria-sort={sort.key === k ? (sort.dir === 1 ? 'ascending' : 'descending') : undefined}>
570
+ <button type="button" onclick={() => sortBy(k as SortKey)}>
571
+ {text}{sort.key === k ? (sort.dir === 1 ? ' ↑' : ' ↓') : ''}
572
+ </button>
573
+ </th>
574
+ {/each}
575
+ </tr>
576
+ </thead>
577
+ <tbody>
578
+ {#each items as node, i (node.id)}
579
+ <tr
580
+ id="{PREFIX}-{String(node.id)}"
581
+ class="file-explorer-item"
582
+ role="option"
583
+ aria-selected={selected.has(node.id)}
584
+ data-index={i}
585
+ data-focus={focusAt === i ? '' : undefined}
586
+ draggable="true"
587
+ onclick={(e) => selectAt(i, e)}
588
+ ondblclick={() => open(node)}
589
+ oncontextmenu={(e) => contextmenu(e, i)}
590
+ ondragstart={(e) => dragstart(e, i)}
591
+ ondragend={dragend}
592
+ {...node.kind === 'folder' ? target(node) : {}}
593
+ >
594
+ <td>
595
+ <span class="file-explorer-entry">
596
+ {@render nodeIcon(node)}
597
+ <span class="file-explorer-name">{node.name}</span>
598
+ </span>
599
+ </td>
600
+ <td class="file-explorer-meta">{node.modified ? formatDate(node.modified) : '—'}</td>
601
+ <td class="file-explorer-meta">{node.kind === 'file' && node.size !== undefined ? formatSize(node.size) : '—'}</td>
602
+ </tr>
603
+ {/each}
604
+ </tbody>
605
+ </table>
606
+ {:else}
607
+ <ul class="file-explorer-grid" role="presentation">
608
+ {#each items as node, i (node.id)}
609
+ <li
610
+ id="{PREFIX}-{String(node.id)}"
611
+ class="file-explorer-item"
612
+ role="option"
613
+ aria-selected={selected.has(node.id)}
614
+ data-index={i}
615
+ data-focus={focusAt === i ? '' : undefined}
616
+ draggable="true"
617
+ title={node.name}
618
+ onclick={(e) => selectAt(i, e)}
619
+ ondblclick={() => open(node)}
620
+ oncontextmenu={(e) => contextmenu(e, i)}
621
+ ondragstart={(e) => dragstart(e, i)}
622
+ ondragend={dragend}
623
+ {...node.kind === 'folder' ? target(node) : {}}
624
+ >
625
+ {@render nodeIcon(node)}
626
+ <span class="file-explorer-name">{node.name}</span>
627
+ </li>
628
+ {/each}
629
+ </ul>
630
+ {/if}
631
+ </div>
632
+
633
+ {#if showDetails}
634
+ <aside class="file-explorer-details">{@render details!(selection)}</aside>
635
+ {/if}
636
+ </div>
637
+
638
+ {#if menuAt && menu}
639
+ <div bind:this={menuBox} class="file-explorer-menu" role="menu" style={menuStyle}>
640
+ {@render menu({ items: menuItems, close: closeMenu })}
641
+ </div>
642
+ {/if}
643
+ </div>