@k2b/ui 0.33.0 → 0.34.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 (240) hide show
  1. package/dist/base.css +14 -1
  2. package/dist/browser/actions/ContextMenu.js +13 -0
  3. package/dist/browser/actions/ContextMenu.js.map +1 -1
  4. package/dist/browser/actions/Dropdown.js +15 -4
  5. package/dist/browser/actions/Dropdown.js.map +1 -1
  6. package/dist/browser/actions/SelectionToolbar.js +129 -0
  7. package/dist/browser/actions/SelectionToolbar.js.map +1 -0
  8. package/dist/browser/actions/index.js +1 -0
  9. package/dist/browser/actions/index.js.map +1 -1
  10. package/dist/browser/charts.js +6 -0
  11. package/dist/browser/charts.js.map +1 -0
  12. package/dist/browser/chat/ChatComposer.js +525 -407
  13. package/dist/browser/chat/ChatComposer.js.map +1 -1
  14. package/dist/browser/chat/chat-messages.js +4 -0
  15. package/dist/browser/chat/chat-messages.js.map +1 -1
  16. package/dist/browser/chat/composer-document.js +19 -0
  17. package/dist/browser/chat/composer-document.js.map +1 -1
  18. package/dist/browser/chat/index.js.map +1 -1
  19. package/dist/browser/content/Chart.js +8 -0
  20. package/dist/browser/content/Chart.js.map +1 -1
  21. package/dist/browser/content/FileGrid.js +231 -9
  22. package/dist/browser/content/FileGrid.js.map +1 -1
  23. package/dist/browser/content/FileTypeIcon.js +15 -0
  24. package/dist/browser/content/FileTypeIcon.js.map +1 -1
  25. package/dist/browser/content/FileView.js +11 -4
  26. package/dist/browser/content/FileView.js.map +1 -1
  27. package/dist/browser/content/MessageRow.js +73 -53
  28. package/dist/browser/content/MessageRow.js.map +1 -1
  29. package/dist/browser/content/ReactionBar.js +3 -1
  30. package/dist/browser/content/ReactionBar.js.map +1 -1
  31. package/dist/browser/content/calendar-day-scroll.js +4 -3
  32. package/dist/browser/content/calendar-day-scroll.js.map +1 -1
  33. package/dist/browser/content/chart-bar.js +476 -0
  34. package/dist/browser/content/chart-bar.js.map +1 -0
  35. package/dist/browser/content/file-list-messages.js +52 -0
  36. package/dist/browser/content/file-list-messages.js.map +1 -0
  37. package/dist/browser/content/index.js +1 -1
  38. package/dist/browser/content/index.js.map +1 -1
  39. package/dist/browser/feedback/Tooltip.js +31 -14
  40. package/dist/browser/feedback/Tooltip.js.map +1 -1
  41. package/dist/browser/inputs/ChoiceList.js +178 -0
  42. package/dist/browser/inputs/ChoiceList.js.map +1 -0
  43. package/dist/browser/inputs/ChoiceSlider.js +314 -0
  44. package/dist/browser/inputs/ChoiceSlider.js.map +1 -0
  45. package/dist/browser/inputs/ChoiceTiles.js +135 -0
  46. package/dist/browser/inputs/ChoiceTiles.js.map +1 -0
  47. package/dist/browser/inputs/ColorChoices.js +120 -0
  48. package/dist/browser/inputs/ColorChoices.js.map +1 -0
  49. package/dist/browser/inputs/QrScanner.js +95 -13
  50. package/dist/browser/inputs/QrScanner.js.map +1 -1
  51. package/dist/browser/inputs/TextInput.js +15 -13
  52. package/dist/browser/inputs/TextInput.js.map +1 -1
  53. package/dist/browser/inputs/index.js +4 -0
  54. package/dist/browser/inputs/index.js.map +1 -1
  55. package/dist/browser/inputs/qr-messages.js +2 -0
  56. package/dist/browser/inputs/qr-messages.js.map +1 -1
  57. package/dist/browser/internal/radio-keys.js +13 -0
  58. package/dist/browser/internal/radio-keys.js.map +1 -0
  59. package/dist/browser/layout/AnchoredPopover.js +180 -0
  60. package/dist/browser/layout/AnchoredPopover.js.map +1 -0
  61. package/dist/browser/layout/AppWorkspace.js +155 -20
  62. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  63. package/dist/browser/layout/DetailPanel.js +443 -333
  64. package/dist/browser/layout/DetailPanel.js.map +1 -1
  65. package/dist/browser/layout/MobileShell.js +21 -7
  66. package/dist/browser/layout/MobileShell.js.map +1 -1
  67. package/dist/browser/layout/Settings.js +283 -175
  68. package/dist/browser/layout/Settings.js.map +1 -1
  69. package/dist/browser/layout/SidebarRailHint.js +79 -0
  70. package/dist/browser/layout/SidebarRailHint.js.map +1 -0
  71. package/dist/browser/layout/SidebarSwitcher.js +79 -66
  72. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  73. package/dist/browser/layout/TabBar.js +18 -4
  74. package/dist/browser/layout/TabBar.js.map +1 -1
  75. package/dist/browser/layout/app-workspace-controller.js +11 -4
  76. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  77. package/dist/browser/layout/detail-panel-messages.js +4 -2
  78. package/dist/browser/layout/detail-panel-messages.js.map +1 -1
  79. package/dist/browser/layout/index.js +2 -1
  80. package/dist/browser/layout/index.js.map +1 -1
  81. package/dist/browser/layout/settings-messages.js +26 -2
  82. package/dist/browser/layout/settings-messages.js.map +1 -1
  83. package/dist/browser/surfaces/Avatar.js +2 -14
  84. package/dist/browser/surfaces/Avatar.js.map +1 -1
  85. package/dist/browser/surfaces/DotStrip.js +70 -0
  86. package/dist/browser/surfaces/DotStrip.js.map +1 -0
  87. package/dist/browser/surfaces/ProgressRing.js +3 -0
  88. package/dist/browser/surfaces/ProgressRing.js.map +1 -1
  89. package/dist/browser/surfaces/ResourceCard.js +125 -68
  90. package/dist/browser/surfaces/ResourceCard.js.map +1 -1
  91. package/dist/browser/surfaces/index.js +2 -0
  92. package/dist/browser/surfaces/index.js.map +1 -1
  93. package/dist/browser/surfaces/person-color.js +18 -0
  94. package/dist/browser/surfaces/person-color.js.map +1 -0
  95. package/dist/browser/surfaces/resource-messages.js +6 -2
  96. package/dist/browser/surfaces/resource-messages.js.map +1 -1
  97. package/dist/browser/widgets/Widget.js +3 -2
  98. package/dist/browser/widgets/Widget.js.map +1 -1
  99. package/dist/ssr/actions/ContextMenu.js +9 -0
  100. package/dist/ssr/actions/ContextMenu.js.map +1 -1
  101. package/dist/ssr/actions/Dropdown.js +14 -4
  102. package/dist/ssr/actions/Dropdown.js.map +1 -1
  103. package/dist/ssr/actions/SelectionToolbar.js +104 -0
  104. package/dist/ssr/actions/SelectionToolbar.js.map +1 -0
  105. package/dist/ssr/actions/index.js +1 -0
  106. package/dist/ssr/actions/index.js.map +1 -1
  107. package/dist/ssr/charts.js +6 -0
  108. package/dist/ssr/charts.js.map +1 -0
  109. package/dist/ssr/chat/ChatComposer.js +132 -53
  110. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  111. package/dist/ssr/chat/chat-messages.js +4 -0
  112. package/dist/ssr/chat/chat-messages.js.map +1 -1
  113. package/dist/ssr/chat/composer-document.js +19 -0
  114. package/dist/ssr/chat/composer-document.js.map +1 -1
  115. package/dist/ssr/chat/index.js.map +1 -1
  116. package/dist/ssr/content/Chart.js +8 -0
  117. package/dist/ssr/content/Chart.js.map +1 -1
  118. package/dist/ssr/content/FileGrid.js +132 -5
  119. package/dist/ssr/content/FileGrid.js.map +1 -1
  120. package/dist/ssr/content/FileTypeIcon.js +15 -0
  121. package/dist/ssr/content/FileTypeIcon.js.map +1 -1
  122. package/dist/ssr/content/FileView.js +11 -4
  123. package/dist/ssr/content/FileView.js.map +1 -1
  124. package/dist/ssr/content/MessageRow.js +16 -6
  125. package/dist/ssr/content/MessageRow.js.map +1 -1
  126. package/dist/ssr/content/ReactionBar.js +3 -1
  127. package/dist/ssr/content/ReactionBar.js.map +1 -1
  128. package/dist/ssr/content/calendar-day-scroll.js +4 -3
  129. package/dist/ssr/content/calendar-day-scroll.js.map +1 -1
  130. package/dist/ssr/content/chart-bar.js +476 -0
  131. package/dist/ssr/content/chart-bar.js.map +1 -0
  132. package/dist/ssr/content/file-list-messages.js +52 -0
  133. package/dist/ssr/content/file-list-messages.js.map +1 -0
  134. package/dist/ssr/content/index.js +1 -1
  135. package/dist/ssr/content/index.js.map +1 -1
  136. package/dist/ssr/feedback/Tooltip.js +31 -14
  137. package/dist/ssr/feedback/Tooltip.js.map +1 -1
  138. package/dist/ssr/inputs/ChoiceList.js +95 -0
  139. package/dist/ssr/inputs/ChoiceList.js.map +1 -0
  140. package/dist/ssr/inputs/ChoiceSlider.js +191 -0
  141. package/dist/ssr/inputs/ChoiceSlider.js.map +1 -0
  142. package/dist/ssr/inputs/ChoiceTiles.js +61 -0
  143. package/dist/ssr/inputs/ChoiceTiles.js.map +1 -0
  144. package/dist/ssr/inputs/ColorChoices.js +60 -0
  145. package/dist/ssr/inputs/ColorChoices.js.map +1 -0
  146. package/dist/ssr/inputs/QrScanner.js +89 -10
  147. package/dist/ssr/inputs/QrScanner.js.map +1 -1
  148. package/dist/ssr/inputs/TextInput.js +13 -1
  149. package/dist/ssr/inputs/TextInput.js.map +1 -1
  150. package/dist/ssr/inputs/index.js +4 -0
  151. package/dist/ssr/inputs/index.js.map +1 -1
  152. package/dist/ssr/inputs/qr-messages.js +2 -0
  153. package/dist/ssr/inputs/qr-messages.js.map +1 -1
  154. package/dist/ssr/internal/radio-keys.js +13 -0
  155. package/dist/ssr/internal/radio-keys.js.map +1 -0
  156. package/dist/ssr/layout/AnchoredPopover.js +142 -0
  157. package/dist/ssr/layout/AnchoredPopover.js.map +1 -0
  158. package/dist/ssr/layout/AppWorkspace.js +154 -20
  159. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  160. package/dist/ssr/layout/DetailPanel.js +181 -108
  161. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  162. package/dist/ssr/layout/MobileShell.js +20 -6
  163. package/dist/ssr/layout/MobileShell.js.map +1 -1
  164. package/dist/ssr/layout/Settings.js +122 -45
  165. package/dist/ssr/layout/Settings.js.map +1 -1
  166. package/dist/ssr/layout/SidebarRailHint.js +79 -0
  167. package/dist/ssr/layout/SidebarRailHint.js.map +1 -0
  168. package/dist/ssr/layout/SidebarSwitcher.js +13 -3
  169. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  170. package/dist/ssr/layout/TabBar.js +11 -3
  171. package/dist/ssr/layout/TabBar.js.map +1 -1
  172. package/dist/ssr/layout/app-workspace-controller.js +11 -4
  173. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  174. package/dist/ssr/layout/detail-panel-messages.js +4 -2
  175. package/dist/ssr/layout/detail-panel-messages.js.map +1 -1
  176. package/dist/ssr/layout/index.js +2 -1
  177. package/dist/ssr/layout/index.js.map +1 -1
  178. package/dist/ssr/layout/settings-messages.js +26 -2
  179. package/dist/ssr/layout/settings-messages.js.map +1 -1
  180. package/dist/ssr/surfaces/Avatar.js +2 -14
  181. package/dist/ssr/surfaces/Avatar.js.map +1 -1
  182. package/dist/ssr/surfaces/DotStrip.js +31 -0
  183. package/dist/ssr/surfaces/DotStrip.js.map +1 -0
  184. package/dist/ssr/surfaces/ProgressRing.js +3 -0
  185. package/dist/ssr/surfaces/ProgressRing.js.map +1 -1
  186. package/dist/ssr/surfaces/ResourceCard.js +40 -15
  187. package/dist/ssr/surfaces/ResourceCard.js.map +1 -1
  188. package/dist/ssr/surfaces/index.js +2 -0
  189. package/dist/ssr/surfaces/index.js.map +1 -1
  190. package/dist/ssr/surfaces/person-color.js +18 -0
  191. package/dist/ssr/surfaces/person-color.js.map +1 -0
  192. package/dist/ssr/surfaces/resource-messages.js +6 -2
  193. package/dist/ssr/surfaces/resource-messages.js.map +1 -1
  194. package/dist/ssr/widgets/Widget.js +3 -2
  195. package/dist/ssr/widgets/Widget.js.map +1 -1
  196. package/dist/styles.css +1 -1
  197. package/dist/types/actions/ContextMenu.d.ts +5 -0
  198. package/dist/types/actions/Dropdown.d.ts +5 -0
  199. package/dist/types/actions/SelectionToolbar.d.ts +38 -0
  200. package/dist/types/actions/index.d.ts +2 -0
  201. package/dist/types/charts.d.ts +5 -0
  202. package/dist/types/chat/ChatComposer.d.ts +15 -0
  203. package/dist/types/chat/chat-messages.d.ts +2 -0
  204. package/dist/types/chat/composer-document.d.ts +5 -0
  205. package/dist/types/chat/index.d.ts +1 -1
  206. package/dist/types/content/Chart.d.ts +2 -1
  207. package/dist/types/content/FileGrid.d.ts +43 -3
  208. package/dist/types/content/FileTypeIcon.d.ts +10 -0
  209. package/dist/types/content/FileView.d.ts +5 -1
  210. package/dist/types/content/MessageRow.d.ts +2 -0
  211. package/dist/types/content/calendar-day-scroll.d.ts +2 -1
  212. package/dist/types/content/chart-bar.d.ts +60 -0
  213. package/dist/types/content/file-list-messages.d.ts +24 -0
  214. package/dist/types/content/index.d.ts +2 -2
  215. package/dist/types/feedback/Tooltip.d.ts +8 -2
  216. package/dist/types/inputs/ChoiceList.d.ts +28 -0
  217. package/dist/types/inputs/ChoiceSlider.d.ts +36 -0
  218. package/dist/types/inputs/ChoiceTiles.d.ts +25 -0
  219. package/dist/types/inputs/ColorChoices.d.ts +19 -0
  220. package/dist/types/inputs/index.d.ts +8 -0
  221. package/dist/types/inputs/qr-messages.d.ts +1 -0
  222. package/dist/types/internal/radio-keys.d.ts +5 -0
  223. package/dist/types/layout/AnchoredPopover.d.ts +31 -0
  224. package/dist/types/layout/AppWorkspace.d.ts +10 -0
  225. package/dist/types/layout/DetailPanel.d.ts +17 -0
  226. package/dist/types/layout/MobileShell.d.ts +5 -2
  227. package/dist/types/layout/Settings.d.ts +22 -0
  228. package/dist/types/layout/SidebarRailHint.d.ts +26 -0
  229. package/dist/types/layout/TabBar.d.ts +5 -0
  230. package/dist/types/layout/detail-panel-messages.d.ts +1 -0
  231. package/dist/types/layout/index.d.ts +3 -2
  232. package/dist/types/layout/settings-messages.d.ts +12 -0
  233. package/dist/types/surfaces/DotStrip.d.ts +22 -0
  234. package/dist/types/surfaces/ProgressRing.d.ts +2 -1
  235. package/dist/types/surfaces/ResourceCard.d.ts +9 -0
  236. package/dist/types/surfaces/index.d.ts +4 -1
  237. package/dist/types/surfaces/person-color.d.ts +10 -0
  238. package/dist/types/surfaces/resource-messages.d.ts +2 -0
  239. package/dist/types/widgets/Widget.d.ts +3 -2
  240. package/package.json +6 -1
