@hoardodile/ui 0.1.0 → 0.1.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.
- package/dist/components/breadcrumb.js +3 -0
- package/dist/components/breadcrumb.js.map +1 -1
- package/dist/components/caption-bar.js +3 -0
- package/dist/components/caption-bar.js.map +1 -1
- package/dist/components/color-picker.js +3 -0
- package/dist/components/color-picker.js.map +1 -1
- package/dist/components/combobox.js +3 -0
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/dropdown-menu.js +3 -0
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/dropdown-select.js +3 -0
- package/dist/components/dropdown-select.js.map +1 -1
- package/dist/components/font-picker.js +3 -0
- package/dist/components/font-picker.js.map +1 -1
- package/dist/components/image-crop-panel.js +3 -0
- package/dist/components/image-crop-panel.js.map +1 -1
- package/dist/components/pagination-bar.js +3 -0
- package/dist/components/pagination-bar.js.map +1 -1
- package/dist/components/pagination.js +3 -0
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/panel-toolbar.js +3 -0
- package/dist/components/panel-toolbar.js.map +1 -1
- package/dist/components/plugin-download-consent.d.ts +16 -9
- package/dist/components/plugin-download-consent.js +89 -33
- package/dist/components/plugin-download-consent.js.map +1 -1
- package/dist/components/search-field.js +3 -0
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/sidebar.js +3 -0
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/toast.js +3 -0
- package/dist/components/toast.js.map +1 -1
- package/dist/icons/actions.js +3 -0
- package/dist/icons/actions.js.map +1 -1
- package/dist/icons/registry.d.ts +2 -1
- package/dist/icons/registry.js +9 -1
- package/dist/icons/registry.js.map +1 -1
- package/package.json +2 -2
- package/src/components/plugin-download-consent.test.tsx +40 -4
- package/src/components/plugin-download-consent.tsx +130 -39
- package/src/icons/registry.ts +8 -0
|
@@ -9,6 +9,10 @@ import { useTranslation } from "react-i18next"
|
|
|
9
9
|
* both host surfaces (the app and the workbench) so the user sees one
|
|
10
10
|
* dialog with one contract wherever a plugin asks to download.
|
|
11
11
|
*
|
|
12
|
+
* One entry = one batch question: a single download is an items array of
|
|
13
|
+
* one (rendered with the single-file layout); a batched plugin call is
|
|
14
|
+
* one dialog listing every item, answered all-or-nothing.
|
|
15
|
+
*
|
|
12
16
|
* Presentational by design: the queue lives in the host's consent store
|
|
13
17
|
* (`@hoardodile/host-web`), this component renders exactly one ticket
|
|
14
18
|
* and reports the decision through callbacks. Copy comes from the shared
|
|
@@ -24,16 +28,20 @@ import { useTranslation } from "react-i18next"
|
|
|
24
28
|
* (and to `@hoardodile/host-web`'s consent-store entry). Declared here
|
|
25
29
|
* instead of imported so this package stays closure-clean.
|
|
26
30
|
*/
|
|
27
|
-
export type
|
|
28
|
-
readonly ticketId: string
|
|
29
|
-
readonly pluginId: string
|
|
30
|
-
readonly pluginName: string
|
|
31
|
+
export type PluginConsentItem = {
|
|
31
32
|
readonly url: string
|
|
32
33
|
readonly dest: string
|
|
33
34
|
readonly sizeBytes?: number
|
|
34
35
|
readonly reason?: string
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export type PluginConsentTicket = {
|
|
39
|
+
readonly ticketId: string
|
|
40
|
+
readonly pluginId: string
|
|
41
|
+
readonly pluginName: string
|
|
42
|
+
readonly items: readonly PluginConsentItem[]
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
export type PluginDownloadConsentDialogProps = {
|
|
38
46
|
/** Ticket to show; `null` hides the dialog (one at a time). */
|
|
39
47
|
readonly entry: PluginConsentTicket | null
|
|
@@ -45,10 +53,10 @@ export type PluginDownloadConsentDialogProps = {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
48
|
-
* Renders the shared consent question: URL verbatim
|
|
49
|
-
* monospace, never a link),
|
|
50
|
-
* plugin's stated reason, with Allow / Deny and a session-remember
|
|
51
|
-
* checkbox.
|
|
56
|
+
* Renders the shared consent question: every download URL verbatim
|
|
57
|
+
* (selectable monospace, never a link), its vault-relative destination
|
|
58
|
+
* and the plugin's stated reason, with Allow / Deny and a session-remember
|
|
59
|
+
* checkbox. A batch is one dialog listing all items.
|
|
52
60
|
*
|
|
53
61
|
* Design contract (hd-plugin-design): card + hairline + `--radius-2xl`
|
|
54
62
|
* + `--shadow-dialog`, footer parted by an inset hairline, focus on the
|
|
@@ -61,6 +69,7 @@ export function PluginDownloadConsentDialog(
|
|
|
61
69
|
const { t } = useTranslation("ui", { useSuspense: false })
|
|
62
70
|
const formatBytes = props.formatBytes ?? ((bytes: number) => `${bytes} B`)
|
|
63
71
|
const activeId = entry === null ? null : entry.ticketId
|
|
72
|
+
const itemCount = entry === null ? 0 : entry.items.length
|
|
64
73
|
const [remember, setRemember] = useState(false)
|
|
65
74
|
|
|
66
75
|
// A fresh ticket starts with "remember" unchecked — the choice must
|
|
@@ -75,14 +84,23 @@ export function PluginDownloadConsentDialog(
|
|
|
75
84
|
onOpenChange={(open) => {
|
|
76
85
|
if (!open && activeId !== null) onDeny(activeId)
|
|
77
86
|
}}
|
|
78
|
-
title={
|
|
87
|
+
title={
|
|
88
|
+
entry === null || itemCount <= 1
|
|
89
|
+
? t("pluginDownload.title", undefined)
|
|
90
|
+
: t("pluginDownload.titleMany", { count: itemCount })
|
|
91
|
+
}
|
|
79
92
|
eyebrow={t("pluginDownload.eyebrow", undefined)}
|
|
80
93
|
description={
|
|
81
94
|
entry === null
|
|
82
95
|
? undefined
|
|
83
|
-
:
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
: itemCount <= 1
|
|
97
|
+
? t("pluginDownload.description", {
|
|
98
|
+
pluginName: entry.pluginName,
|
|
99
|
+
})
|
|
100
|
+
: t("pluginDownload.descriptionMany", {
|
|
101
|
+
pluginName: entry.pluginName,
|
|
102
|
+
count: itemCount,
|
|
103
|
+
})
|
|
86
104
|
}
|
|
87
105
|
size="md"
|
|
88
106
|
contentTestId="plugin-download-consent"
|
|
@@ -110,20 +128,26 @@ export function PluginDownloadConsentDialog(
|
|
|
110
128
|
</div>
|
|
111
129
|
}
|
|
112
130
|
>
|
|
113
|
-
{entry === null ? null : (
|
|
131
|
+
{entry === null ? null : itemCount <= 1 ? (
|
|
114
132
|
<ConsentBody
|
|
115
|
-
entry={entry}
|
|
133
|
+
entry={entry.items[0]!}
|
|
116
134
|
remember={remember}
|
|
117
135
|
onRememberChange={setRemember}
|
|
118
136
|
formatBytes={formatBytes}
|
|
119
137
|
/>
|
|
138
|
+
) : (
|
|
139
|
+
<ConsentBatch
|
|
140
|
+
items={entry.items}
|
|
141
|
+
remember={remember}
|
|
142
|
+
onRememberChange={setRemember}
|
|
143
|
+
/>
|
|
120
144
|
)}
|
|
121
145
|
</AppDialog>
|
|
122
146
|
)
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
function ConsentBody(props: {
|
|
126
|
-
readonly entry:
|
|
150
|
+
readonly entry: PluginConsentItem
|
|
127
151
|
readonly remember: boolean
|
|
128
152
|
readonly onRememberChange: (value: boolean) => void
|
|
129
153
|
readonly formatBytes: (bytes: number) => string
|
|
@@ -132,17 +156,7 @@ function ConsentBody(props: {
|
|
|
132
156
|
const { t } = useTranslation("ui", { useSuspense: false })
|
|
133
157
|
return (
|
|
134
158
|
<div className="flex flex-col gap-4">
|
|
135
|
-
<
|
|
136
|
-
<span className="text-xs tracking-label text-muted-foreground uppercase">
|
|
137
|
-
{t("pluginDownload.urlLabel", undefined)}
|
|
138
|
-
</span>
|
|
139
|
-
<code
|
|
140
|
-
className="rounded-sm bg-muted px-2 py-1.5 text-xs text-secondary-foreground break-all select-all"
|
|
141
|
-
data-testid="plugin-download-url"
|
|
142
|
-
>
|
|
143
|
-
{entry.url}
|
|
144
|
-
</code>
|
|
145
|
-
</div>
|
|
159
|
+
<ConsentUrlField label={t("pluginDownload.urlLabel", undefined)} url={entry.url} />
|
|
146
160
|
<div className="flex flex-col gap-1.5">
|
|
147
161
|
<span className="text-xs tracking-label text-muted-foreground uppercase">
|
|
148
162
|
{t("pluginDownload.destLabel", undefined)}
|
|
@@ -165,19 +179,96 @@ function ConsentBody(props: {
|
|
|
165
179
|
{entry.reason}
|
|
166
180
|
</p>
|
|
167
181
|
) : null}
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
<RememberRow remember={remember} onRememberChange={onRememberChange} />
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Batch layout: one row per download, URL verbatim + dest + reason. */
|
|
188
|
+
function ConsentBatch(props: {
|
|
189
|
+
readonly items: readonly PluginConsentItem[]
|
|
190
|
+
readonly remember: boolean
|
|
191
|
+
readonly onRememberChange: (value: boolean) => void
|
|
192
|
+
}) {
|
|
193
|
+
const { items, remember, onRememberChange } = props
|
|
194
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
195
|
+
return (
|
|
196
|
+
<div className="flex flex-col gap-4">
|
|
197
|
+
<ul className="flex max-h-52 flex-col gap-3 overflow-y-auto pr-1">
|
|
198
|
+
{items.map((item, index) => (
|
|
199
|
+
<li
|
|
200
|
+
key={`${item.url}#${item.dest}`}
|
|
201
|
+
className="flex flex-col gap-1 rounded-md border border-border px-2.5 py-2"
|
|
202
|
+
data-testid={`plugin-download-item-${index}`}
|
|
203
|
+
>
|
|
204
|
+
<ConsentUrlField
|
|
205
|
+
label={t("pluginDownload.itemLabel", {
|
|
206
|
+
index: index + 1,
|
|
207
|
+
})}
|
|
208
|
+
url={item.url}
|
|
209
|
+
testId={`plugin-download-url-${index}`}
|
|
210
|
+
/>
|
|
211
|
+
<code
|
|
212
|
+
className="text-xs text-secondary-foreground break-all select-all"
|
|
213
|
+
data-testid={`plugin-download-dest-${index}`}
|
|
214
|
+
>
|
|
215
|
+
{item.dest}
|
|
216
|
+
</code>
|
|
217
|
+
{item.reason !== undefined && item.reason.length > 0 ? (
|
|
218
|
+
<p className="text-xs text-secondary-foreground italic">
|
|
219
|
+
{item.reason}
|
|
220
|
+
</p>
|
|
221
|
+
) : null}
|
|
222
|
+
</li>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
<p className="text-xs text-muted-foreground">
|
|
226
|
+
{t("pluginDownload.batchNote", { count: items.length })}
|
|
227
|
+
</p>
|
|
228
|
+
<RememberRow remember={remember} onRememberChange={onRememberChange} />
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ConsentUrlField(props: {
|
|
234
|
+
readonly label: string
|
|
235
|
+
readonly url: string
|
|
236
|
+
readonly testId?: string
|
|
237
|
+
}) {
|
|
238
|
+
return (
|
|
239
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
240
|
+
<span className="text-xs tracking-label text-muted-foreground uppercase">
|
|
241
|
+
{props.label}
|
|
242
|
+
</span>
|
|
243
|
+
<code
|
|
244
|
+
className="rounded-sm bg-muted px-2 py-1.5 text-xs text-secondary-foreground break-all select-all"
|
|
245
|
+
data-testid={props.testId ?? "plugin-download-url"}
|
|
246
|
+
>
|
|
247
|
+
{props.url}
|
|
248
|
+
</code>
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function RememberRow(props: {
|
|
254
|
+
readonly remember: boolean
|
|
255
|
+
readonly onRememberChange: (value: boolean) => void
|
|
256
|
+
}) {
|
|
257
|
+
const { remember, onRememberChange } = props
|
|
258
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
259
|
+
return (
|
|
260
|
+
<div className="flex items-center gap-2 border-t pt-3">
|
|
261
|
+
<Checkbox
|
|
262
|
+
checked={remember}
|
|
263
|
+
onCheckedChange={(checked) => onRememberChange(checked === true)}
|
|
264
|
+
id="plugin-download-remember"
|
|
265
|
+
/>
|
|
266
|
+
<label
|
|
267
|
+
htmlFor="plugin-download-remember"
|
|
268
|
+
className="text-xs text-secondary-foreground select-none"
|
|
269
|
+
>
|
|
270
|
+
{t("pluginDownload.remember", undefined)}
|
|
271
|
+
</label>
|
|
181
272
|
</div>
|
|
182
273
|
)
|
|
183
274
|
}
|
package/src/icons/registry.ts
CHANGED
|
@@ -140,6 +140,7 @@ import { ServerIcon as ServerBoldWeight } from "@solar-icons/react/bold/server"
|
|
|
140
140
|
import { SettingsIcon as SettingsBoldWeight } from "@solar-icons/react/bold/settings"
|
|
141
141
|
import { ShareIcon as ShareBoldWeight } from "@solar-icons/react/bold/share"
|
|
142
142
|
import { ShieldCheckIcon as ShieldCheckBoldWeight } from "@solar-icons/react/bold/shield-check"
|
|
143
|
+
import { Shop2Icon as Shop2BoldWeight } from "@solar-icons/react/bold/shop-2"
|
|
143
144
|
import { SidebarMinimalisticIcon as SidebarMinimalisticBoldWeight } from "@solar-icons/react/bold/sidebar-minimalistic"
|
|
144
145
|
import { SliderHorizontalIcon as SliderHorizontalBoldWeight } from "@solar-icons/react/bold/slider-horizontal"
|
|
145
146
|
import { SmartphoneIcon as SmartphoneBoldWeight } from "@solar-icons/react/bold/smartphone"
|
|
@@ -281,6 +282,7 @@ import { ServerIcon as ServerBoldDuotone } from "@solar-icons/react/bold-duotone
|
|
|
281
282
|
import { SettingsIcon as SettingsBoldDuotone } from "@solar-icons/react/bold-duotone/settings"
|
|
282
283
|
import { ShareIcon as ShareBoldDuotone } from "@solar-icons/react/bold-duotone/share"
|
|
283
284
|
import { ShieldCheckIcon as ShieldCheckBoldDuotone } from "@solar-icons/react/bold-duotone/shield-check"
|
|
285
|
+
import { Shop2Icon as Shop2BoldDuotone } from "@solar-icons/react/bold-duotone/shop-2"
|
|
284
286
|
import { SidebarMinimalisticIcon as SidebarMinimalisticBoldDuotone } from "@solar-icons/react/bold-duotone/sidebar-minimalistic"
|
|
285
287
|
import { SliderHorizontalIcon as SliderHorizontalBoldDuotone } from "@solar-icons/react/bold-duotone/slider-horizontal"
|
|
286
288
|
import { SmartphoneIcon as SmartphoneBoldDuotone } from "@solar-icons/react/bold-duotone/smartphone"
|
|
@@ -421,6 +423,7 @@ import { ServerIcon as ServerLinear } from "@solar-icons/react/linear/server"
|
|
|
421
423
|
import { SettingsIcon as SettingsLinear } from "@solar-icons/react/linear/settings"
|
|
422
424
|
import { ShareIcon as ShareLinear } from "@solar-icons/react/linear/share"
|
|
423
425
|
import { ShieldCheckIcon as ShieldCheckLinear } from "@solar-icons/react/linear/shield-check"
|
|
426
|
+
import { Shop2Icon as Shop2Linear } from "@solar-icons/react/linear/shop-2"
|
|
424
427
|
import { SidebarMinimalisticIcon as SidebarMinimalisticLinear } from "@solar-icons/react/linear/sidebar-minimalistic"
|
|
425
428
|
import { SliderHorizontalIcon as SliderHorizontalLinear } from "@solar-icons/react/linear/slider-horizontal"
|
|
426
429
|
import { SmartphoneIcon as SmartphoneLinear } from "@solar-icons/react/linear/smartphone"
|
|
@@ -1001,6 +1004,11 @@ export const ShieldCheck = createIcon({
|
|
|
1001
1004
|
boldDuotone: ShieldCheckBoldDuotone,
|
|
1002
1005
|
linear: ShieldCheckLinear,
|
|
1003
1006
|
})
|
|
1007
|
+
export const Shop2 = createIcon({
|
|
1008
|
+
bold: Shop2BoldWeight,
|
|
1009
|
+
boldDuotone: Shop2BoldDuotone,
|
|
1010
|
+
linear: Shop2Linear,
|
|
1011
|
+
})
|
|
1004
1012
|
export const SidebarMinimalistic = createIcon({
|
|
1005
1013
|
bold: SidebarMinimalisticBoldWeight,
|
|
1006
1014
|
boldDuotone: SidebarMinimalisticBoldDuotone,
|