@revenexx/studio-shared 0.4.0 → 0.4.1

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 (110) hide show
  1. package/dist/module.d.mts +5 -3
  2. package/dist/module.json +2 -2
  3. package/dist/module.mjs +7 -3
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +1 -1
  5. package/dist/runtime/components/Dashboard/DashboardGrid.vue +45 -1
  6. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +1 -1
  7. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +1 -1
  8. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +1 -1
  9. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +1 -1
  10. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +2 -2
  11. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +14 -3
  12. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +2 -2
  13. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +73 -34
  14. package/dist/runtime/components/DataTable/DataTable.vue +352 -52
  15. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +73 -34
  16. package/dist/runtime/components/DataTable/FacetPanel.vue +57 -8
  17. package/dist/runtime/components/DataTable/FilterMenu.vue +5 -5
  18. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +61 -12
  19. package/dist/runtime/components/DataTable/SortMenu.vue +4 -1
  20. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.d.vue.ts +23 -0
  21. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue +29 -0
  22. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue.d.ts +23 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +17 -0
  24. package/dist/runtime/components/Form/FieldGrid.vue +1 -1
  25. package/dist/runtime/components/Form/SaveBar.d.vue.ts +1 -1
  26. package/dist/runtime/components/Form/SaveBar.vue.d.ts +1 -1
  27. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +4 -4
  28. package/dist/runtime/components/Media/MediaFolderTree.vue +1 -2
  29. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +4 -4
  30. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +11 -0
  31. package/dist/runtime/components/Media/MediaPickerDialog.vue +206 -54
  32. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +11 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue +1 -1
  34. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +1 -1
  35. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +1 -1
  36. package/dist/runtime/components/Shared/CardSizeToggle.d.vue.ts +11 -0
  37. package/dist/runtime/components/Shared/CardSizeToggle.vue +19 -0
  38. package/dist/runtime/components/Shared/CardSizeToggle.vue.d.ts +11 -0
  39. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +1 -1
  40. package/dist/runtime/components/Shared/DetailSheet.vue +9 -5
  41. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +1 -1
  42. package/dist/runtime/components/Shared/EmptyNote.d.vue.ts +19 -0
  43. package/dist/runtime/components/Shared/EmptyNote.vue +25 -0
  44. package/dist/runtime/components/Shared/EmptyNote.vue.d.ts +19 -0
  45. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +6 -0
  46. package/dist/runtime/components/Shared/KpiTile.vue +10 -9
  47. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +6 -0
  48. package/dist/runtime/components/Shared/PageHeader.vue +15 -4
  49. package/dist/runtime/components/Shared/RecordLayout.vue +1 -2
  50. package/dist/runtime/components/Shared/RecordRailCard.vue +3 -3
  51. package/dist/runtime/components/Shared/SelectionBar.d.vue.ts +22 -0
  52. package/dist/runtime/components/Shared/SelectionBar.vue +40 -0
  53. package/dist/runtime/components/Shared/SelectionBar.vue.d.ts +22 -0
  54. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -2
  55. package/dist/runtime/components/Shared/SidebarNav.vue +71 -25
  56. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -2
  57. package/dist/runtime/components/Shared/SidebarNavAreas.d.vue.ts +7 -0
  58. package/dist/runtime/components/Shared/SidebarNavAreas.vue +291 -0
  59. package/dist/runtime/components/Shared/SidebarNavAreas.vue.d.ts +7 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +1 -9
  61. package/dist/runtime/components/Shared/SidebarNavLink.vue +31 -5
  62. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +1 -9
  63. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  64. package/dist/runtime/components/Shared/StateCard.vue +1 -1
  65. package/dist/runtime/components/Shared/TabBar.vue +6 -7
  66. package/dist/runtime/components/Shared/Wizard.d.vue.ts +11 -1
  67. package/dist/runtime/components/Shared/Wizard.vue +9 -5
  68. package/dist/runtime/components/Shared/Wizard.vue.d.ts +11 -1
  69. package/dist/runtime/composables/useMediaLibrary.d.ts +12 -0
  70. package/dist/runtime/composables/useSavedViews.d.ts +11 -1
  71. package/dist/runtime/composables/useSavedViews.js +4 -4
  72. package/dist/runtime/composables/useWidgetFigures.d.ts +12 -0
  73. package/dist/runtime/composables/useWidgetFigures.js +24 -0
  74. package/dist/runtime/composables/useWidgetHeader.d.ts +15 -0
  75. package/dist/runtime/composables/useWidgetHeader.js +15 -0
  76. package/dist/runtime/lib/auditEnvelope.d.ts +43 -0
  77. package/dist/runtime/lib/auditEnvelope.js +55 -0
  78. package/dist/runtime/lib/cardSize.d.ts +13 -0
  79. package/dist/runtime/lib/cardSize.js +20 -0
  80. package/dist/runtime/lib/dashboardIcons.js +2 -0
  81. package/dist/runtime/lib/dataTableFilters.d.ts +22 -0
  82. package/dist/runtime/lib/dataTableFilters.js +5 -0
  83. package/dist/runtime/lib/facets.d.ts +13 -0
  84. package/dist/runtime/lib/formLayout.d.ts +74 -4
  85. package/dist/runtime/lib/formLayout.js +53 -6
  86. package/dist/runtime/lib/gridTracks.d.ts +14 -1
  87. package/dist/runtime/lib/gridTracks.js +13 -2
  88. package/dist/runtime/lib/isoLists.d.ts +37 -2
  89. package/dist/runtime/lib/isoLists.js +54 -1
  90. package/dist/runtime/lib/notice.d.ts +14 -0
  91. package/dist/runtime/lib/notice.js +19 -0
  92. package/dist/runtime/lib/pageSize.d.ts +15 -0
  93. package/dist/runtime/lib/pageSize.js +26 -0
  94. package/dist/runtime/lib/sidebarAreas.d.ts +46 -0
  95. package/dist/runtime/lib/sidebarAreas.js +25 -0
  96. package/dist/runtime/lib/sidebarNav.d.ts +21 -2
  97. package/dist/runtime/lib/sidebarNav.js +23 -4
  98. package/dist/runtime/lib/solarIconMap.d.ts +2 -1
  99. package/dist/runtime/lib/solarIconMap.js +50 -4
  100. package/dist/runtime/lib/swipeToClose.d.ts +25 -0
  101. package/dist/runtime/lib/swipeToClose.js +118 -0
  102. package/dist/runtime/lib/tableExport.d.ts +31 -0
  103. package/dist/runtime/lib/tableExport.js +143 -0
  104. package/dist/runtime/lib/talkback.d.ts +5 -0
  105. package/dist/runtime/lib/talkback.js +8 -0
  106. package/dist/runtime/lib/talkbackTokenBatch.d.ts +35 -0
  107. package/dist/runtime/lib/talkbackTokenBatch.js +101 -0
  108. package/dist/runtime/types/nav.d.ts +9 -0
  109. package/dist/types.d.mts +4 -0
  110. package/package.json +4 -3