@@ -1,21 +1,56 @@
1
+ import { memo as _$memo } from "solid-js/web";
1
2
  import { ssrElement as _$ssrElement } from "solid-js/web";
2
- import { mergeProps as _$mergeProps } from "solid-js/web";
3
3
  import { ssr as _$ssr } from "solid-js/web";
4
- import { createComponent as _$createComponent } from "solid-js/web";
5
4
  import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
6
5
  import { escape as _$escape } from "solid-js/web";
6
+ import { mergeProps as _$mergeProps } from "solid-js/web";
7
+ import { createComponent as _$createComponent } from "solid-js/web";
7
8
  var _tmpl$ = ["<div class=\"", "\"", " role=\"grid\"", ">", "</div>"],
8
9
  _tmpl$2 = ["<div class=\"k2b-file-grid__preview\">", "</div>"],
9
10
  _tmpl$3 = ["<div class=\"k2b-file-grid__name\">", "</div>"],
10
11
  _tmpl$4 = ["<div class=\"k2b-file-grid__meta\">", "</div>"],
11
12
  _tmpl$5 = ["<div class=\"k2b-file-grid__actions\">", "</div>"],
12
- _tmpl$6 = ["<div role=\"row\" class=\"k2b-file-grid__row\">", "</div>"];
13
- import { For } from "solid-js";
13
+ _tmpl$6 = ["<div role=\"row\" class=\"k2b-file-grid__row\">", "</div>"],
14
+ _tmpl$7 = ["<div class=\"", "\"", " data-open-items=\"true\" role=\"list\"", ">", "</div>"],
15
+ _tmpl$8 = ["<button type=\"button\" class=\"k2b-file-grid__open\" aria-labelledby=\"", "\"", "></button>"],
16
+ _tmpl$9 = ["<div class=\"k2b-file-grid__name\" id=\"", "\">", "</div>"],
17
+ _tmpl$0 = ["<div class=\"k2b-file-grid__meta\" id=\"", "\">", "</div>"],
18
+ _tmpl$1 = ["<div role=\"listitem\" class=\"k2b-file-grid__row\">", "</div>"],
19
+ _tmpl$10 = ["<div class=\"k2b-file-grid-stack__toolbar\">", "</div>"],
20
+ _tmpl$11 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\"><span>", "</span><i class=\"ti ti-chevron-up\" aria-hidden=\"true\"></i></button>"],
21
+ _tmpl$12 = ["<div class=\"k2b-file-grid-stack\"", ">", "", "", "</div>"],
22
+ _tmpl$13 = ["<div class=\"k2b-file-grid-stack__group\"><div class=\"k2b-file-grid-stack__label\"", ">", "<span class=\"k2b-file-grid-stack__count\">", "</span></div>", "</div>"],
23
+ _tmpl$14 = ["<span class=\"k2b-file-grid-stack__more-meta\">", "</span>"],
24
+ _tmpl$15 = ["<button type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\"><span>", "</span><i class=\"ti ti-chevron-down\" aria-hidden=\"true\"></i>", "</button>"];
25
+ import { createEffect, createMemo, createSignal, createUniqueId, For, Index, on, Show } from "solid-js";
26
+ import { useFileListMessages } from "./file-list-messages";
27
+
28
+ /** One part of an open file list, such as its files as rows and its images as tiles. */
29
+
30
+ /** A browser or picker: items are selected, and Enter or a double click opens them. */
31
+
32
+ /** A list without selection, as in a details panel: one click on an item opens it. */
33
+
14
34
  /**
15
35
  * File presentation only; the host owns data, menus, leases and permissions. Items the selection marks with
16
36
  * `isDisabled` stay reachable by keyboard but cannot be selected or opened; say why in `renderMeta`.
17
37
  */
