@hoardodile/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +70 -0
- package/dist/components/alert.d.ts +13 -0
- package/dist/components/alert.js +97 -0
- package/dist/components/alert.js.map +1 -0
- package/dist/components/app-dialog.d.ts +62 -0
- package/dist/components/app-dialog.js +656 -0
- package/dist/components/app-dialog.js.map +1 -0
- package/dist/components/badge.d.ts +11 -0
- package/dist/components/badge.js +70 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +617 -0
- package/dist/components/breadcrumb.js.map +1 -0
- package/dist/components/button.d.ts +17 -0
- package/dist/components/button.js +78 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/caption-bar.d.ts +44 -0
- package/dist/components/caption-bar.js +827 -0
- package/dist/components/caption-bar.js.map +1 -0
- package/dist/components/chart-card.d.ts +15 -0
- package/dist/components/chart-card.js +27 -0
- package/dist/components/chart-card.js.map +1 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.js +76 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip-button.d.ts +28 -0
- package/dist/components/chip-button.js +87 -0
- package/dist/components/chip-button.js.map +1 -0
- package/dist/components/close-confirm-dialog.d.ts +19 -0
- package/dist/components/close-confirm-dialog.js +743 -0
- package/dist/components/close-confirm-dialog.js.map +1 -0
- package/dist/components/color-picker.d.ts +32 -0
- package/dist/components/color-picker.js +667 -0
- package/dist/components/color-picker.js.map +1 -0
- package/dist/components/combobox.d.ts +32 -0
- package/dist/components/combobox.js +1211 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/confirm-by-typing-dialog.d.ts +48 -0
- package/dist/components/confirm-by-typing-dialog.js +759 -0
- package/dist/components/confirm-by-typing-dialog.js.map +1 -0
- package/dist/components/confirm-dialog.d.ts +34 -0
- package/dist/components/confirm-dialog.js +710 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/count-badge.d.ts +13 -0
- package/dist/components/count-badge.js +39 -0
- package/dist/components/count-badge.js.map +1 -0
- package/dist/components/dialog.d.ts +40 -0
- package/dist/components/dialog.js +571 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +45 -0
- package/dist/components/dropdown-menu.js +1003 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/dropdown-select.d.ts +29 -0
- package/dist/components/dropdown-select.js +1036 -0
- package/dist/components/dropdown-select.js.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.js +119 -0
- package/dist/components/empty.js.map +1 -0
- package/dist/components/filter-rail.d.ts +48 -0
- package/dist/components/filter-rail.js +231 -0
- package/dist/components/filter-rail.js.map +1 -0
- package/dist/components/fixed-action-bar.d.ts +24 -0
- package/dist/components/fixed-action-bar.js +48 -0
- package/dist/components/fixed-action-bar.js.map +1 -0
- package/dist/components/font-picker.d.ts +45 -0
- package/dist/components/font-picker.js +759 -0
- package/dist/components/font-picker.js.map +1 -0
- package/dist/components/form.d.ts +17 -0
- package/dist/components/form.js +150 -0
- package/dist/components/form.js.map +1 -0
- package/dist/components/group-label.d.ts +12 -0
- package/dist/components/group-label.js +10 -0
- package/dist/components/group-label.js.map +1 -0
- package/dist/components/icon-tile.d.ts +16 -0
- package/dist/components/icon-tile.js +67 -0
- package/dist/components/icon-tile.js.map +1 -0
- package/dist/components/icon-toggle.d.ts +24 -0
- package/dist/components/icon-toggle.js +81 -0
- package/dist/components/icon-toggle.js.map +1 -0
- package/dist/components/icon.d.ts +2 -0
- package/dist/components/icon.js +49 -0
- package/dist/components/icon.js.map +1 -0
- package/dist/components/image-crop-panel.d.ts +80 -0
- package/dist/components/image-crop-panel.js +1104 -0
- package/dist/components/image-crop-panel.js.map +1 -0
- package/dist/components/image-cropper.d.ts +63 -0
- package/dist/components/image-cropper.js +166 -0
- package/dist/components/image-cropper.js.map +1 -0
- package/dist/components/input-group.d.ts +22 -0
- package/dist/components/input-group.js +270 -0
- package/dist/components/input-group.js.map +1 -0
- package/dist/components/input.d.ts +16 -0
- package/dist/components/input.js +60 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +40 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/list-empty-row.d.ts +16 -0
- package/dist/components/list-empty-row.js +40 -0
- package/dist/components/list-empty-row.js.map +1 -0
- package/dist/components/management-empty.d.ts +14 -0
- package/dist/components/management-empty.js +40 -0
- package/dist/components/management-empty.js.map +1 -0
- package/dist/components/management-skeleton.d.ts +11 -0
- package/dist/components/management-skeleton.js +72 -0
- package/dist/components/management-skeleton.js.map +1 -0
- package/dist/components/meta-chip.d.ts +17 -0
- package/dist/components/meta-chip.js +49 -0
- package/dist/components/meta-chip.js.map +1 -0
- package/dist/components/mobile-drawer.d.ts +29 -0
- package/dist/components/mobile-drawer.js +331 -0
- package/dist/components/mobile-drawer.js.map +1 -0
- package/dist/components/page-header.d.ts +20 -0
- package/dist/components/page-header.js +44 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/page-scaffold.d.ts +34 -0
- package/dist/components/page-scaffold.js +157 -0
- package/dist/components/page-scaffold.js.map +1 -0
- package/dist/components/pagination-bar.d.ts +22 -0
- package/dist/components/pagination-bar.js +703 -0
- package/dist/components/pagination-bar.js.map +1 -0
- package/dist/components/pagination.d.ts +22 -0
- package/dist/components/pagination.js +680 -0
- package/dist/components/pagination.js.map +1 -0
- package/dist/components/panel-toolbar.d.ts +37 -0
- package/dist/components/panel-toolbar.js +781 -0
- package/dist/components/panel-toolbar.js.map +1 -0
- package/dist/components/pill-tabs.d.ts +33 -0
- package/dist/components/pill-tabs.js +66 -0
- package/dist/components/pill-tabs.js.map +1 -0
- package/dist/components/plugin-download-consent.d.ts +52 -0
- package/dist/components/plugin-download-consent.js +780 -0
- package/dist/components/plugin-download-consent.js.map +1 -0
- package/dist/components/popover.d.ts +26 -0
- package/dist/components/popover.js +343 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/progress.d.ts +10 -0
- package/dist/components/progress.js +103 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/query-state-view.d.ts +36 -0
- package/dist/components/query-state-view.js +17 -0
- package/dist/components/query-state-view.js.map +1 -0
- package/dist/components/radio-group.d.ts +8 -0
- package/dist/components/radio-group.js +60 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +80 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/search-field.d.ts +49 -0
- package/dist/components/search-field.js +586 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/secondary-stat.d.ts +13 -0
- package/dist/components/secondary-stat.js +56 -0
- package/dist/components/secondary-stat.js.map +1 -0
- package/dist/components/section-header.d.ts +23 -0
- package/dist/components/section-header.js +76 -0
- package/dist/components/section-header.js.map +1 -0
- package/dist/components/section-label.d.ts +20 -0
- package/dist/components/section-label.js +46 -0
- package/dist/components/section-label.js.map +1 -0
- package/dist/components/section-tabs.d.ts +47 -0
- package/dist/components/section-tabs.js +145 -0
- package/dist/components/section-tabs.js.map +1 -0
- package/dist/components/separator.d.ts +14 -0
- package/dist/components/separator.js +49 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +22 -0
- package/dist/components/sheet.js +469 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/sidebar.d.ts +69 -0
- package/dist/components/sidebar.js +1645 -0
- package/dist/components/sidebar.js.map +1 -0
- package/dist/components/skeleton.d.ts +8 -0
- package/dist/components/skeleton.js +41 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/slider.d.ts +6 -0
- package/dist/components/slider.js +75 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sortable-chip-list.d.ts +26 -0
- package/dist/components/sortable-chip-list.js +59 -0
- package/dist/components/sortable-chip-list.js.map +1 -0
- package/dist/components/spinner.d.ts +5 -0
- package/dist/components/spinner.js +65 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/surface.d.ts +13 -0
- package/dist/components/surface.js +51 -0
- package/dist/components/surface.js.map +1 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/switch.js +53 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +129 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/textarea.d.ts +15 -0
- package/dist/components/textarea.js +58 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/toast.d.ts +19 -0
- package/dist/components/toast.js +805 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle.d.ts +12 -0
- package/dist/components/toggle.js +63 -0
- package/dist/components/toggle.js.map +1 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +82 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/hooks/use-dialog-footer-actions.d.ts +18 -0
- package/dist/hooks/use-dialog-footer-actions.js +11 -0
- package/dist/hooks/use-dialog-footer-actions.js.map +1 -0
- package/dist/hooks/use-form-field.d.ts +33 -0
- package/dist/hooks/use-form-field.js +36 -0
- package/dist/hooks/use-form-field.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +34 -0
- package/dist/hooks/use-mobile.js +29 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-pinch-pan.d.ts +56 -0
- package/dist/hooks/use-pinch-pan.js +223 -0
- package/dist/hooks/use-pinch-pan.js.map +1 -0
- package/dist/hooks/useMobileBackToClose.d.ts +61 -0
- package/dist/hooks/useMobileBackToClose.js +234 -0
- package/dist/hooks/useMobileBackToClose.js.map +1 -0
- package/dist/icon-BOgL5bpL.d.ts +85 -0
- package/dist/icons/actions.d.ts +4 -0
- package/dist/icons/actions.js +520 -0
- package/dist/icons/actions.js.map +1 -0
- package/dist/icons/icon-style.d.ts +2 -0
- package/dist/icons/icon-style.js +51 -0
- package/dist/icons/icon-style.js.map +1 -0
- package/dist/icons/marks.d.ts +15 -0
- package/dist/icons/marks.js +48 -0
- package/dist/icons/marks.js.map +1 -0
- package/dist/icons/registry.d.ts +177 -0
- package/dist/icons/registry.js +1180 -0
- package/dist/icons/registry.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/merge-config.d.ts +15 -0
- package/dist/lib/merge-config.js +20 -0
- package/dist/lib/merge-config.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.js +26 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +26 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles/theme.css +480 -0
- package/dist/styles/theme.css.map +1 -0
- package/dist/styles/theme.d.ts +2 -0
- package/dist/viewport.d.ts +64 -0
- package/dist/viewport.js +13 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +122 -0
- package/src/components/alert.tsx +76 -0
- package/src/components/app-dialog.tsx +187 -0
- package/src/components/badge.tsx +52 -0
- package/src/components/breadcrumb.tsx +129 -0
- package/src/components/button.tsx +70 -0
- package/src/components/caption-bar.test.tsx +151 -0
- package/src/components/caption-bar.tsx +392 -0
- package/src/components/chart-card.tsx +31 -0
- package/src/components/checkbox.tsx +28 -0
- package/src/components/chip-button.tsx +69 -0
- package/src/components/close-confirm-dialog.tsx +99 -0
- package/src/components/color-picker.tsx +186 -0
- package/src/components/combobox.tsx +335 -0
- package/src/components/confirm-by-typing-dialog.tsx +130 -0
- package/src/components/confirm-dialog.test.tsx +39 -0
- package/src/components/confirm-dialog.tsx +98 -0
- package/src/components/count-badge.tsx +23 -0
- package/src/components/dialog.tsx +315 -0
- package/src/components/dropdown-menu.tsx +311 -0
- package/src/components/dropdown-select.tsx +211 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/filter-rail.tsx +139 -0
- package/src/components/fixed-action-bar.tsx +46 -0
- package/src/components/font-picker.tsx +257 -0
- package/src/components/form.tsx +127 -0
- package/src/components/group-label.tsx +13 -0
- package/src/components/icon-tile.tsx +35 -0
- package/src/components/icon-toggle.tsx +57 -0
- package/src/components/icon.tsx +66 -0
- package/src/components/image-crop-panel.test.tsx +167 -0
- package/src/components/image-crop-panel.tsx +465 -0
- package/src/components/image-cropper.test.tsx +144 -0
- package/src/components/image-cropper.tsx +265 -0
- package/src/components/input-group.tsx +156 -0
- package/src/components/input.tsx +45 -0
- package/src/components/label.tsx +20 -0
- package/src/components/list-empty-row.tsx +27 -0
- package/src/components/management-empty.tsx +25 -0
- package/src/components/management-skeleton.tsx +23 -0
- package/src/components/meta-chip.tsx +40 -0
- package/src/components/mobile-drawer.test.tsx +52 -0
- package/src/components/mobile-drawer.tsx +98 -0
- package/src/components/page-header.tsx +47 -0
- package/src/components/page-scaffold.test.tsx +24 -0
- package/src/components/page-scaffold.tsx +131 -0
- package/src/components/pagination-bar.test.tsx +87 -0
- package/src/components/pagination-bar.tsx +139 -0
- package/src/components/pagination.tsx +139 -0
- package/src/components/panel-toolbar.tsx +99 -0
- package/src/components/pill-tabs.test.tsx +84 -0
- package/src/components/pill-tabs.tsx +77 -0
- package/src/components/plugin-download-consent.test.tsx +90 -0
- package/src/components/plugin-download-consent.tsx +183 -0
- package/src/components/popover.tsx +133 -0
- package/src/components/progress.tsx +88 -0
- package/src/components/query-state-view.tsx +47 -0
- package/src/components/radio-group.tsx +36 -0
- package/src/components/scroll-area.tsx +52 -0
- package/src/components/search-field.test.tsx +86 -0
- package/src/components/search-field.tsx +119 -0
- package/src/components/secondary-stat.tsx +24 -0
- package/src/components/section-header.tsx +54 -0
- package/src/components/section-label.tsx +40 -0
- package/src/components/section-tabs.test.tsx +154 -0
- package/src/components/section-tabs.tsx +190 -0
- package/src/components/separator.tsx +38 -0
- package/src/components/sheet.tsx +170 -0
- package/src/components/sidebar.tsx +725 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/slider.tsx +52 -0
- package/src/components/sortable-chip-list.tsx +56 -0
- package/src/components/spinner.tsx +35 -0
- package/src/components/surface.tsx +37 -0
- package/src/components/switch.tsx +30 -0
- package/src/components/table.tsx +116 -0
- package/src/components/textarea.tsx +41 -0
- package/src/components/toast.tsx +239 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.tsx +64 -0
- package/src/hooks/use-dialog-footer-actions.ts +23 -0
- package/src/hooks/use-form-field.ts +62 -0
- package/src/hooks/use-mobile.ts +54 -0
- package/src/hooks/use-pinch-pan.test.tsx +42 -0
- package/src/hooks/use-pinch-pan.ts +340 -0
- package/src/hooks/useMobileBackToClose.test.tsx +140 -0
- package/src/hooks/useMobileBackToClose.ts +508 -0
- package/src/icons/actions.ts +10 -0
- package/src/icons/icon-style.test.tsx +69 -0
- package/src/icons/icon-style.ts +104 -0
- package/src/icons/marks.tsx +72 -0
- package/src/icons/registry.test.ts +65 -0
- package/src/icons/registry.ts +1163 -0
- package/src/index.ts +22 -0
- package/src/lib/merge-config.test.ts +109 -0
- package/src/lib/merge-config.ts +22 -0
- package/src/lib/pagination.test.ts +55 -0
- package/src/lib/pagination.ts +42 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/scan-contract.test.ts +74 -0
- package/src/styles/theme.css +798 -0
- package/src/test/i18n.tsx +22 -0
- package/src/test/setup.ts +104 -0
- package/src/viewport.ts +69 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
4
|
+
import { Button } from "@hoardodile/ui/components/button"
|
|
5
|
+
import { AltArrowLeft } from "@hoardodile/ui/icons/registry"
|
|
6
|
+
import { AltArrowRight } from "@hoardodile/ui/icons/registry"
|
|
7
|
+
import { MenuDots } from "@hoardodile/ui/icons/registry"
|
|
8
|
+
import { useTranslation } from "react-i18next"
|
|
9
|
+
|
|
10
|
+
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
|
11
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
12
|
+
return (
|
|
13
|
+
<nav
|
|
14
|
+
role="navigation"
|
|
15
|
+
aria-label={t("pagination.region")}
|
|
16
|
+
data-slot="pagination"
|
|
17
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PaginationContent({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"ul">) {
|
|
27
|
+
return (
|
|
28
|
+
<ul
|
|
29
|
+
data-slot="pagination-content"
|
|
30
|
+
className={cn("flex items-center gap-1", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
|
37
|
+
return <li data-slot="pagination-item" {...props} />
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PaginationLinkProps = {
|
|
41
|
+
isActive?: boolean
|
|
42
|
+
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
|
43
|
+
React.ComponentProps<"a">
|
|
44
|
+
|
|
45
|
+
function PaginationLink({
|
|
46
|
+
className,
|
|
47
|
+
isActive,
|
|
48
|
+
size = "icon",
|
|
49
|
+
...props
|
|
50
|
+
}: PaginationLinkProps) {
|
|
51
|
+
return (
|
|
52
|
+
<Button
|
|
53
|
+
variant={isActive ? "outline" : "ghost"}
|
|
54
|
+
size={size}
|
|
55
|
+
className={cn(className)}
|
|
56
|
+
nativeButton={false}
|
|
57
|
+
render={
|
|
58
|
+
<a
|
|
59
|
+
aria-current={isActive ? "page" : undefined}
|
|
60
|
+
data-slot="pagination-link"
|
|
61
|
+
data-active={isActive}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function PaginationPrevious({
|
|
70
|
+
className,
|
|
71
|
+
text,
|
|
72
|
+
...props
|
|
73
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
74
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
75
|
+
return (
|
|
76
|
+
<PaginationLink
|
|
77
|
+
aria-label={t("pagination.previous")}
|
|
78
|
+
size="default"
|
|
79
|
+
className={cn("pl-2!", className)}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<AltArrowLeft data-icon="inline-start" />
|
|
83
|
+
<span className="hidden sm:block">
|
|
84
|
+
{text ?? t("pagination.previous")}
|
|
85
|
+
</span>
|
|
86
|
+
</PaginationLink>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function PaginationNext({
|
|
91
|
+
className,
|
|
92
|
+
text,
|
|
93
|
+
...props
|
|
94
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
95
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
96
|
+
return (
|
|
97
|
+
<PaginationLink
|
|
98
|
+
aria-label={t("pagination.next")}
|
|
99
|
+
size="default"
|
|
100
|
+
className={cn("pr-2!", className)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
<span className="hidden sm:block">{text ?? t("pagination.next")}</span>
|
|
104
|
+
<AltArrowRight data-icon="inline-end" />
|
|
105
|
+
</PaginationLink>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function PaginationEllipsis({
|
|
110
|
+
className,
|
|
111
|
+
...props
|
|
112
|
+
}: React.ComponentProps<"span">) {
|
|
113
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
114
|
+
return (
|
|
115
|
+
<span
|
|
116
|
+
aria-hidden
|
|
117
|
+
data-slot="pagination-ellipsis"
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex size-9 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
>
|
|
124
|
+
<MenuDots
|
|
125
|
+
/>
|
|
126
|
+
<span className="sr-only">{t("pagination.morePages")}</span>
|
|
127
|
+
</span>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
Pagination,
|
|
133
|
+
PaginationContent,
|
|
134
|
+
PaginationEllipsis,
|
|
135
|
+
PaginationItem,
|
|
136
|
+
PaginationLink,
|
|
137
|
+
PaginationNext,
|
|
138
|
+
PaginationPrevious,
|
|
139
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Button } from "@hoardodile/ui/components/button"
|
|
2
|
+
import { Icon } from "@hoardodile/ui/components/icon"
|
|
3
|
+
import { Add } from "@hoardodile/ui/icons/actions"
|
|
4
|
+
import { HamburgerMenu } from "@hoardodile/ui/icons/registry"
|
|
5
|
+
import { useTranslation } from "react-i18next"
|
|
6
|
+
import { ChipButton } from "./chip-button"
|
|
7
|
+
import { SearchField } from "./search-field"
|
|
8
|
+
|
|
9
|
+
export type PanelToolbarProps = {
|
|
10
|
+
readonly placeholder: string
|
|
11
|
+
readonly query: string
|
|
12
|
+
readonly onQuery: (query: string) => void
|
|
13
|
+
readonly reorder: boolean
|
|
14
|
+
readonly onToggleReorder: () => void
|
|
15
|
+
readonly unusedCount: number
|
|
16
|
+
readonly unusedOnly: boolean
|
|
17
|
+
readonly onToggleUnused: () => void
|
|
18
|
+
/** Add button — opens the shared create dialog. */
|
|
19
|
+
readonly onAdd: () => void
|
|
20
|
+
/** Add button label — defaults to the shared "Add" copy. */
|
|
21
|
+
readonly addLabel?: string
|
|
22
|
+
/** Unused chip label — defaults to the shared "Unused" copy. */
|
|
23
|
+
readonly unusedLabel?: string
|
|
24
|
+
/** Reorder chip label — defaults to the shared "Reorder" copy. */
|
|
25
|
+
readonly reorderLabel?: string
|
|
26
|
+
/** Forwarded to the filter field's `maxLength` (e.g. the query limit). */
|
|
27
|
+
readonly maxLength?: number
|
|
28
|
+
readonly testIds?: {
|
|
29
|
+
readonly filter?: string
|
|
30
|
+
readonly unused?: string
|
|
31
|
+
readonly reorder?: string
|
|
32
|
+
readonly add?: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One toolbar per custom-page panel: filter left; triage, structure and
|
|
38
|
+
* add right. Unused and Reorder ride ChipButton — the Unused filter wears
|
|
39
|
+
* the dashed outline, Reorder quiet text, both latching the accent fill
|
|
40
|
+
* when on. The Unused chip only appears when there is something to clean.
|
|
41
|
+
*/
|
|
42
|
+
export function PanelToolbar(props: PanelToolbarProps) {
|
|
43
|
+
const {
|
|
44
|
+
placeholder,
|
|
45
|
+
query,
|
|
46
|
+
onQuery,
|
|
47
|
+
reorder,
|
|
48
|
+
onToggleReorder,
|
|
49
|
+
unusedCount,
|
|
50
|
+
unusedOnly,
|
|
51
|
+
onToggleUnused,
|
|
52
|
+
onAdd,
|
|
53
|
+
addLabel,
|
|
54
|
+
unusedLabel,
|
|
55
|
+
reorderLabel,
|
|
56
|
+
maxLength,
|
|
57
|
+
testIds,
|
|
58
|
+
} = props
|
|
59
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
60
|
+
const resolvedAddLabel = addLabel ?? t("panelToolbar.add")
|
|
61
|
+
const resolvedUnusedLabel = unusedLabel ?? t("panelToolbar.unused")
|
|
62
|
+
const resolvedReorderLabel = reorderLabel ?? t("panelToolbar.reorder")
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
65
|
+
<SearchField
|
|
66
|
+
value={query}
|
|
67
|
+
onCommit={onQuery}
|
|
68
|
+
placeholder={placeholder}
|
|
69
|
+
maxLength={maxLength}
|
|
70
|
+
className="w-60"
|
|
71
|
+
testId={testIds?.filter}
|
|
72
|
+
/>
|
|
73
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
74
|
+
{unusedCount > 0 ? (
|
|
75
|
+
<ChipButton
|
|
76
|
+
dashed
|
|
77
|
+
active={unusedOnly}
|
|
78
|
+
onClick={onToggleUnused}
|
|
79
|
+
testId={testIds?.unused}
|
|
80
|
+
>
|
|
81
|
+
{resolvedUnusedLabel} · {unusedCount}
|
|
82
|
+
</ChipButton>
|
|
83
|
+
) : null}
|
|
84
|
+
<ChipButton
|
|
85
|
+
icon={HamburgerMenu}
|
|
86
|
+
active={reorder}
|
|
87
|
+
onClick={onToggleReorder}
|
|
88
|
+
testId={testIds?.reorder}
|
|
89
|
+
>
|
|
90
|
+
{resolvedReorderLabel}
|
|
91
|
+
</ChipButton>
|
|
92
|
+
<Button variant="secondary" onClick={onAdd} data-testid={testIds?.add}>
|
|
93
|
+
<Icon icon={Add} />
|
|
94
|
+
{resolvedAddLabel}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react"
|
|
2
|
+
import { describe, expect, it, vi } from "vitest"
|
|
3
|
+
import { PillTabs, pillButtonClassName } from "./pill-tabs"
|
|
4
|
+
|
|
5
|
+
describe("pillButtonClassName", () => {
|
|
6
|
+
it("uses the chip-sized pill", () => {
|
|
7
|
+
expect(pillButtonClassName(false)).toContain("h-chip px-3")
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("lifts the active pill to the card fill", () => {
|
|
11
|
+
// tailwind-merge reorders classes — assert tokens, not a sequence.
|
|
12
|
+
const active = pillButtonClassName(true)
|
|
13
|
+
expect(active).toContain("bg-card")
|
|
14
|
+
expect(active).toContain("font-medium")
|
|
15
|
+
expect(active).toContain("text-foreground")
|
|
16
|
+
expect(pillButtonClassName(false)).not.toContain("bg-card")
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("PillTabs", () => {
|
|
21
|
+
const items = [
|
|
22
|
+
{ value: "a", label: "Alpha" },
|
|
23
|
+
{ value: "b", label: "Beta" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
it("renders the track with one button per item and the active state", () => {
|
|
27
|
+
render(<PillTabs value="b" items={items} />)
|
|
28
|
+
expect(screen.getByRole("button", { name: "Alpha" })).toBeInTheDocument()
|
|
29
|
+
const beta = screen.getByRole("button", { name: "Beta" })
|
|
30
|
+
expect(beta.className).toContain("bg-card")
|
|
31
|
+
expect(
|
|
32
|
+
screen.getByRole("button", { name: "Alpha" }).className,
|
|
33
|
+
).not.toContain("bg-card")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("reports the picked value through onChange", () => {
|
|
37
|
+
const onChange = vi.fn()
|
|
38
|
+
render(<PillTabs value="a" items={items} onChange={onChange} />)
|
|
39
|
+
fireEvent.click(screen.getByRole("button", { name: "Beta" }))
|
|
40
|
+
expect(onChange).toHaveBeenCalledWith("b")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("lets a render slot replace the button (Link-style items)", () => {
|
|
44
|
+
render(
|
|
45
|
+
<PillTabs
|
|
46
|
+
value="a"
|
|
47
|
+
items={[
|
|
48
|
+
{
|
|
49
|
+
value: "a",
|
|
50
|
+
label: "Alpha",
|
|
51
|
+
render: (active, className) => (
|
|
52
|
+
<a href="/stats" className={className} data-active={active}>
|
|
53
|
+
Alpha link
|
|
54
|
+
</a>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
/>,
|
|
59
|
+
)
|
|
60
|
+
const link = screen.getByRole("link", { name: "Alpha link" })
|
|
61
|
+
expect(link.getAttribute("data-active")).toBe("true")
|
|
62
|
+
expect(link.className).toContain("bg-card")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("announces the active pill via aria-pressed when requested", () => {
|
|
66
|
+
render(
|
|
67
|
+
<PillTabs
|
|
68
|
+
value="a"
|
|
69
|
+
items={items.map((item) => ({
|
|
70
|
+
...item,
|
|
71
|
+
ariaPressed: item.value === "a",
|
|
72
|
+
}))}
|
|
73
|
+
/>,
|
|
74
|
+
)
|
|
75
|
+
expect(screen.getByRole("button", { name: "Alpha" })).toHaveAttribute(
|
|
76
|
+
"aria-pressed",
|
|
77
|
+
"true",
|
|
78
|
+
)
|
|
79
|
+
expect(screen.getByRole("button", { name: "Beta" })).toHaveAttribute(
|
|
80
|
+
"aria-pressed",
|
|
81
|
+
"false",
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
export type PillTabItem<T extends string> = {
|
|
5
|
+
readonly value: T
|
|
6
|
+
readonly label: ReactNode
|
|
7
|
+
readonly testId?: string
|
|
8
|
+
/** Toggle-state announcement for the settings variants; plain buttons
|
|
9
|
+
render without it. */
|
|
10
|
+
readonly ariaPressed?: boolean
|
|
11
|
+
/** Screen-reader name when the label alone is not descriptive. */
|
|
12
|
+
readonly ariaLabel?: string
|
|
13
|
+
/** Layout overrides for the pill (e.g. the relationship-kind grid). */
|
|
14
|
+
readonly className?: string
|
|
15
|
+
/** Replaces the button element entirely (e.g. a Link) — receives the
|
|
16
|
+
active state and the pill classes to apply. */
|
|
17
|
+
readonly render?: (active: boolean, className: string) => ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PillTabsProps<T extends string> = {
|
|
21
|
+
readonly value: T
|
|
22
|
+
readonly items: readonly PillTabItem<T>[]
|
|
23
|
+
readonly onChange?: (value: T) => void
|
|
24
|
+
readonly className?: string
|
|
25
|
+
/** Disables every pill (e.g. while a sibling control takes over). */
|
|
26
|
+
readonly disabled?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pillButtonClassName(active: boolean): string {
|
|
30
|
+
return cn(
|
|
31
|
+
"inline-flex cursor-pointer font-medium appearance-none items-center rounded-md border-0 text-xs h-chip px-3 disabled:pointer-events-none disabled:opacity-50",
|
|
32
|
+
active
|
|
33
|
+
? "bg-card text-foreground"
|
|
34
|
+
: "bg-transparent text-muted-foreground hover:text-secondary-foreground",
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Segmented pill tab group: a muted track whose active pill lifts to
|
|
39
|
+
the card fill. Shared by the stats Time/Views toggle, the settings
|
|
40
|
+
segmented controls, and the filter-bar toggles. */
|
|
41
|
+
export function PillTabs<T extends string>(props: PillTabsProps<T>) {
|
|
42
|
+
const { value, items, onChange, className, disabled } = props
|
|
43
|
+
return (
|
|
44
|
+
<span
|
|
45
|
+
className={cn(
|
|
46
|
+
"inline-flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{items.map((item) => {
|
|
51
|
+
const active = item.value === value
|
|
52
|
+
const buttonClassName = pillButtonClassName(active)
|
|
53
|
+
if (item.render !== undefined) {
|
|
54
|
+
return (
|
|
55
|
+
<span key={item.value} data-testid={item.testId}>
|
|
56
|
+
{item.render(active, buttonClassName)}
|
|
57
|
+
</span>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={item.value}
|
|
63
|
+
type="button"
|
|
64
|
+
aria-pressed={item.ariaPressed}
|
|
65
|
+
aria-label={item.ariaLabel}
|
|
66
|
+
data-testid={item.testId}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
onClick={() => onChange?.(item.value)}
|
|
69
|
+
className={cn(buttonClassName, item.className)}
|
|
70
|
+
>
|
|
71
|
+
{item.label}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
})}
|
|
75
|
+
</span>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cleanup, screen } from "@testing-library/react"
|
|
2
|
+
import userEvent from "@testing-library/user-event"
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
4
|
+
import { renderWithI18n, testI18n } from "../test/i18n"
|
|
5
|
+
import {
|
|
6
|
+
PluginDownloadConsentDialog,
|
|
7
|
+
type PluginConsentTicket,
|
|
8
|
+
} from "./plugin-download-consent"
|
|
9
|
+
|
|
10
|
+
const TICKET: PluginConsentTicket = {
|
|
11
|
+
ticketId: "t-1",
|
|
12
|
+
pluginId: "p-1",
|
|
13
|
+
pluginName: "Gallery",
|
|
14
|
+
url: "https://example.com/runtime/live2d.min.js?x=1",
|
|
15
|
+
dest: "runtime/live2d.min.js",
|
|
16
|
+
sizeBytes: 2048,
|
|
17
|
+
reason: "live2d runtime",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
cleanup()
|
|
22
|
+
void testI18n.changeLanguage("en")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function renderDialog(
|
|
26
|
+
overrides: Partial<Parameters<typeof PluginDownloadConsentDialog>[0]> = {},
|
|
27
|
+
) {
|
|
28
|
+
const onDeny = vi.fn()
|
|
29
|
+
const onAllow = vi.fn()
|
|
30
|
+
renderWithI18n(
|
|
31
|
+
<PluginDownloadConsentDialog
|
|
32
|
+
entry={TICKET}
|
|
33
|
+
onDeny={onDeny}
|
|
34
|
+
onAllow={onAllow}
|
|
35
|
+
formatBytes={(n) => `${n} B`}
|
|
36
|
+
{...overrides}
|
|
37
|
+
/>,
|
|
38
|
+
)
|
|
39
|
+
return { onDeny, onAllow }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("PluginDownloadConsentDialog", () => {
|
|
43
|
+
it("shows the URL verbatim, destination and plugin reason", async () => {
|
|
44
|
+
renderDialog()
|
|
45
|
+
// Base UI mounts the dialog portal on effects; wait for the first
|
|
46
|
+
// assertion instead of querying the pre-mount tree.
|
|
47
|
+
expect(await screen.findByTestId("plugin-download-url")).toHaveTextContent(
|
|
48
|
+
"https://example.com/runtime/live2d.min.js?x=1",
|
|
49
|
+
)
|
|
50
|
+
expect(screen.getByTestId("plugin-download-dest")).toHaveTextContent(
|
|
51
|
+
"runtime/live2d.min.js",
|
|
52
|
+
)
|
|
53
|
+
expect(screen.getByText("live2d runtime")).toBeInTheDocument()
|
|
54
|
+
expect(screen.getByText(/2048 B/)).toBeInTheDocument()
|
|
55
|
+
expect(screen.getByTestId("plugin-download-deny")).toHaveTextContent("Deny")
|
|
56
|
+
expect(screen.getByTestId("plugin-download-allow")).toHaveTextContent("Allow")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("Deny reports the ticket id", async () => {
|
|
60
|
+
const { onDeny } = renderDialog()
|
|
61
|
+
await userEvent.click(screen.getByTestId("plugin-download-deny"))
|
|
62
|
+
expect(onDeny).toHaveBeenCalledWith("t-1")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("Allow reports the ticket id with the remember flag", async () => {
|
|
66
|
+
const { onAllow } = renderDialog()
|
|
67
|
+
await userEvent.click(screen.getByTestId("plugin-download-allow"))
|
|
68
|
+
expect(onAllow).toHaveBeenCalledWith("t-1", false)
|
|
69
|
+
await userEvent.click(screen.getByRole("checkbox"))
|
|
70
|
+
await userEvent.click(screen.getByTestId("plugin-download-allow"))
|
|
71
|
+
expect(onAllow).toHaveBeenLastCalledWith("t-1", true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("renders nothing actionable when no ticket is queued", () => {
|
|
75
|
+
renderDialog({ entry: null })
|
|
76
|
+
expect(
|
|
77
|
+
screen.queryByTestId("plugin-download-allow"),
|
|
78
|
+
).not.toBeInTheDocument()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("follows the host language via the ui catalog", async () => {
|
|
82
|
+
await testI18n.changeLanguage("zh")
|
|
83
|
+
renderDialog()
|
|
84
|
+
// Base UI mounts the dialog portal on effects; wait for the first
|
|
85
|
+
// assertion instead of querying the pre-mount tree.
|
|
86
|
+
expect(
|
|
87
|
+
await screen.findByRole("button", { name: "允许" }),
|
|
88
|
+
).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Button } from "./button.tsx"
|
|
2
|
+
import { Checkbox } from "./checkbox.tsx"
|
|
3
|
+
import { AppDialog } from "./app-dialog.tsx"
|
|
4
|
+
import { useEffect, useState } from "react"
|
|
5
|
+
import { useTranslation } from "react-i18next"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shared plugin asset-download consent dialog — host chrome used by
|
|
9
|
+
* both host surfaces (the app and the workbench) so the user sees one
|
|
10
|
+
* dialog with one contract wherever a plugin asks to download.
|
|
11
|
+
*
|
|
12
|
+
* Presentational by design: the queue lives in the host's consent store
|
|
13
|
+
* (`@hoardodile/host-web`), this component renders exactly one ticket
|
|
14
|
+
* and reports the decision through callbacks. Copy comes from the shared
|
|
15
|
+
* `ui` catalog namespace (`pluginDownload.*`), so every React surface
|
|
16
|
+
* renders the same localized strings; nothing here imports anything
|
|
17
|
+
* outside `@hoardodile/ui` + `@hoardodile/i18n`, keeping the SDK closure
|
|
18
|
+
* intact.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A queued consent ticket, structurally identical to the server's
|
|
23
|
+
* `pluginDownloadRequested` SSE event minus its `type` discriminator
|
|
24
|
+
* (and to `@hoardodile/host-web`'s consent-store entry). Declared here
|
|
25
|
+
* instead of imported so this package stays closure-clean.
|
|
26
|
+
*/
|
|
27
|
+
export type PluginConsentTicket = {
|
|
28
|
+
readonly ticketId: string
|
|
29
|
+
readonly pluginId: string
|
|
30
|
+
readonly pluginName: string
|
|
31
|
+
readonly url: string
|
|
32
|
+
readonly dest: string
|
|
33
|
+
readonly sizeBytes?: number
|
|
34
|
+
readonly reason?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PluginDownloadConsentDialogProps = {
|
|
38
|
+
/** Ticket to show; `null` hides the dialog (one at a time). */
|
|
39
|
+
readonly entry: PluginConsentTicket | null
|
|
40
|
+
/** Decision callbacks — the host wires them to its own decide path. */
|
|
41
|
+
readonly onDeny: (ticketId: string) => void
|
|
42
|
+
readonly onAllow: (ticketId: string, remember: boolean) => void
|
|
43
|
+
/** Byte formatter; defaults to `"<n> B"`. */
|
|
44
|
+
readonly formatBytes?: (bytes: number) => string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renders the shared consent question: URL verbatim (selectable
|
|
49
|
+
* monospace, never a link), the vault-relative destination and the
|
|
50
|
+
* plugin's stated reason, with Allow / Deny and a session-remember
|
|
51
|
+
* checkbox.
|
|
52
|
+
*
|
|
53
|
+
* Design contract (hd-plugin-design): card + hairline + `--radius-2xl`
|
|
54
|
+
* + `--shadow-dialog`, footer parted by an inset hairline, focus on the
|
|
55
|
+
* dialog container; the danger stays in copy and actions, never in color.
|
|
56
|
+
*/
|
|
57
|
+
export function PluginDownloadConsentDialog(
|
|
58
|
+
props: PluginDownloadConsentDialogProps,
|
|
59
|
+
) {
|
|
60
|
+
const { entry, onDeny, onAllow } = props
|
|
61
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
62
|
+
const formatBytes = props.formatBytes ?? ((bytes: number) => `${bytes} B`)
|
|
63
|
+
const activeId = entry === null ? null : entry.ticketId
|
|
64
|
+
const [remember, setRemember] = useState(false)
|
|
65
|
+
|
|
66
|
+
// A fresh ticket starts with "remember" unchecked — the choice must
|
|
67
|
+
// never leak from one download to the next.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setRemember(false)
|
|
70
|
+
}, [activeId])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<AppDialog
|
|
74
|
+
open={entry !== null}
|
|
75
|
+
onOpenChange={(open) => {
|
|
76
|
+
if (!open && activeId !== null) onDeny(activeId)
|
|
77
|
+
}}
|
|
78
|
+
title={t("pluginDownload.title", undefined)}
|
|
79
|
+
eyebrow={t("pluginDownload.eyebrow", undefined)}
|
|
80
|
+
description={
|
|
81
|
+
entry === null
|
|
82
|
+
? undefined
|
|
83
|
+
: t("pluginDownload.description", {
|
|
84
|
+
pluginName: entry.pluginName,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
size="md"
|
|
88
|
+
contentTestId="plugin-download-consent"
|
|
89
|
+
footer={
|
|
90
|
+
<div className="flex w-full items-center justify-between">
|
|
91
|
+
<Button
|
|
92
|
+
variant="outline"
|
|
93
|
+
size="sm"
|
|
94
|
+
disabled={activeId === null}
|
|
95
|
+
onClick={() => activeId !== null && onDeny(activeId)}
|
|
96
|
+
data-testid="plugin-download-deny"
|
|
97
|
+
>
|
|
98
|
+
{t("pluginDownload.deny", undefined)}
|
|
99
|
+
</Button>
|
|
100
|
+
<Button
|
|
101
|
+
size="sm"
|
|
102
|
+
disabled={activeId === null}
|
|
103
|
+
onClick={() =>
|
|
104
|
+
activeId !== null && onAllow(activeId, remember)
|
|
105
|
+
}
|
|
106
|
+
data-testid="plugin-download-allow"
|
|
107
|
+
>
|
|
108
|
+
{t("pluginDownload.allow", undefined)}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
{entry === null ? null : (
|
|
114
|
+
<ConsentBody
|
|
115
|
+
entry={entry}
|
|
116
|
+
remember={remember}
|
|
117
|
+
onRememberChange={setRemember}
|
|
118
|
+
formatBytes={formatBytes}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
</AppDialog>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ConsentBody(props: {
|
|
126
|
+
readonly entry: PluginConsentTicket
|
|
127
|
+
readonly remember: boolean
|
|
128
|
+
readonly onRememberChange: (value: boolean) => void
|
|
129
|
+
readonly formatBytes: (bytes: number) => string
|
|
130
|
+
}) {
|
|
131
|
+
const { entry, remember, onRememberChange, formatBytes } = props
|
|
132
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex flex-col gap-4">
|
|
135
|
+
<div className="flex flex-col gap-1.5">
|
|
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>
|
|
146
|
+
<div className="flex flex-col gap-1.5">
|
|
147
|
+
<span className="text-xs tracking-label text-muted-foreground uppercase">
|
|
148
|
+
{t("pluginDownload.destLabel", undefined)}
|
|
149
|
+
</span>
|
|
150
|
+
<code
|
|
151
|
+
className="text-xs text-secondary-foreground break-all select-all"
|
|
152
|
+
data-testid="plugin-download-dest"
|
|
153
|
+
>
|
|
154
|
+
{entry.dest}
|
|
155
|
+
</code>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="text-xs text-muted-foreground">
|
|
158
|
+
{t("pluginDownload.sizeLabel", undefined)}:{" "}
|
|
159
|
+
{entry.sizeBytes === undefined
|
|
160
|
+
? t("pluginDownload.unknownSize", undefined)
|
|
161
|
+
: formatBytes(entry.sizeBytes)}
|
|
162
|
+
</div>
|
|
163
|
+
{entry.reason !== undefined && entry.reason.length > 0 ? (
|
|
164
|
+
<p className="text-xs text-secondary-foreground italic">
|
|
165
|
+
{entry.reason}
|
|
166
|
+
</p>
|
|
167
|
+
) : null}
|
|
168
|
+
<div className="flex items-center gap-2 border-t pt-3">
|
|
169
|
+
<Checkbox
|
|
170
|
+
checked={remember}
|
|
171
|
+
onCheckedChange={(checked) => onRememberChange(checked === true)}
|
|
172
|
+
id="plugin-download-remember"
|
|
173
|
+
/>
|
|
174
|
+
<label
|
|
175
|
+
htmlFor="plugin-download-remember"
|
|
176
|
+
className="text-xs text-secondary-foreground select-none"
|
|
177
|
+
>
|
|
178
|
+
{t("pluginDownload.remember", undefined)}
|
|
179
|
+
</label>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|