@@ -29,8 +29,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
29
  onCancel?: (() => any) | undefined;
30
30
  }>, {
31
31
  dirty: boolean;
32
- canSubmit: boolean;
33
32
  busy: boolean;
33
+ canSubmit: boolean;
34
34
  allowClean: boolean;
35
35
  hideCancel: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,7 +1,7 @@
1
1
  import type { StorageFolderNode } from '../../types/storage.js';
2
2
  /**
3
- * Recursive folder tree for the media browser sidebar. Emits `select` with a
4
- * folder id (or null for the synthetic root). Self-renders for children.
3
+ * Recursive folder tree for the media browser sidebar. Emits `select` with the
4
+ * clicked folder's id. Self-renders for children.
5
5
  */
6
6
  type __VLS_Props = {
7
7
  nodes: StorageFolderNode[];
@@ -10,9 +10,9 @@ type __VLS_Props = {
10
10
  depth?: number;
11
11
  };
12
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
- select: (id: string | null) => any;
13
+ select: (id: string) => any;
14
14
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
- onSelect?: ((id: string | null) => any) | undefined;
15
+ onSelect?: ((id: string) => any) | undefined;
16
16
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
17
  declare const _default: typeof __VLS_export;
18
18
  export default _default;
@@ -38,8 +38,7 @@ const t = useT();
38
38
  <AltArrowRight
39
39
  :size="12"
40
40
  class="transition-transform"
41
- :class="{ 'rotate-90': expanded.has(node.id) }"
42
- />
41
+ :class="{ 'rotate-90': expanded.has(node.id) }" weight="Bold" />
43
42
  </button>
44
43
  <component
45
44
  :is="currentId === node.id ? FolderOpen : FolderIcon"
@@ -1,7 +1,7 @@
1
1
  import type { StorageFolderNode } from '../../types/storage.js';
2
2
  /**
3
- * Recursive folder tree for the media browser sidebar. Emits `select` with a
4
- * folder id (or null for the synthetic root). Self-renders for children.
3
+ * Recursive folder tree for the media browser sidebar. Emits `select` with the
4
+ * clicked folder's id. Self-renders for children.
5
5
  */
6
6
  type __VLS_Props = {
7
7
  nodes: StorageFolderNode[];
@@ -10,9 +10,9 @@ type __VLS_Props = {
10
10
  depth?: number;
11
11
  };
12
12
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
- select: (id: string | null) => any;
13
+ select: (id: string) => any;
14
14
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
- onSelect?: ((id: string | null) => any) | undefined;
15
+ onSelect?: ((id: string) => any) | undefined;
16
16
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
17
  declare const _default: typeof __VLS_export;
18
18
  export default _default;
@@ -1,19 +1,30 @@
1
1
  import type { StorageAsset } from '../../types/storage.js';
2
+ export interface PickerCatalogueEntry {
3
+ /** What the record is called — shown instead of the file name. */
4
+ title: string;
5
+ /** One quiet line under it: type, licence. */
6
+ sub?: string;
7
+ }
2
8
  type __VLS_Props = {
3
9
  open: boolean;
4
10
  multiple?: boolean;
5
11
  /** Restrict the grid to these asset kinds (e.g. ['image']). */
6
12
  kinds?: string[];
13
+ /** Records that manage these files, keyed by storage id — see the header. */
14
+ catalogue?: Record<string, PickerCatalogueEntry> | null;
7
15
  };
8
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
17
  "update:open": (value: boolean) => any;
10
18
  select: (assets: StorageAsset[]) => any;
19
+ uploaded: (assets: StorageAsset[]) => any;
11
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
21
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
13
22
  onSelect?: ((assets: StorageAsset[]) => any) | undefined;
23
+ onUploaded?: ((assets: StorageAsset[]) => any) | undefined;
14
24
  }>, {
15
25
  multiple: boolean;
16
26
  kinds: string[];
27
+ catalogue: Record<string, PickerCatalogueEntry> | null;
17
28
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
29
  declare const _default: typeof __VLS_export;
19
30
  export default _default;
@@ -1,14 +1,16 @@
1
1
  <script setup>
2
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@revenexx/studio";
3
- import { CloudUpload, FileText } from "#solar-icons";
2
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, SegmentedToggle, SegmentedToggleItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@revenexx/studio";
3
+ import { CloudUpload, FileText, Gallery, ListCheck, Magnifer } from "#solar-icons";
4
4
  import MediaFolderTree from "./MediaFolderTree.vue";
5
5
  import MediaUploader from "./MediaUploader.vue";
6
6
  const props = defineProps({
7
7
  open: { type: Boolean, required: true },
8
8
  multiple: { type: Boolean, required: false, default: false },
9
- kinds: { type: Array, required: false, default: void 0 }
9
+ kinds: { type: Array, required: false, default: void 0 },
10
+ catalogue: { type: [Object, null], required: false, default: null }
10
11
  });
11
- const emit = defineEmits(["update:open", "select"]);
12
+ const emit = defineEmits(["update:open", "select", "uploaded"]);
13
+ const lib = useMediaLibrary();
12
14
  const {
13
15
  assets,
14
16
  folderTree,
@@ -23,15 +25,76 @@ const {
23
25
  loadMore,
24
26
  thumbnailUrl,
25
27
  isProcessing
26
- } = useMediaLibrary();
28
+ } = lib;
27
29
  const t = useT();
28
30
  const selected = ref([]);
29
31
  const uploadOpen = ref(false);
30
32
  const failedThumbs = ref(/* @__PURE__ */ new Set());
31
- const kindIcon = {};
32
- const visibleAssets = computed(
33
- () => props.kinds?.length ? assets.value.filter((a) => props.kinds.includes(a.kind)) : assets.value
34
- );
33
+ const canSearch = computed(() => !!lib.search && !!lib.loadAssets);
34
+ const query = ref("");
35
+ const KIND_CHIPS = [
36
+ { value: "image", key: "images" },
37
+ { value: "video", key: "videos" },
38
+ { value: "pdf", key: "pdfs" },
39
+ { value: "document", key: "documents" }
40
+ ];
41
+ const chips = computed(() => KIND_CHIPS.filter((c) => !props.kinds?.length || props.kinds.includes(c.value)));
42
+ const kindChip = ref(null);
43
+ const sort = ref("-created_at");
44
+ let searchTimer = null;
45
+ function reload() {
46
+ if (!lib.loadAssets) return;
47
+ if (lib.search) lib.search.value = query.value.trim();
48
+ if (lib.kind) lib.kind.value = kindChip.value;
49
+ if (lib.sortKey) lib.sortKey.value = sort.value;
50
+ void lib.loadAssets();
51
+ }
52
+ watch(query, () => {
53
+ if (searchTimer) clearTimeout(searchTimer);
54
+ searchTimer = setTimeout(reload, 300);
55
+ });
56
+ watch([kindChip, sort], reload);
57
+ const scope = ref("assets");
58
+ const hasCatalogue = computed(() => !!props.catalogue);
59
+ const entryOf = (a) => props.catalogue?.[a.id] ?? null;
60
+ const visibleAssets = computed(() => {
61
+ let list = props.kinds?.length ? assets.value.filter((a) => props.kinds.includes(a.kind)) : assets.value;
62
+ if (hasCatalogue.value && scope.value === "assets") list = list.filter((a) => !!entryOf(a));
63
+ return list;
64
+ });
65
+ const titleOf = (a) => entryOf(a)?.title || a.display_name || a.original_name;
66
+ function factsOf(a) {
67
+ const ext = /\.([a-z0-9]{2,5})$/i.exec(a.original_name ?? "")?.[1]?.toUpperCase() ?? (a.mime_type?.split("/")[1] ?? "").toUpperCase();
68
+ const kb = a.size_bytes != null ? a.size_bytes >= 1048576 ? `${(a.size_bytes / 1048576).toFixed(1).replace(".", ",")} MB` : `${Math.max(1, Math.round(a.size_bytes / 1024))} KB` : "";
69
+ const dims = a.width && a.height ? `${a.width} \xD7 ${a.height}` : "";
70
+ return [ext, kb, dims].filter(Boolean).join(" \xB7 ");
71
+ }
72
+ const VIEW_KEY = "rx.media.picker.view";
73
+ function storedView() {
74
+ try {
75
+ return import.meta.client && localStorage.getItem(VIEW_KEY) === "list" ? "list" : "gallery";
76
+ } catch {
77
+ return "gallery";
78
+ }
79
+ }
80
+ const view = ref(storedView());
81
+ watch(view, (v) => {
82
+ try {
83
+ localStorage.setItem(VIEW_KEY, v);
84
+ } catch {
85
+ }
86
+ });
87
+ const sentinel = ref(null);
88
+ let observer = null;
89
+ watch(sentinel, (el) => {
90
+ observer?.disconnect();
91
+ if (!el || typeof IntersectionObserver === "undefined") return;
92
+ observer = new IntersectionObserver((entries) => {
93
+ if (entries.some((e) => e.isIntersecting) && hasMore.value && !loadingAssets.value) void loadMore();
94
+ }, { rootMargin: "200px" });
95
+ observer.observe(el);
96
+ });
97
+ onBeforeUnmount(() => observer?.disconnect());
35
98
  function isSelected(asset) {
36
99
  return selected.value.some((a) => a.id === asset.id);
37
100
  }
@@ -52,31 +115,46 @@ function confirm() {
52
115
  emit("select", selected.value);
53
116
  emit("update:open", false);
54
117
  }
118
+ let beforeUpload = /* @__PURE__ */ new Set();
119
+ function startUpload() {
120
+ beforeUpload = new Set(assets.value.map((a) => a.id));
121
+ uploadOpen.value = true;
122
+ }
55
123
  function onUploaded() {
56
124
  uploadOpen.value = false;
125
+ const fresh = assets.value.filter((a) => !beforeUpload.has(a.id));
126
+ if (fresh.length) emit("uploaded", fresh);
57
127
  }
58
128
  watch(() => props.open, (open) => {
59
129
  if (open) {
60
130
  selected.value = [];
131
+ query.value = "";
132
+ kindChip.value = null;
133
+ sort.value = "-created_at";
134
+ scope.value = "assets";
135
+ lib.resetFilters?.();
61
136
  void init();
137
+ } else {
138
+ lib.resetFilters?.();
62
139
  }
63
140
  });
64
141
  </script>
65
142
 
66
143
  <template>
67
144
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
68
- <DialogContent class="max-w-4xl">
145
+ <DialogContent class="sm:max-w-5xl">
69
146
  <DialogHeader>
70
147
  <DialogTitle>{{ multiple ? t("shared.media.picker.title.multiple") : t("shared.media.picker.title.single") }}</DialogTitle>
148
+ <DialogDescription>{{ multiple ? t("shared.media.picker.description.multiple") : t("shared.media.picker.description.single") }}</DialogDescription>
71
149
  </DialogHeader>
72
150
 
73
- <div class="flex gap-4 h-[26rem]">
151
+ <div class="flex h-[30rem] gap-4">
74
152
  <!-- Folder sidebar -->
75
153
  <div class="w-56 shrink-0 overflow-y-auto border-r border-black/[0.06] pr-2">
76
154
  <button
77
155
  type="button"
78
- class="w-full text-left rounded-md px-2 py-1 text-sm transition-colors"
79
- :class="currentView === 'all' ? 'bg-primary/10 text-[#0B0522] font-medium' : 'text-foreground/80 hover:bg-primary/5'"
156
+ class="w-full rounded-md px-2 py-1 text-left text-sm transition-colors"
157
+ :class="currentView === 'all' ? 'bg-primary/10 font-medium text-[#0B0522]' : 'text-foreground/80 hover:bg-primary/5'"
80
158
  @click="showAllMedia"
81
159
  >
82
160
  {{ t("shared.media.picker.action.allmedia") }}
@@ -88,53 +166,127 @@ watch(() => props.open, (open) => {
88
166
  />
89
167
  </div>
90
168
 
91
- <!-- Asset grid -->
92
- <div class="flex-1 overflow-y-auto">
93
- <div class="flex items-center justify-between mb-2">
94
- <p class="text-sm text-muted-foreground">
95
- {{ t("shared.media.picker.count", visibleAssets.length) }}
96
- </p>
97
- <Button variant="outline" size="sm" class="h-7 text-sm" @click="uploadOpen = true">
169
+ <div class="flex min-w-0 flex-1 flex-col">
170
+ <!-- Toolbar: search, kinds, sort, scope, view, upload -->
171
+ <div class="mb-2 flex flex-wrap items-center gap-2">
172
+ <div v-if="canSearch" class="relative min-w-48 flex-1">
173
+ <Magnifer :size="14" class="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
174
+ <Input v-model="query" type="search" class="h-8 pl-8 text-sm" :placeholder="t('shared.media.picker.search')" :aria-label="t('shared.media.picker.search')" />
175
+ </div>
176
+ <Select v-if="canSearch && lib.sortKey" v-model="sort">
177
+ <SelectTrigger class="h-8 w-36 text-sm" :aria-label="t('shared.media.picker.sort.label')"><SelectValue /></SelectTrigger>
178
+ <SelectContent>
179
+ <SelectItem value="-created_at">{{ t("shared.media.picker.sort.newest") }}</SelectItem>
180
+ <SelectItem value="original_name">{{ t("shared.media.picker.sort.name") }}</SelectItem>
181
+ </SelectContent>
182
+ </Select>
183
+ <SegmentedToggle v-model="view" variant="inset" :aria-label="t('shared.media.picker.view.label')">
184
+ <SegmentedToggleItem value="gallery" :aria-label="t('shared.media.picker.view.gallery')"><Gallery :size="14" /></SegmentedToggleItem>
185
+ <SegmentedToggleItem value="list" :aria-label="t('shared.media.picker.view.list')"><ListCheck :size="14" /></SegmentedToggleItem>
186
+ </SegmentedToggle>
187
+ <Button variant="outline" size="sm" class="h-8 text-sm" @click="startUpload">
98
188
  <CloudUpload :size="14" /> {{ t("shared.media.picker.action.upload") }}
99
189
  </Button>
100
190
  </div>
101
-
102
- <div v-if="loadingAssets && !visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
103
- {{ t("shared.media.picker.loading") }}
104
- </div>
105
- <div v-else-if="!visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
106
- {{ t("shared.media.picker.empty") }}
107
- </div>
108
- <div v-else class="grid grid-cols-3 gap-3 sm:grid-cols-4">
109
- <button
110
- v-for="asset in visibleAssets"
111
- :key="asset.id"
112
- type="button"
113
- class="group relative aspect-square rounded-md border overflow-hidden transition-all"
114
- :class="isSelected(asset) ? 'border-primary ring-2 ring-primary' : 'border-black/[0.06] hover:border-primary/40'"
115
- @click="toggle(asset)"
116
- >
117
- <img
118
- v-if="thumbnailUrl(asset) && !failedThumbs.has(thumbnailUrl(asset))"
119
- :src="thumbnailUrl(asset)"
120
- :alt="asset.alt_text ?? asset.display_name ?? asset.original_name"
121
- class="h-full w-full object-cover"
122
- :class="{ 'opacity-50': isProcessing(asset) }"
123
- @error="markFailed(thumbnailUrl(asset))"
191
+ <div class="mb-2 flex flex-wrap items-center gap-1.5">
192
+ <template v-if="hasCatalogue">
193
+ <button
194
+ v-for="s in ['assets', 'files']"
195
+ :key="s"
196
+ type="button"
197
+ class="rounded-full border px-2.5 py-0.5 text-xs transition-colors"
198
+ :class="scope === s ? 'border-primary bg-primary/10 text-[#0B0522]' : 'border-black/[0.08] text-muted-foreground hover:border-primary/40'"
199
+ @click="scope = s"
200
+ >
201
+ {{ t(`shared.media.picker.scope.${s}`) }}
202
+ </button>
203
+ <span class="mx-1 h-4 w-px bg-black/[0.08]" />
204
+ </template>
205
+ <template v-if="canSearch && lib.kind && chips.length > 1">
206
+ <button
207
+ type="button"
208
+ class="rounded-full border px-2.5 py-0.5 text-xs transition-colors"
209
+ :class="kindChip === null ? 'border-primary bg-primary/10 text-[#0B0522]' : 'border-black/[0.08] text-muted-foreground hover:border-primary/40'"
210
+ @click="kindChip = null"
124
211
  >
125
- <span v-else class="flex h-full w-full items-center justify-center">
126
- <component :is="kindIcon[asset.kind] ?? FileText" :size="28" class="text-foreground/30" />
127
- </span>
128
- <span class="absolute inset-x-0 bottom-0 truncate bg-black/50 px-1.5 py-0.5 text-[11px] text-white">
129
- {{ asset.display_name ?? asset.original_name }}
130
- </span>
131
- </button>
212
+ {{ t("shared.media.picker.kind.all") }}
213
+ </button>
214
+ <button
215
+ v-for="c in chips"
216
+ :key="c.value"
217
+ type="button"
218
+ class="rounded-full border px-2.5 py-0.5 text-xs transition-colors"
219
+ :class="kindChip === c.value ? 'border-primary bg-primary/10 text-[#0B0522]' : 'border-black/[0.08] text-muted-foreground hover:border-primary/40'"
220
+ @click="kindChip = kindChip === c.value ? null : c.value"
221
+ >
222
+ {{ t(`shared.media.picker.kind.${c.key}`) }}
223
+ </button>
224
+ </template>
225
+ <span class="ml-auto text-xs text-muted-foreground">{{ t("shared.media.picker.count", visibleAssets.length) }}</span>
132
226
  </div>
133
227
 
134
- <div v-if="hasMore" class="mt-3 flex justify-center">
135
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loadingAssets" @click="loadMore">
136
- {{ t("shared.media.picker.action.loadmore") }}
137
- </Button>
228
+ <div class="min-h-0 flex-1 overflow-y-auto">
229
+ <div v-if="loadingAssets && !visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
230
+ {{ t("shared.media.picker.loading") }}
231
+ </div>
232
+ <div v-else-if="!visibleAssets.length" class="py-12 text-center text-sm text-muted-foreground">
233
+ <p class="font-medium text-foreground/80">{{ hasCatalogue && scope === "assets" ? t("shared.media.picker.emptyAssets") : t("shared.media.picker.empty") }}</p>
234
+ <button v-if="hasCatalogue && scope === 'assets'" type="button" class="mt-1 text-primary hover:underline" @click="scope = 'files'">{{ t("shared.media.picker.showFiles") }}</button>
235
+ </div>
236
+ <div v-else-if="view === 'gallery'" class="grid grid-cols-3 gap-3 sm:grid-cols-4">
237
+ <button
238
+ v-for="asset in visibleAssets"
239
+ :key="asset.id"
240
+ type="button"
241
+ class="group relative aspect-square overflow-hidden rounded-md border transition-all"
242
+ :class="isSelected(asset) ? 'border-primary ring-2 ring-primary' : 'border-black/[0.06] hover:border-primary/40'"
243
+ :aria-pressed="isSelected(asset)"
244
+ @click="toggle(asset)"
245
+ >
246
+ <img
247
+ v-if="thumbnailUrl(asset) && !failedThumbs.has(thumbnailUrl(asset))"
248
+ :src="thumbnailUrl(asset)"
249
+ :alt="asset.alt_text ?? titleOf(asset)"
250
+ class="h-full w-full object-cover"
251
+ :class="{ 'opacity-50': isProcessing(asset) }"
252
+ @error="markFailed(thumbnailUrl(asset))"
253
+ >
254
+ <span v-else class="flex h-full w-full items-center justify-center">
255
+ <FileText :size="28" class="text-foreground/30" />
256
+ </span>
257
+ <span class="absolute inset-x-0 bottom-0 bg-black/55 px-1.5 py-0.5 text-left text-[11px] text-white">
258
+ <span class="block truncate">{{ titleOf(asset) }}</span>
259
+ <span v-if="entryOf(asset)?.sub" class="block truncate text-white/75">{{ entryOf(asset).sub }}</span>
260
+ </span>
261
+ </button>
262
+ </div>
263
+ <ul v-else class="divide-y divide-black/[0.06] rounded-md border border-black/[0.06]">
264
+ <li v-for="asset in visibleAssets" :key="asset.id">
265
+ <button
266
+ type="button"
267
+ class="flex w-full items-center gap-3 px-2 py-1.5 text-left text-sm transition-colors"
268
+ :class="isSelected(asset) ? 'bg-primary/10' : 'hover:bg-primary/[0.04]'"
269
+ :aria-pressed="isSelected(asset)"
270
+ @click="toggle(asset)"
271
+ >
272
+ <span class="grid size-9 shrink-0 place-items-center overflow-hidden rounded border border-black/[0.06] bg-muted/40">
273
+ <img v-if="thumbnailUrl(asset, 80) && !failedThumbs.has(thumbnailUrl(asset, 80))" :src="thumbnailUrl(asset, 80)" alt="" class="h-full w-full object-cover" @error="markFailed(thumbnailUrl(asset, 80))">
274
+ <FileText v-else :size="16" class="text-foreground/30" />
275
+ </span>
276
+ <span class="min-w-0 flex-1">
277
+ <span class="block truncate font-medium text-[#0B0522]">{{ titleOf(asset) }}</span>
278
+ <span class="block truncate text-xs text-muted-foreground">{{ [entryOf(asset)?.sub, factsOf(asset)].filter(Boolean).join(" \xB7 ") }}</span>
279
+ </span>
280
+ <span v-if="!asset.alt_text && asset.kind === 'image'" class="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[11px] text-amber-700">{{ t("shared.media.picker.altMissing") }}</span>
281
+ </button>
282
+ </li>
283
+ </ul>
284
+
285
+ <div v-if="hasMore" ref="sentinel" class="mt-3 flex justify-center">
286
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loadingAssets" @click="loadMore">
287
+ {{ t("shared.media.picker.action.loadmore") }}
288
+ </Button>
289
+ </div>
138
290
  </div>
139
291
  </div>
140
292
  </div>
@@ -1,19 +1,30 @@
1
1
  import type { StorageAsset } from '../../types/storage.js';
2
+ export interface PickerCatalogueEntry {
3
+ /** What the record is called — shown instead of the file name. */
4
+ title: string;
5
+ /** One quiet line under it: type, licence. */
6
+ sub?: string;
7
+ }
2
8
  type __VLS_Props = {
3
9
  open: boolean;
4
10
  multiple?: boolean;
5
11
  /** Restrict the grid to these asset kinds (e.g. ['image']). */
6
12
  kinds?: string[];
13
+ /** Records that manage these files, keyed by storage id — see the header. */
14
+ catalogue?: Record<string, PickerCatalogueEntry> | null;
7
15
  };
8
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
17
  "update:open": (value: boolean) => any;
10
18
  select: (assets: StorageAsset[]) => any;
19
+ uploaded: (assets: StorageAsset[]) => any;
11
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
21
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
13
22
  onSelect?: ((assets: StorageAsset[]) => any) | undefined;
23
+ onUploaded?: ((assets: StorageAsset[]) => any) | undefined;
14
24
  }>, {
15
25
  multiple: boolean;
16
26
  kinds: string[];
27
+ catalogue: Record<string, PickerCatalogueEntry> | null;
17
28
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
29
  declare const _default: typeof __VLS_export;
19
30
  export default _default;
@@ -82,7 +82,7 @@ const folderLabel = computed(() => props.folderName ?? t("shared.media.uploader.
82
82
 
83
83
  <template>
84
84
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
85
- <DialogContent class="max-w-lg">
85
+ <DialogContent class="sm:max-w-xl">
86
86
  <DialogHeader>
87
87
  <DialogTitle>{{ t("shared.media.uploader.title") }}</DialogTitle>
88
88
  <DialogDescription>
@@ -13,8 +13,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
13
13
  size: number;
14
14
  icon: string | null;
15
15
  fill: boolean;
16
- navIcon: string | null;
17
16
  alt: string;
17
+ navIcon: string | null;
18
18
  fallback: Component;
19
19
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
20
  declare const _default: typeof __VLS_export;
@@ -13,8 +13,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
13
13
  size: number;
14
14
  icon: string | null;
15
15
  fill: boolean;
16
- navIcon: string | null;
17
16
  alt: string;
17
+ navIcon: string | null;
18
18
  fallback: Component;
19
19
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
20
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,11 @@
1
+ import type { CardSize } from '../../lib/cardSize.js';
2
+ type __VLS_ModelProps = {
3
+ modelValue?: CardSize;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:modelValue": (value: CardSize) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:modelValue"?: ((value: CardSize) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,19 @@
1
+ <script setup>
2
+ import { HamburgerMenu, List } from "#solar-icons";
3
+ import { SegmentedToggle, SegmentedToggleItem } from "@revenexx/studio";
4
+ import { useT } from "../../composables/useT";
5
+ const size = defineModel({ type: String, ...{ default: "comfortable" } });
6
+ const t = useT();
7
+ const ITEM = "transition-colors data-[state=on]:text-primary data-[state=off]:hover:text-primary!";
8
+ </script>
9
+
10
+ <template>
11
+ <SegmentedToggle v-model="size" variant="inset" :aria-label="t('shared.cardSize.label')" data-test="card-size">
12
+ <SegmentedToggleItem value="comfortable" :aria-label="t('shared.cardSize.comfortable')" :class="ITEM">
13
+ <List :size="14" weight="Bold" />
14
+ </SegmentedToggleItem>
15
+ <SegmentedToggleItem value="compact" :aria-label="t('shared.cardSize.compact')" :class="ITEM">
16
+ <HamburgerMenu :size="14" weight="Linear" />
17
+ </SegmentedToggleItem>
18
+ </SegmentedToggle>
19
+ </template>
@@ -0,0 +1,11 @@
1
+ import type { CardSize } from '../../lib/cardSize.js';
2
+ type __VLS_ModelProps = {
3
+ modelValue?: CardSize;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:modelValue": (value: CardSize) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:modelValue"?: ((value: CardSize) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -41,9 +41,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
41
41
  "onUpdate:open"?: ((open: boolean) => any) | undefined;
42
42
  onStep?: ((dir: 1 | -1) => any) | undefined;
43
43
  }>, {
44
+ busy: boolean;
44
45
  width: string;
45
46
  top: string;
46
- busy: boolean;
47
47
  returnFocus: () => HTMLElement | null | undefined;
48
48
  canStep: {
49
49
  prev: boolean;
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { AltArrowDown, AltArrowLeft, AltArrowUp, CloseCircle } from "#solar-icons";
3
3
  import { Sheet, SheetContent, SheetTitle } from "@revenexx/studio";
4
+ import { vSwipeClose } from "../../lib/swipeToClose";
4
5
  import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
5
6
  import { isCloseKey, isRecordStepKey } from "../../lib/detailSheet";
6
7
  import { useT } from "../../composables/useT";
@@ -109,7 +110,7 @@ onBeforeUnmount(() => {
109
110
  :aria-disabled="canStep.prev ? void 0 : 'true'"
110
111
  @click="stepBy(-1)"
111
112
  >
112
- <AltArrowUp :size="18" />
113
+ <AltArrowUp :size="18" weight="Bold" />
113
114
  </button>
114
115
  <button
115
116
  type="button"
@@ -120,7 +121,7 @@ onBeforeUnmount(() => {
120
121
  :aria-disabled="canStep.next ? void 0 : 'true'"
121
122
  @click="stepBy(1)"
122
123
  >
123
- <AltArrowDown :size="18" />
124
+ <AltArrowDown :size="18" weight="Bold" />
124
125
  </button>
125
126
  </template>
126
127
  <button
@@ -157,6 +158,9 @@ onBeforeUnmount(() => {
157
158
  class="w-full gap-0 p-0 sm:max-w-md [&>button.absolute]:hidden motion-reduce:animate-none motion-reduce:transition-none"
158
159
  @close-auto-focus="onCloseAutoFocus"
159
160
  >
161
+ <div v-swipe-close="close" class="flex shrink-0 justify-center pt-2 md:hidden">
162
+ <span class="h-1.5 w-10 rounded-full bg-black/15" />
163
+ </div>
160
164
  <SheetTitle class="sr-only">{{ label }}</SheetTitle>
161
165
  <div class="flex shrink-0 items-start gap-2 border-b border-black/[0.06] p-3">
162
166
  <button
@@ -165,7 +169,7 @@ onBeforeUnmount(() => {
165
169
  :aria-label="t('shared.detailsheet.action.back')"
166
170
  @click="close"
167
171
  >
168
- <AltArrowLeft :size="20" />
172
+ <AltArrowLeft :size="20" weight="Bold" />
169
173
  </button>
170
174
  <div class="min-w-0 flex-1 self-center">
171
175
  <slot name="header" />
@@ -180,7 +184,7 @@ onBeforeUnmount(() => {
180
184
  :aria-disabled="canStep.prev ? void 0 : 'true'"
181
185
  @click="stepBy(-1)"
182
186
  >
183
- <AltArrowUp :size="20" />
187
+ <AltArrowUp :size="20" weight="Bold" />
184
188
  </button>
185
189
  <button
186
190
  type="button"
@@ -191,7 +195,7 @@ onBeforeUnmount(() => {
191
195
  :aria-disabled="canStep.next ? void 0 : 'true'"
192
196
  @click="stepBy(1)"
193
197
  >
194
- <AltArrowDown :size="20" />
198
+ <AltArrowDown :size="20" weight="Bold" />
195
199
  </button>
196
200
  </template>
197
201
  </div>
@@ -41,9 +41,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
41
41
  "onUpdate:open"?: ((open: boolean) => any) | undefined;
42
42
  onStep?: ((dir: 1 | -1) => any) | undefined;
43
43
  }>, {
44
+ busy: boolean;
44
45
  width: string;
45
46
  top: string;
46
- busy: boolean;
47
47
  returnFocus: () => HTMLElement | null | undefined;
48
48
  canStep: {
49
49
  prev: boolean;
@@ -0,0 +1,19 @@
1
+ type __VLS_Props = {
2
+ title?: string | null;
3
+ description?: string | null;
4
+ /** One paragraph, split into headline and subtitle at its first sentence. */
5
+ text?: string | null;
6
+ };
7
+ declare var __VLS_1: {};
8
+ type __VLS_Slots = {} & {
9
+ default?: (props: typeof __VLS_1) => any;
10
+ };
11
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
15
+ type __VLS_WithSlots<T, S> = T & {
16
+ new (): {
17
+ $slots: S;
18
+ };
19
+ };