18
38
  export function FileGrid(props) {
39
+ return _$createComponent(Show, {
40
+ get when() {
41
+ return props.selection;
42
+ },
43
+ get fallback() {
44
+ return _$createComponent(OpenFileGrid, props);
45
+ },
46
+ children: selection => _$createComponent(SelectableFileGrid, _$mergeProps(props, {
47
+ get selection() {
48
+ return selection();
49
+ }
50
+ }))
51
+ });
52
+ }
53
+ function SelectableFileGrid(props) {
19
54
  let grid;
20
55
  // Both clicks of a double-click belong to the item that took the first one. When that click replaced the rows,
21
56
  // for example by opening a folder, the item now under the pointer ignores the rest of the gesture.
@@ -26,7 +61,7 @@ export function FileGrid(props) {
26
61
  };
27
62
  const columns = () => grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(" ").length) : 1;
28
63
  const nested = event => event.target instanceof Element && !!event.target.closest("button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]");
29
- return _$ssr(_tmpl$, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", _$escape(props.layout ?? "grid", true), false), _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-multiselectable", _$escape(props.selection.multiple, true), false), _$escape(_$createComponent(For, {
64
+ return _$ssr(_tmpl$, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false), _$ssrAttribute("aria-label", _$escape(props.label, true), false) + _$ssrAttribute("aria-multiselectable", _$escape(props.selection.multiple, true), false), _$escape(_$createComponent(For, {
30
65
  get each() {
31
66
  return props.rows;
32
67
  },
@@ -49,4 +84,96 @@ export function FileGrid(props) {
49
84
  }
50
85
  })));
51
86
  }
87
+ function OpenFileGrid(props) {
88
+ const messages = useFileListMessages();
89
+ const [ownExpanded, setOwnExpanded] = createSignal(false);
90
+ const limited = () => props.limit !== undefined && props.rows.length > props.limit;
91
+ // Without a limit the list is always complete, so its toolbar and groups always apply.
92
+ const full = () => props.limit === undefined || limited() && (props.expanded ?? ownExpanded());
93
+ let more;
94
+ let less;
95
+ let moveFocus = false;
96
+ const toggle = next => {
97
+ moveFocus = true;
98
+ if (props.expanded === undefined) setOwnExpanded(next);
99
+ props.onExpandedChange?.(next);
100
+ };
101
+ // The pressed button leaves with the state it showed; once the new state is there, focus moves to its
102
+ // counterpart, also when a controlling host applies it later.
103
+ createEffect(on(full, open => {
104
+ if (!moveFocus) return;
105
+ moveFocus = false;
106
+ queueMicrotask(() => (open ? less : more)?.focus());
107
+ }, {
108
+ defer: true
109
+ }));
110
+ const groups = createMemo(() => full() && props.groups ? props.groups(props.rows).filter(group => group.rows.length > 0) : [{
111
+ id: "rows",
112
+ label: undefined,
113
+ rows: full() || !limited() ? props.rows : props.rows.slice(0, props.limit)
114
+ }]);
115
+ const list = (group, labelId) => _$ssr(_tmpl$7, `k2b-file-grid ${_$escape(props.class ?? "", true)}`, _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false) + _$ssrAttribute("data-layout", group().layout === "tiles" ? "tiles" : _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-action-visibility", _$escape(props.actionVisibility ?? "always", true), false), _$ssrAttribute("aria-label", labelId ? _$escape(undefined, true) : _$escape(props.label, true), false) + _$ssrAttribute("aria-labelledby", _$escape(labelId, true), false), _$escape(_$createComponent(For, {
116
+ get each() {
117
+ return group().rows;
118
+ },
119
+ children: row => {
120
+ const id = `k2b-file-grid-${createUniqueId()}`;
121
+ return _$ssr(_tmpl$1, _$ssrElement("div", _$mergeProps(() => props.itemProps?.(row), {
122
+ "class": "k2b-file-grid__item"
123
+ }), [_$ssr(_tmpl$8, `${_$escape(id, true)}-name`, _$ssrAttribute("aria-describedby", props.renderMeta ? `${_$escape(id, true)}-meta` : _$escape(undefined, true), false)), _$ssr(_tmpl$2, _$escape(props.renderPreview(row))), _$ssr(_tmpl$9, `${_$escape(id, true)}-name`, _$escape(props.renderLabel(row))), _$ssr(_tmpl$0, `${_$escape(id, true)}-meta`, _$escape(props.renderMeta?.(row))), _$ssr(_tmpl$5, _$escape(props.renderActions?.(row)))], false));
124
+ }
125
+ })));
126
+ return _$ssr(_tmpl$12, _$ssrAttribute("data-expanded", full() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
127
+ get when() {
128
+ return _$memo(() => !!full())() && props.toolbar;
129
+ },
130
+ get children() {
131
+ return _$ssr(_tmpl$10, _$escape(props.toolbar));
132
+ }
133
+ })), _$escape(_$createComponent(Index, {
134
+ get each() {
135
+ return groups();
136
+ },
137
+ children: group => {
138
+ const labelId = `k2b-file-grid-group-${createUniqueId()}`;
139
+ return _$createComponent(Show, {
140
+ get when() {
141
+ return group().label !== undefined;
142
+ },
143
+ get fallback() {
144
+ return list(group);
145
+ },
146
+ get children() {
147
+ return _$ssr(_tmpl$13, _$ssrAttribute("id", _$escape(labelId, true), false), _$escape(group().label), _$escape(group().rows.length), _$escape(list(group, labelId)));
148
+ }
149
+ });
150
+ }
151
+ })), _$escape(_$createComponent(Show, {
152
+ get when() {
153
+ return limited();
154
+ },
155
+ get children() {
156
+ return _$createComponent(Show, {
157
+ get when() {
158
+ return full();
159
+ },
160
+ get fallback() {
161
+ return _$ssr(_tmpl$15, _$escape(messages().showAll({
162
+ count: props.rows.length
163
+ })), _$escape(_$createComponent(Show, {
164
+ get when() {
165
+ return props.moreMeta;
166
+ },
167
+ get children() {
168
+ return _$ssr(_tmpl$14, _$escape(props.moreMeta));
169
+ }
170
+ })));
171
+ },
172
+ get children() {
173
+ return _$ssr(_tmpl$11, _$escape(messages().showLess));
174
+ }
175
+ });
176
+ }
177
+ })));
178
+ }
52
179
  //# sourceMappingURL=FileGrid.js.map
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["content/FileGrid.tsx"],"sourcesContent":["import { For, type JSX } from \"solid-js\";\nimport type { CollectionSelection } from \"./collection-selection\";\n\nexport type FileGridProps<T> = {\n rows: readonly T[];\n getRowId: (row: T) => string;\n selection: CollectionSelection;\n label: string;\n size?: \"sm\" | \"md\" | \"lg\";\n /** `list` shows one compact row per item: preview, name, and meta beside each other. Defaults to `grid`. */\n layout?: \"grid\" | \"list\";\n renderPreview: (row: T) => JSX.Element;\n renderLabel: (row: T) => JSX.Element;\n renderMeta?: (row: T) => JSX.Element;\n renderActions?: (row: T) => JSX.Element;\n onRowClick?: (row: T) => void;\n onOpen: (row: T) => void;\n onContextMenu?: (row: T) => void;\n /** Extra attributes for one tile, for example drag-and-drop handlers or data attributes the host styles. */\n itemProps?: (row: T) => JSX.HTMLAttributes<HTMLDivElement>;\n class?: string;\n};\n\n/**\n * File presentation only; the host owns data, menus, leases and permissions. Items the selection marks with\n * `isDisabled` stay reachable by keyboard but cannot be selected or opened; say why in `renderMeta`.\n */\nexport function FileGrid<T>(props: FileGridProps<T>) {\n let grid: HTMLDivElement | undefined;\n // Both clicks of a double-click belong to the item that took the first one. When that click replaced the rows,\n // for example by opening a folder, the item now under the pointer ignores the rest of the gesture.\n let pressed: EventTarget | null | undefined;\n const ownsClick = (event: MouseEvent) => {\n if (event.detail > 1 && pressed !== event.currentTarget) pressed = null;\n else pressed = event.currentTarget;\n return pressed !== null;\n };\n const columns = () => (grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(\" \").length) : 1);\n const nested = (event: Event) =>\n event.target instanceof Element &&\n !!event.target.closest(\"button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]\");\n return (\n <div\n ref={grid}\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={props.layout ?? \"grid\"}\n role=\"grid\"\n aria-label={props.label}\n aria-multiselectable={props.selection.multiple}\n >\n <For each={props.rows}>\n {(row) => {\n const id = () => props.getRowId(row);\n const disabled = () => props.selection.isDisabled(id());\n return (\n <div role=\"row\" class=\"k2b-file-grid__row\">\n <div\n {...props.itemProps?.(row)}\n role=\"gridcell\"\n class=\"k2b-file-grid__item\"\n aria-selected={props.selection.selected().has(id())}\n aria-disabled={disabled() || undefined}\n ref={(element) => props.selection.register(id(), element)}\n tabIndex={props.selection.focused() === id() ? 0 : -1}\n onFocus={() => props.selection.markFocused(id())}\n onClick={(event) => {\n if (!nested(event) && ownsClick(event) && !disabled()) {\n props.selection.select(id(), event);\n props.onRowClick?.(row);\n }\n }}\n onDblClick={(event) => {\n if (!nested(event) && pressed !== null && !disabled()) props.onOpen(row);\n }}\n onContextMenu={() => props.onContextMenu?.(row)}\n onKeyDown={(event) => {\n if (nested(event)) return;\n if (props.selection.keyDown(event, id(), columns())) return;\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (!disabled()) props.onOpen(row);\n }\n }}\n >\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\">{props.renderLabel(row)}</div>\n <div class=\"k2b-file-grid__meta\">{props.renderMeta?.(row)}</div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,GAAG,QAAkB,UAAU;AAuBxC;AACA;AACA;AACA;AACA,OAAO,SAAS,QAAQ,CAAI,KAAuB,EAAE;EACnD,IAAI,IAAgC;EACpC;EACA;EACA,IAAI,OAAuC;EAC3C,MAAM,SAAS,GAAI,KAAiB,IAAK;IACvC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,KAAK,CAAC,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC,KACnE,OAAO,GAAG,KAAK,CAAC,aAAa;IAClC,OAAO,OAAO,KAAK,IAAI;EACzB,CAAC;EACD,MAAM,OAAO,GAAG,MAAO,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAE;EAC5G,MAAM,MAAM,GAAI,KAAY,IAC1B,KAAK,CAAC,MAAM,YAAY,OAAO,IAC/B,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,0FAA0F,CAAC;EACpH,qBAGW,0BAAiB,KAAK,CAAC,KAAK,IAAI,EAAE,SAAE,uCAChC,KAAK,CAAC,IAAI,IAAI,IAAI,yDAChB,KAAK,CAAC,MAAM,IAAI,MAAM,uDAEvB,KAAK,CAAC,KAAK,kEACD,KAAK,CAAC,SAAS,CAAC,QAAQ,4CAE7C,GAAG;IAAA,IAAC,IAAI;MAAA,OAAE,KAAK,CAAC,IAAI;IAAA;IAAA,UACjB,GAAG,IAAK;MACR,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;MACpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;MACvD,6DAGU,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC;QAC1B,IAAI,EAAC,UAAU;QAAA,SACT,qBAAqB;QAAA;UAAA,OACZ,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAAA;QAAA;UAAA,OACpC,QAAQ,CAAC,CAAC,IAAI,SAAS;QAAA;QAAA,IAEtC,QAAQ;UAAA,OAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAAA;MAAA,6BAqBhB,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,4BAC3B,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,4BACtB,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,4BACpB,KAAK,CAAC,aAAa,GAAG,GAAG,CAAC;IAIvE;EAAC;AAIT","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["content/FileGrid.tsx"],"sourcesContent":["import { createEffect, createMemo, createSignal, createUniqueId, For, Index, type JSX, on, Show } from \"solid-js\";\nimport type { CollectionSelection } from \"./collection-selection\";\nimport { useFileListMessages } from \"./file-list-messages\";\n\n/** One part of an open file list, such as its files as rows and its images as tiles. */\nexport type FileGridGroup<T> = {\n id: string;\n label: JSX.Element;\n rows: readonly T[];\n /** `tiles` fills each item with its preview and lays the name over it. Defaults to `list`. */\n layout?: \"list\" | \"tiles\";\n};\n\ntype FileGridBaseProps<T> = {\n rows: readonly T[];\n getRowId: (row: T) => string;\n label: string;\n size?: \"sm\" | \"md\" | \"lg\";\n /** `list` shows one compact row per item: preview, name, and meta beside each other. Defaults to `grid`. */\n layout?: \"grid\" | \"list\";\n renderPreview: (row: T) => JSX.Element;\n renderLabel: (row: T) => JSX.Element;\n renderMeta?: (row: T) => JSX.Element;\n renderActions?: (row: T) => JSX.Element;\n /**\n * `progressive` shows the actions on hover, keyboard focus, or an open menu with a mouse or trackpad, and always\n * on touch. Their slot is reserved in both states, so nothing moves. Defaults to `always`.\n */\n actionVisibility?: \"always\" | \"progressive\";\n onOpen: (row: T) => void;\n onContextMenu?: (row: T) => void;\n /** Extra attributes for one tile, for example drag-and-drop handlers or data attributes the host styles. */\n itemProps?: (row: T) => JSX.HTMLAttributes<HTMLDivElement>;\n class?: string;\n};\n\n/** A browser or picker: items are selected, and Enter or a double click opens them. */\nexport type FileGridSelectableProps<T> = FileGridBaseProps<T> & {\n selection: CollectionSelection;\n onRowClick?: (row: T) => void;\n limit?: never;\n expanded?: never;\n onExpandedChange?: never;\n moreMeta?: never;\n toolbar?: never;\n groups?: never;\n};\n\n/** A list without selection, as in a details panel: one click on an item opens it. */\nexport type FileGridOpenProps<T> = FileGridBaseProps<T> & {\n selection?: never;\n onRowClick?: never;\n /** Shows the first `limit` items, then \"Show all N\", which opens the full list in place. Without it, the list is always full. */\n limit?: number;\n /** Controls whether the full list is open. Without it, the list keeps its own state. */\n expanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n /** A quiet note at the end of \"Show all N\", such as how many duplicates were merged. */\n moreMeta?: JSX.Element;\n /** Shown above the full list, such as a count, an arrangement switch, or \"Download all\"; not in the short list. */\n toolbar?: JSX.Element;\n /** How the full list arranges its items, such as files as rows, then images as tiles; the short list stays one list. */\n groups?: (rows: readonly T[]) => readonly FileGridGroup<T>[];\n};\n\nexport type FileGridProps<T> = FileGridSelectableProps<T> | FileGridOpenProps<T>;\n\n/**\n * File presentation only; the host owns data, menus, leases and permissions. Items the selection marks with\n * `isDisabled` stay reachable by keyboard but cannot be selected or opened; say why in `renderMeta`.\n */\nexport function FileGrid<T>(props: FileGridProps<T>) {\n return (\n <Show when={props.selection} fallback={<OpenFileGrid {...(props as FileGridOpenProps<T>)} />}>\n {(selection) => <SelectableFileGrid {...(props as FileGridSelectableProps<T>)} selection={selection()} />}\n </Show>\n );\n}\n\nfunction SelectableFileGrid<T>(props: FileGridSelectableProps<T>) {\n let grid: HTMLDivElement | undefined;\n // Both clicks of a double-click belong to the item that took the first one. When that click replaced the rows,\n // for example by opening a folder, the item now under the pointer ignores the rest of the gesture.\n let pressed: EventTarget | null | undefined;\n const ownsClick = (event: MouseEvent) => {\n if (event.detail > 1 && pressed !== event.currentTarget) pressed = null;\n else pressed = event.currentTarget;\n return pressed !== null;\n };\n const columns = () => (grid ? Math.max(1, getComputedStyle(grid).gridTemplateColumns.split(\" \").length) : 1);\n const nested = (event: Event) =>\n event.target instanceof Element &&\n !!event.target.closest(\"button,a,input,label,select,textarea,summary,[contenteditable],[role=button],[role=menu]\");\n return (\n <div\n ref={grid}\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={props.layout ?? \"grid\"}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n role=\"grid\"\n aria-label={props.label}\n aria-multiselectable={props.selection.multiple}\n >\n <For each={props.rows}>\n {(row) => {\n const id = () => props.getRowId(row);\n const disabled = () => props.selection.isDisabled(id());\n return (\n <div role=\"row\" class=\"k2b-file-grid__row\">\n <div\n {...props.itemProps?.(row)}\n role=\"gridcell\"\n class=\"k2b-file-grid__item\"\n aria-selected={props.selection.selected().has(id())}\n aria-disabled={disabled() || undefined}\n ref={(element) => props.selection.register(id(), element)}\n tabIndex={props.selection.focused() === id() ? 0 : -1}\n onFocus={() => props.selection.markFocused(id())}\n onClick={(event) => {\n if (!nested(event) && ownsClick(event) && !disabled()) {\n props.selection.select(id(), event);\n props.onRowClick?.(row);\n }\n }}\n onDblClick={(event) => {\n if (!nested(event) && pressed !== null && !disabled()) props.onOpen(row);\n }}\n onContextMenu={() => props.onContextMenu?.(row)}\n onKeyDown={(event) => {\n if (nested(event)) return;\n if (props.selection.keyDown(event, id(), columns())) return;\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (!disabled()) props.onOpen(row);\n }\n }}\n >\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\">{props.renderLabel(row)}</div>\n <div class=\"k2b-file-grid__meta\">{props.renderMeta?.(row)}</div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n}\n\nfunction OpenFileGrid<T>(props: FileGridOpenProps<T>) {\n const messages = useFileListMessages();\n const [ownExpanded, setOwnExpanded] = createSignal(false);\n const limited = () => props.limit !== undefined && props.rows.length > props.limit;\n // Without a limit the list is always complete, so its toolbar and groups always apply.\n const full = () => props.limit === undefined || (limited() && (props.expanded ?? ownExpanded()));\n let more: HTMLButtonElement | undefined;\n let less: HTMLButtonElement | undefined;\n let moveFocus = false;\n const toggle = (next: boolean) => {\n moveFocus = true;\n if (props.expanded === undefined) setOwnExpanded(next);\n props.onExpandedChange?.(next);\n };\n // The pressed button leaves with the state it showed; once the new state is there, focus moves to its\n // counterpart, also when a controlling host applies it later.\n createEffect(\n on(\n full,\n (open) => {\n if (!moveFocus) return;\n moveFocus = false;\n queueMicrotask(() => (open ? less : more)?.focus());\n },\n { defer: true },\n ),\n );\n const groups = createMemo((): readonly FileGridGroup<T>[] =>\n full() && props.groups\n ? props.groups(props.rows).filter((group) => group.rows.length > 0)\n : [{ id: \"rows\", label: undefined, rows: full() || !limited() ? props.rows : props.rows.slice(0, props.limit) }],\n );\n\n const list = (group: () => FileGridGroup<T>, labelId?: string) => (\n <div\n class={`k2b-file-grid ${props.class ?? \"\"}`}\n data-size={props.size ?? \"md\"}\n data-layout={group().layout === \"tiles\" ? \"tiles\" : (props.layout ?? \"grid\")}\n data-action-visibility={props.actionVisibility ?? \"always\"}\n data-open-items=\"true\"\n role=\"list\"\n aria-label={labelId ? undefined : props.label}\n aria-labelledby={labelId}\n >\n {/* Keyed by the row itself: new data keeps every existing item, its focus, and its state. */}\n <For each={group().rows}>\n {(row) => {\n const id = `k2b-file-grid-${createUniqueId()}`;\n return (\n <div role=\"listitem\" class=\"k2b-file-grid__row\">\n <div {...props.itemProps?.(row)} class=\"k2b-file-grid__item\">\n {/* The whole item opens the file; its actions lie above this layer in their own slot. */}\n <button\n type=\"button\"\n class=\"k2b-file-grid__open\"\n aria-labelledby={`${id}-name`}\n aria-describedby={props.renderMeta ? `${id}-meta` : undefined}\n onClick={() => props.onOpen(row)}\n onContextMenu={() => props.onContextMenu?.(row)}\n />\n <div class=\"k2b-file-grid__preview\">{props.renderPreview(row)}</div>\n <div class=\"k2b-file-grid__name\" id={`${id}-name`}>\n {props.renderLabel(row)}\n </div>\n <div class=\"k2b-file-grid__meta\" id={`${id}-meta`}>\n {props.renderMeta?.(row)}\n </div>\n <div class=\"k2b-file-grid__actions\">{props.renderActions?.(row)}</div>\n </div>\n </div>\n );\n }}\n </For>\n </div>\n );\n\n return (\n <div class=\"k2b-file-grid-stack\" data-expanded={full() ? \"true\" : undefined}>\n <Show when={full() && props.toolbar}>\n <div class=\"k2b-file-grid-stack__toolbar\">{props.toolbar}</div>\n </Show>\n {/* By position: a group that only changes its rows keeps its list, so new data remounts nothing. */}\n <Index each={groups()}>\n {(group) => {\n const labelId = `k2b-file-grid-group-${createUniqueId()}`;\n return (\n <Show when={group().label !== undefined} fallback={list(group)}>\n <div class=\"k2b-file-grid-stack__group\">\n <div class=\"k2b-file-grid-stack__label\" id={labelId}>\n {group().label}\n <span class=\"k2b-file-grid-stack__count\">{group().rows.length}</span>\n </div>\n {list(group, labelId)}\n </div>\n </Show>\n );\n }}\n </Index>\n <Show when={limited()}>\n <Show\n when={full()}\n fallback={\n <button ref={more} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"false\" onClick={() => toggle(true)}>\n <span>{messages().showAll({ count: props.rows.length })}</span>\n <i class=\"ti ti-chevron-down\" aria-hidden=\"true\" />\n <Show when={props.moreMeta}>\n <span class=\"k2b-file-grid-stack__more-meta\">{props.moreMeta}</span>\n </Show>\n </button>\n }\n >\n <button ref={less} type=\"button\" class=\"k2b-file-grid-stack__more\" aria-expanded=\"true\" onClick={() => toggle(false)}>\n <span>{messages().showLess}</span>\n <i class=\"ti ti-chevron-up\" aria-hidden=\"true\" />\n </button>\n </Show>\n </Show>\n </div>\n );\n}\n"],"mappings":"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","ignoreList":[]}
@@ -3,6 +3,7 @@ import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
3
3
  import { escape as _$escape } from "solid-js/web";
4
4
  var _tmpl$ = ["<span class=\"", "\"", " aria-hidden=\"true\"><i class=\"", "\"></i></span>"];
5
5
  import { fileIcons } from "@k2b/stdlib";
6
+ import { resolveFileListMessages } from "./file-list-messages";
6
7
 
7
8
  /** The colour family a file's icon tile uses. `other` covers code, text, and unknown files. */
8
9
 
@@ -23,6 +24,20 @@ export const fileTypeOf = file => {
23
24
  return category === "pdf" || category === "image" || category === "video" || category === "audio" || category === "archive" ? category : "other";
24
25
  };
25
26
 
27
+ /**
28
+ * The readable kind of a file in `locale`, such as "PDF", "Image", or "Spreadsheet", for a meta line like
29
+ * "PDF · 4.2 MB" instead of a media type. An unknown kind names the file's extension ("ICC"), and a file without
30
+ * one reads "File".
31
+ */
32
+ export const fileKindLabel = (file, locale) => {
33
+ const t = resolveFileListMessages(locale);
34
+ const type = fileTypeOf(file);
35
+ if (type !== "other") return t[type];
36
+ const dot = file.name.lastIndexOf(".");
37
+ const extension = dot > 0 ? file.name.slice(dot + 1) : "";
38
+ return /^[a-z0-9]+$/i.test(extension) ? extension.toUpperCase() : t.file;
39
+ };
40
+
26
41
  /** `fileIcons` adds Tailwind colour utilities that @k2b/ui does not ship; only the `ti-*` glyph is kept. */
27
42
  const glyph = props => {
28
43
  // `fileIcons` looks names up in plain objects, so a name such as `constructor` returns an inherited member
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["content/FileTypeIcon.tsx"],"sourcesContent":["import { fileIcons } from \"@k2b/stdlib\";\nimport type { JSX } from \"solid-js\";\n\n/** The colour family a file's icon tile uses. `other` covers code, text, and unknown files. */\nexport type FileType = \"folder\" | \"pdf\" | \"image\" | \"sheet\" | \"document\" | \"slides\" | \"video\" | \"audio\" | \"archive\" | \"other\";\n\nexport type FileTypeIconProps = {\n /** The file or folder name; its extension picks the glyph and the type colour. */\n name: string;\n mediaType?: string;\n folder?: boolean;\n /** Shows an open folder, for example the folder a list is showing. */\n open?: boolean;\n /** A Tabler icon class that replaces the glyph the name picks; the type colour still follows the name. */\n icon?: string;\n /** `sm` for tree rows, `md` for list rows, `lg` for grid previews. Defaults to `md`. */\n size?: \"sm\" | \"md\" | \"lg\";\n class?: string;\n};\n\n// Known extensions decide first: a generic media type such as `text/plain` or `text/csv` must not hide a sheet,\n// a slide deck, a document, or an archive behind the neutral family.\nconst BY_EXTENSION = new Map<string, FileType>(\n (\n [\n [\"sheet\", [\"xls\", \"xlsx\", \"ods\", \"csv\"]],\n [\"slides\", [\"ppt\", \"pptx\", \"odp\"]],\n [\"document\", [\"doc\", \"docx\", \"odt\", \"rtf\"]],\n [\"archive\", [\"zip\", \"tar\", \"gz\", \"tgz\", \"bz2\", \"xz\", \"7z\", \"rar\"]],\n ] as const\n ).flatMap(([type, extensions]) => extensions.map((extension) => [extension, type] as const)),\n);\n\n/** Resolve the colour family for a file or folder from its name and media type. */\nexport const fileTypeOf = (file: { name: string; mediaType?: string; folder?: boolean }): FileType => {\n if (file.folder) return \"folder\";\n const known = BY_EXTENSION.get(file.name.split(\".\").pop()?.toLowerCase() ?? \"\");\n if (known) return known;\n const category = fileIcons.getFileCategory({ name: file.name, type: \"file\", mimeType: file.mediaType });\n return category === \"pdf\" || category === \"image\" || category === \"video\" || category === \"audio\" || category === \"archive\"\n ? category\n : \"other\";\n};\n\n/** `fileIcons` adds Tailwind colour utilities that @k2b/ui does not ship; only the `ti-*` glyph is kept. */\nconst glyph = (props: FileTypeIconProps): string => {\n // `fileIcons` looks names up in plain objects, so a name such as `constructor` returns an inherited member\n // instead of a class string; such names fall back to the generic glyph.\n const found: unknown =\n props.icon ?? fileIcons.getFileIcon({ name: props.name, type: props.folder ? \"directory\" : \"file\", mimeType: props.mediaType });\n const icon =\n (typeof found === \"string\" ? found : \"\")\n .split(/\\s+/)\n .filter((token) => token.startsWith(\"ti-\"))\n .join(\" \") || (props.folder ? \"ti-folder\" : \"ti-file\");\n return props.folder && props.open ? icon.replace(/\\bti-folder$/, \"ti-folder-open\") : icon;\n};\n\n/**\n * A file or folder glyph in a soft chip of its type colour. Folders take the app accent hook\n * `--k2b-app-workspace-active`; code, text, and unknown files a neutral ink. The box size is fixed per `size`.\n */\nexport function FileTypeIcon(props: FileTypeIconProps): JSX.Element {\n return (\n <span\n class={`k2b-icon-tile k2b-file-type-icon ${props.class ?? \"\"}`}\n data-file-type={fileTypeOf({ name: props.name, mediaType: props.mediaType, folder: props.folder })}\n data-size={props.size ?? \"md\"}\n aria-hidden=\"true\"\n >\n <i class={`ti ${glyph(props)}`} />\n </span>\n );\n}\n"],"mappings":";;;;AAAA,SAAS,SAAS,QAAQ,aAAa;;AAGvC;;AAiBA;AACA;AACA,MAAM,YAAY,GAAG,IAAI,GAAG,CAExB,CACE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EACxC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,EAClC,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EAC3C,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CACnE,CACD,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,UAAU,CAAC,GAAG,CAAE,SAAS,IAAK,CAAC,SAAS,EAAE,IAAI,CAAU,CAAC,CAC7F,CAAC;;AAED;AACA,OAAO,MAAM,UAAU,GAAI,IAA4D,IAAe;EACpG,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,QAAQ;EAChC,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC;EAC/E,IAAI,KAAK,EAAE,OAAO,KAAK;EACvB,MAAM,QAAQ,GAAG,SAAS,CAAC,eAAe,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC,IAAI;IAAE,IAAI,EAAE,MAAM;IAAE,QAAQ,EAAE,IAAI,CAAC;EAAU,CAAC,CAAC;EACvG,OAAO,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,SAAS,GACvH,QAAQ,GACR,OAAO;AACb,CAAC;;AAED;AACA,MAAM,KAAK,GAAI,KAAwB,IAAa;EAClD;EACA;EACA,MAAM,KAAc,GAClB,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC,WAAW,CAAC;IAAE,IAAI,EAAE,KAAK,CAAC,IAAI;IAAE,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,WAAW,GAAG,MAAM;IAAE,QAAQ,EAAE,KAAK,CAAC;EAAU,CAAC,CAAC;EACjI,MAAM,IAAI,GACR,CAAC,OAAO,KAAK,KAAK,QAAQ,GAAG,KAAK,GAAG,EAAE,EACpC,KAAK,CAAC,KAAK,CAAC,CACZ,MAAM,CAAE,KAAK,IAAK,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAC1C,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;EAC1D,OAAO,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,gBAAgB,CAAC,GAAG,IAAI;AAC3F,CAAC;;AAED;AACA;AACA;AACA;AACA,OAAO,SAAS,YAAY,CAAC,KAAwB,EAAe;EAClE,qBAEW,6CAAoC,KAAK,CAAC,KAAK,IAAI,EAAE,SAAE,4CAC9C,UAAU,CAAC;IAAE,IAAI,EAAE,KAAK,CAAC,IAAI;IAAE,SAAS,EAAE,KAAK,CAAC,SAAS;IAAE,MAAM,EAAE,KAAK,CAAC;EAAO,CAAC,CAAC,uDACvF,KAAK,CAAC,IAAI,IAAI,IAAI,iBAGnB,eAAM,KAAK,CAAC,KAAK,CAAC,SAAE;AAGpC","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["content/FileTypeIcon.tsx"],"sourcesContent":["import { fileIcons } from \"@k2b/stdlib\";\nimport type { JSX } from \"solid-js\";\nimport { resolveFileListMessages } from \"./file-list-messages\";\n\n/** The colour family a file's icon tile uses. `other` covers code, text, and unknown files. */\nexport type FileType = \"folder\" | \"pdf\" | \"image\" | \"sheet\" | \"document\" | \"slides\" | \"video\" | \"audio\" | \"archive\" | \"other\";\n\nexport type FileTypeIconProps = {\n /** The file or folder name; its extension picks the glyph and the type colour. */\n name: string;\n mediaType?: string;\n folder?: boolean;\n /** Shows an open folder, for example the folder a list is showing. */\n open?: boolean;\n /** A Tabler icon class that replaces the glyph the name picks; the type colour still follows the name. */\n icon?: string;\n /** `sm` for tree rows, `md` for list rows, `lg` for grid previews. Defaults to `md`. */\n size?: \"sm\" | \"md\" | \"lg\";\n class?: string;\n};\n\n// Known extensions decide first: a generic media type such as `text/plain` or `text/csv` must not hide a sheet,\n// a slide deck, a document, or an archive behind the neutral family.\nconst BY_EXTENSION = new Map<string, FileType>(\n (\n [\n [\"sheet\", [\"xls\", \"xlsx\", \"ods\", \"csv\"]],\n [\"slides\", [\"ppt\", \"pptx\", \"odp\"]],\n [\"document\", [\"doc\", \"docx\", \"odt\", \"rtf\"]],\n [\"archive\", [\"zip\", \"tar\", \"gz\", \"tgz\", \"bz2\", \"xz\", \"7z\", \"rar\"]],\n ] as const\n ).flatMap(([type, extensions]) => extensions.map((extension) => [extension, type] as const)),\n);\n\n/** Resolve the colour family for a file or folder from its name and media type. */\nexport const fileTypeOf = (file: { name: string; mediaType?: string; folder?: boolean }): FileType => {\n if (file.folder) return \"folder\";\n const known = BY_EXTENSION.get(file.name.split(\".\").pop()?.toLowerCase() ?? \"\");\n if (known) return known;\n const category = fileIcons.getFileCategory({ name: file.name, type: \"file\", mimeType: file.mediaType });\n return category === \"pdf\" || category === \"image\" || category === \"video\" || category === \"audio\" || category === \"archive\"\n ? category\n : \"other\";\n};\n\n/**\n * The readable kind of a file in `locale`, such as \"PDF\", \"Image\", or \"Spreadsheet\", for a meta line like\n * \"PDF · 4.2 MB\" instead of a media type. An unknown kind names the file's extension (\"ICC\"), and a file without\n * one reads \"File\".\n */\nexport const fileKindLabel = (file: { name: string; mediaType?: string; folder?: boolean }, locale: string): string => {\n const t = resolveFileListMessages(locale);\n const type = fileTypeOf(file);\n if (type !== \"other\") return t[type];\n const dot = file.name.lastIndexOf(\".\");\n const extension = dot > 0 ? file.name.slice(dot + 1) : \"\";\n return /^[a-z0-9]+$/i.test(extension) ? extension.toUpperCase() : t.file;\n};\n\n/** `fileIcons` adds Tailwind colour utilities that @k2b/ui does not ship; only the `ti-*` glyph is kept. */\nconst glyph = (props: FileTypeIconProps): string => {\n // `fileIcons` looks names up in plain objects, so a name such as `constructor` returns an inherited member\n // instead of a class string; such names fall back to the generic glyph.\n const found: unknown =\n props.icon ?? fileIcons.getFileIcon({ name: props.name, type: props.folder ? \"directory\" : \"file\", mimeType: props.mediaType });\n const icon =\n (typeof found === \"string\" ? found : \"\")\n .split(/\\s+/)\n .filter((token) => token.startsWith(\"ti-\"))\n .join(\" \") || (props.folder ? \"ti-folder\" : \"ti-file\");\n return props.folder && props.open ? icon.replace(/\\bti-folder$/, \"ti-folder-open\") : icon;\n};\n\n/**\n * A file or folder glyph in a soft chip of its type colour. Folders take the app accent hook\n * `--k2b-app-workspace-active`; code, text, and unknown files a neutral ink. The box size is fixed per `size`.\n */\nexport function FileTypeIcon(props: FileTypeIconProps): JSX.Element {\n return (\n <span\n class={`k2b-icon-tile k2b-file-type-icon ${props.class ?? \"\"}`}\n data-file-type={fileTypeOf({ name: props.name, mediaType: props.mediaType, folder: props.folder })}\n data-size={props.size ?? \"md\"}\n aria-hidden=\"true\"\n >\n <i class={`ti ${glyph(props)}`} />\n </span>\n );\n}\n"],"mappings":"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","ignoreList":[]}
@@ -906,10 +906,17 @@ const BUILTIN_RENDERERS = [{
906
906
 
907
907
  // ── Component ───────────────────────────────────────────────────────────────
908
908
 
909
- export const formatFileViewSize = bytes => {
910
- if (bytes < 1024) return `${bytes} B`;
911
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
912
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
909
+ /**
910
+ * A file size such as "4.2 MB". With `locale`, the number follows that language ("4,2 MB") and drops a trailing
911
+ * zero ("64 KB"); without it, the output stays as before.
912
+ */
913
+ export const formatFileViewSize = (bytes, locale) => {
914
+ const [value, unit] = bytes < 1024 ? [bytes, "B"] : bytes < 1024 * 1024 ? [bytes / 1024, "KB"] : [bytes / (1024 * 1024), "MB"];
915
+ if (unit === "B") return `${bytes} B`;
916
+ if (!locale) return `${value.toFixed(1)} ${unit}`;
917
+ return `${new Intl.NumberFormat(locale, {
918
+ maximumFractionDigits: 1
919
+ }).format(value)} ${unit}`;
913
920
  };
914
921
  export default function FileView(props) {
915
922
  const messages = useUiMessages();