@inshorts/glint-ui 0.5.4 → 0.6.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/README.md +3 -1
- package/dist/alert.cjs +96 -0
- package/dist/alert.cjs.map +1 -0
- package/dist/alert.d.cts +29 -0
- package/dist/alert.d.ts +29 -0
- package/dist/alert.js +10 -0
- package/dist/alert.js.map +1 -0
- package/dist/autocomplete.cjs +578 -0
- package/dist/autocomplete.cjs.map +1 -0
- package/dist/autocomplete.d.cts +56 -0
- package/dist/autocomplete.d.ts +56 -0
- package/dist/autocomplete.js +11 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/badge.cjs +89 -0
- package/dist/badge.cjs.map +1 -0
- package/dist/badge.d.cts +25 -0
- package/dist/badge.d.ts +25 -0
- package/dist/badge.js +10 -0
- package/dist/badge.js.map +1 -0
- package/dist/breadcrumb.cjs +88 -0
- package/dist/breadcrumb.cjs.map +1 -0
- package/dist/breadcrumb.d.cts +14 -0
- package/dist/breadcrumb.d.ts +14 -0
- package/dist/breadcrumb.js +8 -0
- package/dist/breadcrumb.js.map +1 -0
- package/dist/button.cjs +143 -0
- package/dist/button.cjs.map +1 -0
- package/dist/button.d.cts +34 -0
- package/dist/button.d.ts +34 -0
- package/dist/button.js +9 -0
- package/dist/button.js.map +1 -0
- package/dist/card.cjs +111 -0
- package/dist/card.cjs.map +1 -0
- package/dist/card.d.cts +15 -0
- package/dist/card.d.ts +15 -0
- package/dist/card.js +18 -0
- package/dist/card.js.map +1 -0
- package/dist/checkbox.cjs +66 -0
- package/dist/checkbox.cjs.map +1 -0
- package/{src/checkbox.tsx → dist/checkbox.d.cts} +5 -22
- package/dist/checkbox.d.ts +25 -0
- package/dist/checkbox.js +10 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-5MEOZHJA.js +513 -0
- package/dist/chunk-5MEOZHJA.js.map +1 -0
- package/dist/chunk-67ACNS42.js +29 -0
- package/dist/chunk-67ACNS42.js.map +1 -0
- package/dist/chunk-A6GRLKOO.js +85 -0
- package/dist/chunk-A6GRLKOO.js.map +1 -0
- package/dist/chunk-DS3QYIKL.js +90 -0
- package/dist/chunk-DS3QYIKL.js.map +1 -0
- package/dist/chunk-EWSBFU54.js +53 -0
- package/dist/chunk-EWSBFU54.js.map +1 -0
- package/dist/chunk-G4ZQUSAJ.js +38 -0
- package/dist/chunk-G4ZQUSAJ.js.map +1 -0
- package/dist/chunk-GMCUXEFI.js +67 -0
- package/dist/chunk-GMCUXEFI.js.map +1 -0
- package/dist/chunk-HBBOR43A.js +85 -0
- package/dist/chunk-HBBOR43A.js.map +1 -0
- package/dist/chunk-HCA5ODCD.js +11 -0
- package/dist/chunk-HCA5ODCD.js.map +1 -0
- package/dist/chunk-HYRJYUPI.js +26 -0
- package/dist/chunk-HYRJYUPI.js.map +1 -0
- package/dist/chunk-IRCTGJQB.js +26 -0
- package/dist/chunk-IRCTGJQB.js.map +1 -0
- package/dist/chunk-ISPICB4W.js +70 -0
- package/dist/chunk-ISPICB4W.js.map +1 -0
- package/dist/chunk-JDE5XYPT.js +30 -0
- package/dist/chunk-JDE5XYPT.js.map +1 -0
- package/dist/chunk-JSJEEKR3.js +59 -0
- package/dist/chunk-JSJEEKR3.js.map +1 -0
- package/dist/chunk-KR3BCFP7.js +85 -0
- package/dist/chunk-KR3BCFP7.js.map +1 -0
- package/dist/chunk-MLRSOTFI.js +81 -0
- package/dist/chunk-MLRSOTFI.js.map +1 -0
- package/dist/chunk-MQT5RAQV.js +202 -0
- package/dist/chunk-MQT5RAQV.js.map +1 -0
- package/dist/chunk-MUPW2TFJ.js +102 -0
- package/dist/chunk-MUPW2TFJ.js.map +1 -0
- package/dist/chunk-OUYT6SPU.js +76 -0
- package/dist/chunk-OUYT6SPU.js.map +1 -0
- package/dist/chunk-PM2VE7DJ.js +100 -0
- package/dist/chunk-PM2VE7DJ.js.map +1 -0
- package/dist/chunk-PPYR3O3L.js +44 -0
- package/dist/chunk-PPYR3O3L.js.map +1 -0
- package/dist/chunk-PUTC52SI.js +98 -0
- package/dist/chunk-PUTC52SI.js.map +1 -0
- package/dist/chunk-PWAKTEXF.js +18 -0
- package/dist/chunk-PWAKTEXF.js.map +1 -0
- package/dist/chunk-QTFWORA7.js +26 -0
- package/dist/chunk-QTFWORA7.js.map +1 -0
- package/dist/chunk-RUPL2H6V.js +49 -0
- package/dist/chunk-RUPL2H6V.js.map +1 -0
- package/dist/chunk-S7PVZAPL.js +28 -0
- package/dist/chunk-S7PVZAPL.js.map +1 -0
- package/dist/chunk-SQDV2AKJ.js +49 -0
- package/dist/chunk-SQDV2AKJ.js.map +1 -0
- package/dist/chunk-T2KXVH3O.js +297 -0
- package/dist/chunk-T2KXVH3O.js.map +1 -0
- package/dist/chunk-T4WCCHJQ.js +29 -0
- package/dist/chunk-T4WCCHJQ.js.map +1 -0
- package/dist/chunk-UC3ZIQPE.js +93 -0
- package/dist/chunk-UC3ZIQPE.js.map +1 -0
- package/dist/chunk-UE7JY7VR.js +28 -0
- package/dist/chunk-UE7JY7VR.js.map +1 -0
- package/dist/chunk-Y5WCT6XN.js +93 -0
- package/dist/chunk-Y5WCT6XN.js.map +1 -0
- package/dist/chunk-YDICVETX.js +151 -0
- package/dist/chunk-YDICVETX.js.map +1 -0
- package/dist/chunk-ZFSQVMLJ.js +126 -0
- package/dist/chunk-ZFSQVMLJ.js.map +1 -0
- package/dist/chunk-ZHATD73Y.js +46 -0
- package/dist/chunk-ZHATD73Y.js.map +1 -0
- package/dist/chunk-ZLAA4DGG.js +111 -0
- package/dist/chunk-ZLAA4DGG.js.map +1 -0
- package/dist/chunk-ZP252OJZ.js +56 -0
- package/dist/chunk-ZP252OJZ.js.map +1 -0
- package/dist/clipboard.cjs +125 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +52 -0
- package/dist/clipboard.d.ts +52 -0
- package/dist/clipboard.js +23 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/collapsible.cjs +94 -0
- package/dist/collapsible.cjs.map +1 -0
- package/dist/collapsible.d.cts +21 -0
- package/dist/collapsible.d.ts +21 -0
- package/dist/collapsible.js +9 -0
- package/dist/collapsible.js.map +1 -0
- package/dist/confirm-dialog.cjs +369 -0
- package/dist/confirm-dialog.cjs.map +1 -0
- package/dist/confirm-dialog.d.cts +16 -0
- package/dist/confirm-dialog.d.ts +16 -0
- package/dist/confirm-dialog.js +13 -0
- package/dist/confirm-dialog.js.map +1 -0
- package/dist/copy-button.cjs +102 -0
- package/dist/copy-button.cjs.map +1 -0
- package/dist/copy-button.d.cts +18 -0
- package/dist/copy-button.d.ts +18 -0
- package/dist/copy-button.js +10 -0
- package/dist/copy-button.js.map +1 -0
- package/dist/data-table.cjs +140 -0
- package/dist/data-table.cjs.map +1 -0
- package/dist/data-table.d.cts +81 -0
- package/dist/data-table.d.ts +81 -0
- package/dist/data-table.js +8 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-range-picker/dateRange.cjs +138 -0
- package/dist/date-range-picker/dateRange.cjs.map +1 -0
- package/dist/date-range-picker/dateRange.d.cts +57 -0
- package/dist/date-range-picker/dateRange.d.ts +57 -0
- package/dist/date-range-picker/dateRange.js +31 -0
- package/dist/date-range-picker/dateRange.js.map +1 -0
- package/dist/date-range-picker/index.cjs +375 -0
- package/dist/date-range-picker/index.cjs.map +1 -0
- package/dist/date-range-picker/index.d.cts +18 -0
- package/dist/date-range-picker/index.d.ts +18 -0
- package/dist/date-range-picker/index.js +10 -0
- package/dist/date-range-picker/index.js.map +1 -0
- package/dist/dialogLayer.cjs +121 -0
- package/dist/dialogLayer.cjs.map +1 -0
- package/dist/dialogLayer.d.cts +11 -0
- package/dist/dialogLayer.d.ts +11 -0
- package/dist/dialogLayer.js +8 -0
- package/dist/dialogLayer.js.map +1 -0
- package/dist/feature-access-gate.cjs +160 -0
- package/dist/feature-access-gate.cjs.map +1 -0
- package/dist/feature-access-gate.d.cts +14 -0
- package/dist/feature-access-gate.d.ts +14 -0
- package/dist/feature-access-gate.js +11 -0
- package/dist/feature-access-gate.js.map +1 -0
- package/dist/field.cjs +58 -0
- package/dist/field.cjs.map +1 -0
- package/dist/field.d.cts +23 -0
- package/dist/field.d.ts +23 -0
- package/dist/field.js +8 -0
- package/dist/field.js.map +1 -0
- package/dist/image-uploader.cjs +293 -0
- package/dist/image-uploader.cjs.map +1 -0
- package/dist/image-uploader.d.cts +34 -0
- package/dist/image-uploader.d.ts +34 -0
- package/dist/image-uploader.js +12 -0
- package/dist/image-uploader.js.map +1 -0
- package/dist/index.cjs +3008 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +232 -0
- package/dist/index.js.map +1 -0
- package/dist/input.cjs +58 -0
- package/dist/input.cjs.map +1 -0
- package/dist/input.d.cts +7 -0
- package/dist/input.d.ts +7 -0
- package/dist/input.js +10 -0
- package/dist/input.js.map +1 -0
- package/dist/label.cjs +67 -0
- package/dist/label.cjs.map +1 -0
- package/dist/label.d.cts +7 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.js +9 -0
- package/dist/label.js.map +1 -0
- package/dist/modal.cjs +212 -0
- package/dist/modal.cjs.map +1 -0
- package/dist/modal.d.cts +39 -0
- package/dist/modal.d.ts +39 -0
- package/dist/modal.js +12 -0
- package/dist/modal.js.map +1 -0
- package/dist/page-header.cjs +78 -0
- package/dist/page-header.cjs.map +1 -0
- package/dist/page-header.d.cts +35 -0
- package/dist/page-header.d.ts +35 -0
- package/dist/page-header.js +8 -0
- package/dist/page-header.js.map +1 -0
- package/dist/pagination.cjs +237 -0
- package/dist/pagination.cjs.map +1 -0
- package/dist/pagination.d.cts +15 -0
- package/dist/pagination.d.ts +15 -0
- package/dist/pagination.js +11 -0
- package/dist/pagination.js.map +1 -0
- package/dist/panel-brand-mark.cjs +59 -0
- package/dist/panel-brand-mark.cjs.map +1 -0
- package/dist/panel-brand-mark.d.cts +13 -0
- package/dist/panel-brand-mark.d.ts +13 -0
- package/dist/panel-brand-mark.js +8 -0
- package/dist/panel-brand-mark.js.map +1 -0
- package/dist/panel-sign-in.cjs +256 -0
- package/dist/panel-sign-in.cjs.map +1 -0
- package/dist/panel-sign-in.d.cts +31 -0
- package/dist/panel-sign-in.d.ts +31 -0
- package/dist/panel-sign-in.js +14 -0
- package/dist/panel-sign-in.js.map +1 -0
- package/dist/segmented-control.cjs +122 -0
- package/dist/segmented-control.cjs.map +1 -0
- package/dist/segmented-control.d.cts +58 -0
- package/dist/segmented-control.d.ts +58 -0
- package/dist/segmented-control.js +9 -0
- package/dist/segmented-control.js.map +1 -0
- package/dist/select.cjs +648 -0
- package/dist/select.cjs.map +1 -0
- package/dist/select.d.cts +30 -0
- package/dist/select.d.ts +30 -0
- package/dist/select.js +12 -0
- package/dist/select.js.map +1 -0
- package/dist/separator.cjs +70 -0
- package/dist/separator.cjs.map +1 -0
- package/dist/separator.d.cts +13 -0
- package/dist/separator.d.ts +13 -0
- package/dist/separator.js +9 -0
- package/dist/separator.js.map +1 -0
- package/dist/side-drawer.cjs +321 -0
- package/dist/side-drawer.cjs.map +1 -0
- package/dist/side-drawer.d.cts +21 -0
- package/dist/side-drawer.d.ts +21 -0
- package/dist/side-drawer.js +13 -0
- package/dist/side-drawer.js.map +1 -0
- package/dist/spinner.cjs +57 -0
- package/dist/spinner.cjs.map +1 -0
- package/{src/spinner.tsx → dist/spinner.d.cts} +11 -22
- package/dist/spinner.d.ts +27 -0
- package/dist/spinner.js +8 -0
- package/dist/spinner.js.map +1 -0
- package/dist/spreadsheet-grid.cjs +238 -0
- package/dist/spreadsheet-grid.cjs.map +1 -0
- package/dist/spreadsheet-grid.d.cts +48 -0
- package/dist/spreadsheet-grid.d.ts +48 -0
- package/dist/spreadsheet-grid.js +8 -0
- package/dist/spreadsheet-grid.js.map +1 -0
- package/dist/state-panel.cjs +86 -0
- package/dist/state-panel.cjs.map +1 -0
- package/dist/state-panel.d.cts +23 -0
- package/dist/state-panel.d.ts +23 -0
- package/dist/state-panel.js +9 -0
- package/dist/state-panel.js.map +1 -0
- package/dist/switch.cjs +87 -0
- package/dist/switch.cjs.map +1 -0
- package/dist/switch.d.cts +13 -0
- package/dist/switch.d.ts +13 -0
- package/dist/switch.js +9 -0
- package/dist/switch.js.map +1 -0
- package/dist/table.cjs +139 -0
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +16 -0
- package/dist/table.d.ts +16 -0
- package/dist/table.js +22 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.cjs +168 -0
- package/dist/tabs.cjs.map +1 -0
- package/dist/tabs.d.cts +59 -0
- package/dist/tabs.d.ts +59 -0
- package/dist/tabs.js +11 -0
- package/dist/tabs.js.map +1 -0
- package/dist/textarea.cjs +66 -0
- package/dist/textarea.cjs.map +1 -0
- package/dist/textarea.d.cts +18 -0
- package/dist/textarea.d.ts +18 -0
- package/dist/textarea.js +10 -0
- package/dist/textarea.js.map +1 -0
- package/dist/utils.cjs +35 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.cts +5 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/dist/utils.js.map +1 -0
- package/package.json +391 -14
- package/src/alert.tsx +0 -74
- package/src/autocomplete.tsx +0 -666
- package/src/badge.tsx +0 -60
- package/src/breadcrumb.tsx +0 -71
- package/src/button.tsx +0 -113
- package/src/card.tsx +0 -79
- package/src/clipboard.ts +0 -144
- package/src/collapsible.tsx +0 -63
- package/src/confirm-dialog.tsx +0 -84
- package/src/copy-button.tsx +0 -58
- package/src/data-table.tsx +0 -190
- package/src/date-range-picker/dateRange.ts +0 -142
- package/src/date-range-picker/index.tsx +0 -366
- package/src/dialogLayer.ts +0 -142
- package/src/feature-access-gate.tsx +0 -38
- package/src/field.tsx +0 -56
- package/src/image-uploader.tsx +0 -181
- package/src/index.ts +0 -37
- package/src/input.tsx +0 -16
- package/src/label.tsx +0 -23
- package/src/modal.tsx +0 -117
- package/src/page-header.tsx +0 -75
- package/src/pagination.tsx +0 -116
- package/src/panel-brand-mark.tsx +0 -32
- package/src/panel-sign-in.tsx +0 -134
- package/src/segmented-control.tsx +0 -155
- package/src/select.tsx +0 -105
- package/src/separator.tsx +0 -32
- package/src/side-drawer.tsx +0 -87
- package/src/spreadsheet-grid.tsx +0 -264
- package/src/state-panel.tsx +0 -46
- package/src/switch.tsx +0 -47
- package/src/table.tsx +0 -110
- package/src/tabs.tsx +0 -201
- package/src/textarea.tsx +0 -32
- package/src/utils.ts +0 -6
package/src/badge.tsx
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
-
|
|
4
|
-
import { cn } from "./utils";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The status pill every list panel was spelling out by hand.
|
|
8
|
-
*
|
|
9
|
-
* Variants are semantic rather than named after a colour, because the same
|
|
10
|
-
* status means the same thing on every panel and should not have to be
|
|
11
|
-
* re-decided per table — Deleted was `bg-rose-100 text-rose-800` in one place
|
|
12
|
-
* and `text-rose-900` two files away.
|
|
13
|
-
*
|
|
14
|
-
* These stay light-on-dark-text in both themes on purpose. A pill is a small
|
|
15
|
-
* block of colour whose whole job is to be picked out of a row of text, and
|
|
16
|
-
* inverting it per theme made the same status read as two different states.
|
|
17
|
-
*/
|
|
18
|
-
const badgeVariants = cva(
|
|
19
|
-
"inline-flex items-center gap-1 rounded font-medium whitespace-nowrap",
|
|
20
|
-
{
|
|
21
|
-
variants: {
|
|
22
|
-
variant: {
|
|
23
|
-
neutral: "bg-muted text-muted-foreground",
|
|
24
|
-
success: "bg-success/15 text-success",
|
|
25
|
-
warning: "bg-warning/15 text-warning",
|
|
26
|
-
danger: "bg-destructive/15 text-destructive",
|
|
27
|
-
info: "bg-info/15 text-info",
|
|
28
|
-
},
|
|
29
|
-
size: {
|
|
30
|
-
/** Inline chips — a category, a tenant, a permission. */
|
|
31
|
-
sm: "px-1.5 py-0.5 text-xs",
|
|
32
|
-
/** A row's status. */
|
|
33
|
-
md: "px-2 py-0.5 text-xs",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
defaultVariants: {
|
|
37
|
-
variant: "neutral",
|
|
38
|
-
size: "md",
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
export interface BadgeProps
|
|
44
|
-
extends
|
|
45
|
-
React.HTMLAttributes<HTMLSpanElement>,
|
|
46
|
-
VariantProps<typeof badgeVariants> {}
|
|
47
|
-
|
|
48
|
-
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
49
|
-
({ className, variant, size, ...props }, ref) => (
|
|
50
|
-
<span
|
|
51
|
-
ref={ref}
|
|
52
|
-
className={cn(badgeVariants({ variant, size }), className)}
|
|
53
|
-
{...props}
|
|
54
|
-
/>
|
|
55
|
-
),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
Badge.displayName = "Badge";
|
|
59
|
-
|
|
60
|
-
export { badgeVariants };
|
package/src/breadcrumb.tsx
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
import { cn } from "./utils";
|
|
5
|
-
|
|
6
|
-
export type BreadcrumbItem = {
|
|
7
|
-
label: string;
|
|
8
|
-
href?: string;
|
|
9
|
-
onClick?: () => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
interface BreadcrumbProps {
|
|
13
|
-
items: BreadcrumbItem[];
|
|
14
|
-
className?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function Breadcrumb({ items, className }: BreadcrumbProps) {
|
|
18
|
-
return (
|
|
19
|
-
<nav aria-label="Breadcrumb" className={cn("text-sm", className)}>
|
|
20
|
-
<ol className="flex flex-wrap items-center gap-1.5">
|
|
21
|
-
{items.map((item, index) => {
|
|
22
|
-
const isLast = index === items.length - 1;
|
|
23
|
-
const isInteractive = !isLast && (item.href || item.onClick);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<li
|
|
27
|
-
key={`${item.label}-${index}`}
|
|
28
|
-
className="flex items-center gap-1.5"
|
|
29
|
-
>
|
|
30
|
-
{index > 0 && (
|
|
31
|
-
<ChevronRight
|
|
32
|
-
className="text-muted-foreground/70 size-3.5 shrink-0"
|
|
33
|
-
aria-hidden
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
{isInteractive ? (
|
|
37
|
-
item.href ? (
|
|
38
|
-
<a
|
|
39
|
-
href={item.href}
|
|
40
|
-
className="text-muted-foreground hover:text-foreground transition"
|
|
41
|
-
>
|
|
42
|
-
{item.label}
|
|
43
|
-
</a>
|
|
44
|
-
) : (
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
onClick={item.onClick}
|
|
48
|
-
className="text-muted-foreground hover:text-foreground transition"
|
|
49
|
-
>
|
|
50
|
-
{item.label}
|
|
51
|
-
</button>
|
|
52
|
-
)
|
|
53
|
-
) : (
|
|
54
|
-
<span
|
|
55
|
-
className={cn(
|
|
56
|
-
isLast
|
|
57
|
-
? "text-foreground font-medium"
|
|
58
|
-
: "text-muted-foreground",
|
|
59
|
-
)}
|
|
60
|
-
aria-current={isLast ? "page" : undefined}
|
|
61
|
-
>
|
|
62
|
-
{item.label}
|
|
63
|
-
</span>
|
|
64
|
-
)}
|
|
65
|
-
</li>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</ol>
|
|
69
|
-
</nav>
|
|
70
|
-
);
|
|
71
|
-
}
|
package/src/button.tsx
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
-
|
|
5
|
-
import { Spinner } from "./spinner";
|
|
6
|
-
import { cn } from "./utils";
|
|
7
|
-
|
|
8
|
-
const buttonVariants = cva(
|
|
9
|
-
"inline-flex h-9 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 text-sm font-medium transition duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
10
|
-
{
|
|
11
|
-
variants: {
|
|
12
|
-
variant: {
|
|
13
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
14
|
-
secondary: "bg-muted text-foreground hover:bg-muted/80",
|
|
15
|
-
ghost: "hover:bg-muted",
|
|
16
|
-
destructive:
|
|
17
|
-
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
18
|
-
outline: "border border-border bg-background hover:bg-muted",
|
|
19
|
-
},
|
|
20
|
-
size: {
|
|
21
|
-
sm: "h-8 px-2.5",
|
|
22
|
-
md: "h-9 px-3",
|
|
23
|
-
lg: "h-10 px-4",
|
|
24
|
-
icon: "h-9 w-9 px-0",
|
|
25
|
-
// Square, matching `sm`'s height — for an icon-only control sitting in
|
|
26
|
-
// a table row or beside `sm` buttons, where `icon` is a row taller.
|
|
27
|
-
"icon-sm": "h-8 w-8 px-0",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
defaultVariants: {
|
|
31
|
-
variant: "default",
|
|
32
|
-
size: "md",
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
export interface ButtonProps
|
|
38
|
-
extends
|
|
39
|
-
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
40
|
-
VariantProps<typeof buttonVariants> {
|
|
41
|
-
asChild?: boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Work is in flight: shows a spinner, disables the button, and sets
|
|
44
|
-
* `aria-busy`.
|
|
45
|
-
*
|
|
46
|
-
* This exists because the panel had 272 disabled buttons and only 22 that
|
|
47
|
-
* said anything about why — the rest went dead on click with no spinner and
|
|
48
|
-
* no label change, which reads as a broken button rather than a busy one.
|
|
49
|
-
* Making the feedback part of `disabled` rather than something each caller
|
|
50
|
-
* remembers separately is the only way that ratio stays fixed.
|
|
51
|
-
*
|
|
52
|
-
* The label is kept rather than swapped. "Saving…" loses which button you
|
|
53
|
-
* pressed, and the spinner already says it is working. An icon-only button
|
|
54
|
-
* is the exception — there is no room beside the glyph, so the spinner takes
|
|
55
|
-
* its place; the `aria-label` is what names the button either way.
|
|
56
|
-
*
|
|
57
|
-
* "Icon-only" means an icon `size`. A square button that grew a spinner
|
|
58
|
-
* beside its glyph would not just look wrong, it would resize mid-request
|
|
59
|
-
* and shove whatever sits next to it sideways.
|
|
60
|
-
*/
|
|
61
|
-
loading?: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Sizes with no room for a spinner beside the glyph. */
|
|
65
|
-
const ICON_SIZES = new Set<string>(["icon", "icon-sm"]);
|
|
66
|
-
|
|
67
|
-
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
68
|
-
(
|
|
69
|
-
{
|
|
70
|
-
className,
|
|
71
|
-
variant,
|
|
72
|
-
size,
|
|
73
|
-
asChild = false,
|
|
74
|
-
loading,
|
|
75
|
-
disabled,
|
|
76
|
-
children,
|
|
77
|
-
...props
|
|
78
|
-
},
|
|
79
|
-
ref,
|
|
80
|
-
) => {
|
|
81
|
-
const Comp = asChild ? Slot : "button";
|
|
82
|
-
|
|
83
|
-
// `asChild` renders someone else's element, which may not take a spinner or
|
|
84
|
-
// a disabled attribute — so the busy treatment is only applied to a real
|
|
85
|
-
// button, and passing both is a mistake worth not papering over.
|
|
86
|
-
if (asChild) {
|
|
87
|
-
return (
|
|
88
|
-
<Comp
|
|
89
|
-
ref={ref}
|
|
90
|
-
className={cn(buttonVariants({ variant, size }), className)}
|
|
91
|
-
{...props}
|
|
92
|
-
>
|
|
93
|
-
{children}
|
|
94
|
-
</Comp>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<button
|
|
100
|
-
ref={ref}
|
|
101
|
-
disabled={disabled || loading}
|
|
102
|
-
aria-busy={loading || undefined}
|
|
103
|
-
className={cn(buttonVariants({ variant, size }), className)}
|
|
104
|
-
{...props}
|
|
105
|
-
>
|
|
106
|
-
{loading ? <Spinner size="sm" /> : null}
|
|
107
|
-
{loading && size != null && ICON_SIZES.has(size) ? null : children}
|
|
108
|
-
</button>
|
|
109
|
-
);
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
Button.displayName = "Button";
|
package/src/card.tsx
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "./utils";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Surface container for grouped content. Prefer a bare section when there is
|
|
7
|
-
* no need for border / padding chrome — cards are for interactive or
|
|
8
|
-
* self-contained blocks, not every stack of text.
|
|
9
|
-
*/
|
|
10
|
-
export const Card = React.forwardRef<
|
|
11
|
-
HTMLDivElement,
|
|
12
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
13
|
-
>(({ className, ...props }, ref) => (
|
|
14
|
-
<div
|
|
15
|
-
ref={ref}
|
|
16
|
-
className={cn(
|
|
17
|
-
"bg-card text-card-foreground rounded-xl border shadow",
|
|
18
|
-
className,
|
|
19
|
-
)}
|
|
20
|
-
{...props}
|
|
21
|
-
/>
|
|
22
|
-
));
|
|
23
|
-
Card.displayName = "Card";
|
|
24
|
-
|
|
25
|
-
export const CardHeader = React.forwardRef<
|
|
26
|
-
HTMLDivElement,
|
|
27
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
28
|
-
>(({ className, ...props }, ref) => (
|
|
29
|
-
<div
|
|
30
|
-
ref={ref}
|
|
31
|
-
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
32
|
-
{...props}
|
|
33
|
-
/>
|
|
34
|
-
));
|
|
35
|
-
CardHeader.displayName = "CardHeader";
|
|
36
|
-
|
|
37
|
-
export const CardTitle = React.forwardRef<
|
|
38
|
-
HTMLDivElement,
|
|
39
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
40
|
-
>(({ className, ...props }, ref) => (
|
|
41
|
-
<div
|
|
42
|
-
ref={ref}
|
|
43
|
-
className={cn("leading-none font-semibold tracking-tight", className)}
|
|
44
|
-
{...props}
|
|
45
|
-
/>
|
|
46
|
-
));
|
|
47
|
-
CardTitle.displayName = "CardTitle";
|
|
48
|
-
|
|
49
|
-
export const CardDescription = React.forwardRef<
|
|
50
|
-
HTMLDivElement,
|
|
51
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
52
|
-
>(({ className, ...props }, ref) => (
|
|
53
|
-
<div
|
|
54
|
-
ref={ref}
|
|
55
|
-
className={cn("text-muted-foreground text-sm", className)}
|
|
56
|
-
{...props}
|
|
57
|
-
/>
|
|
58
|
-
));
|
|
59
|
-
CardDescription.displayName = "CardDescription";
|
|
60
|
-
|
|
61
|
-
export const CardContent = React.forwardRef<
|
|
62
|
-
HTMLDivElement,
|
|
63
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
64
|
-
>(({ className, ...props }, ref) => (
|
|
65
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
66
|
-
));
|
|
67
|
-
CardContent.displayName = "CardContent";
|
|
68
|
-
|
|
69
|
-
export const CardFooter = React.forwardRef<
|
|
70
|
-
HTMLDivElement,
|
|
71
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
72
|
-
>(({ className, ...props }, ref) => (
|
|
73
|
-
<div
|
|
74
|
-
ref={ref}
|
|
75
|
-
className={cn("flex items-center p-6 pt-0", className)}
|
|
76
|
-
{...props}
|
|
77
|
-
/>
|
|
78
|
-
));
|
|
79
|
-
CardFooter.displayName = "CardFooter";
|
package/src/clipboard.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Range maths and clipboard text for the card table's spreadsheet selection.
|
|
3
|
-
*
|
|
4
|
-
* Ported from video-shorts-panel `app/src/common/components/ExcelTable.jsx`.
|
|
5
|
-
* Kept free of React and the DOM (except `writeToClipboard`) so the escaping
|
|
6
|
-
* and range rules are unit-testable.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type CellRef = { r: number; c: number };
|
|
10
|
-
|
|
11
|
-
export type CellRange = { r1: number; r2: number; c1: number; c2: number };
|
|
12
|
-
|
|
13
|
-
/** Tab for the clipboard (Excel/Sheets split on it), comma for a .csv file. */
|
|
14
|
-
export const TSV_DELIMITER = "\t";
|
|
15
|
-
export const CSV_DELIMITER = ",";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Anchor and focus can be dragged in any direction, so the range is whichever
|
|
19
|
-
* corners they happen to be — normalize to top-left / bottom-right.
|
|
20
|
-
*/
|
|
21
|
-
export function normalizeRange(
|
|
22
|
-
anchor: CellRef | null | undefined,
|
|
23
|
-
focus: CellRef | null | undefined,
|
|
24
|
-
): CellRange | null {
|
|
25
|
-
if (!anchor || !focus) return null;
|
|
26
|
-
return {
|
|
27
|
-
r1: Math.min(anchor.r, focus.r),
|
|
28
|
-
r2: Math.max(anchor.r, focus.r),
|
|
29
|
-
c1: Math.min(anchor.c, focus.c),
|
|
30
|
-
c2: Math.max(anchor.c, focus.c),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A cell needs quoting once it carries the delimiters themselves — the same
|
|
36
|
-
* rule Excel uses when it parses the clipboard. Without it, a title containing
|
|
37
|
-
* a tab or a newline silently becomes extra columns or rows on paste.
|
|
38
|
-
*/
|
|
39
|
-
export function escapeCell(raw: unknown, delimiter: string): string {
|
|
40
|
-
const value = raw == null ? "" : String(raw);
|
|
41
|
-
if (
|
|
42
|
-
value.includes(delimiter) ||
|
|
43
|
-
value.includes("\n") ||
|
|
44
|
-
value.includes("\r") ||
|
|
45
|
-
value.includes('"')
|
|
46
|
-
) {
|
|
47
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type TextColumn<Row> = {
|
|
53
|
-
title: string;
|
|
54
|
-
value: (row: Row) => string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Renders a range as delimited text, optionally with a header line. */
|
|
58
|
-
export function buildRangeText<Row>(
|
|
59
|
-
range: CellRange | null,
|
|
60
|
-
columns: TextColumn<Row>[],
|
|
61
|
-
rows: Row[],
|
|
62
|
-
delimiter: string,
|
|
63
|
-
includeHeaders: boolean,
|
|
64
|
-
): string {
|
|
65
|
-
if (!range) return "";
|
|
66
|
-
const lines: string[] = [];
|
|
67
|
-
|
|
68
|
-
if (includeHeaders) {
|
|
69
|
-
const header: string[] = [];
|
|
70
|
-
for (let c = range.c1; c <= range.c2; c++) {
|
|
71
|
-
header.push(escapeCell(columns[c]?.title, delimiter));
|
|
72
|
-
}
|
|
73
|
-
lines.push(header.join(delimiter));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
for (let r = range.r1; r <= range.r2; r++) {
|
|
77
|
-
const line: string[] = [];
|
|
78
|
-
const row = rows[r];
|
|
79
|
-
for (let c = range.c1; c <= range.c2; c++) {
|
|
80
|
-
const column = columns[c];
|
|
81
|
-
line.push(
|
|
82
|
-
escapeCell(
|
|
83
|
-
column && row !== undefined ? column.value(row) : "",
|
|
84
|
-
delimiter,
|
|
85
|
-
),
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
lines.push(line.join(delimiter));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return lines.join("\n");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The whole grid, or null when there is nothing to select. */
|
|
95
|
-
export function fullRange<Row>(
|
|
96
|
-
columns: TextColumn<Row>[],
|
|
97
|
-
rows: Row[],
|
|
98
|
-
): CellRange | null {
|
|
99
|
-
if (!rows.length || !columns.length) return null;
|
|
100
|
-
return { r1: 0, r2: rows.length - 1, c1: 0, c2: columns.length - 1 };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** "3 × 2 selected" — what the toolbar reports back to the user. */
|
|
104
|
-
export function describeRange(range: CellRange | null): string {
|
|
105
|
-
if (!range) return "No selection";
|
|
106
|
-
const rowCount = range.r2 - range.r1 + 1;
|
|
107
|
-
const colCount = range.c2 - range.c1 + 1;
|
|
108
|
-
if (rowCount === 1 && colCount === 1) return "1 cell selected";
|
|
109
|
-
return `${rowCount} × ${colCount} selected`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function countCells(range: CellRange): {
|
|
113
|
-
rows: number;
|
|
114
|
-
columns: number;
|
|
115
|
-
} {
|
|
116
|
-
return {
|
|
117
|
-
rows: range.r2 - range.r1 + 1,
|
|
118
|
-
columns: range.c2 - range.c1 + 1,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* `navigator.clipboard` is unavailable on http origins and older browsers, so
|
|
124
|
-
* fall back to a hidden textarea + execCommand rather than failing the copy.
|
|
125
|
-
*/
|
|
126
|
-
export async function writeToClipboard(text: string): Promise<void> {
|
|
127
|
-
if (navigator.clipboard?.writeText) {
|
|
128
|
-
return navigator.clipboard.writeText(text);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const textarea = document.createElement("textarea");
|
|
132
|
-
textarea.value = text;
|
|
133
|
-
textarea.style.position = "fixed";
|
|
134
|
-
textarea.style.top = "-1000px";
|
|
135
|
-
document.body.appendChild(textarea);
|
|
136
|
-
textarea.select();
|
|
137
|
-
try {
|
|
138
|
-
if (!document.execCommand("copy")) {
|
|
139
|
-
throw new Error("Could not write to the clipboard");
|
|
140
|
-
}
|
|
141
|
-
} finally {
|
|
142
|
-
document.body.removeChild(textarea);
|
|
143
|
-
}
|
|
144
|
-
}
|
package/src/collapsible.tsx
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { ChevronDown } from "lucide-react";
|
|
4
|
-
import * as React from "react";
|
|
5
|
-
|
|
6
|
-
import { cn } from "./utils";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A titled section that folds away.
|
|
10
|
-
*
|
|
11
|
-
* For sidebar panels that are long but not always in use — the body stays
|
|
12
|
-
* mounted so its state (a loaded list, a half-filled filter) survives being
|
|
13
|
-
* collapsed, rather than being thrown away and refetched on the way back.
|
|
14
|
-
*/
|
|
15
|
-
export function CollapsibleSection({
|
|
16
|
-
title,
|
|
17
|
-
icon,
|
|
18
|
-
defaultOpen = true,
|
|
19
|
-
right,
|
|
20
|
-
className,
|
|
21
|
-
children,
|
|
22
|
-
}: {
|
|
23
|
-
title: string;
|
|
24
|
-
/** Optional leading glyph, shown between the chevron and the title. */
|
|
25
|
-
icon?: React.ReactNode;
|
|
26
|
-
defaultOpen?: boolean;
|
|
27
|
-
/** Optional trailing content in the header, e.g. a count. */
|
|
28
|
-
right?: React.ReactNode;
|
|
29
|
-
className?: string;
|
|
30
|
-
children: React.ReactNode;
|
|
31
|
-
}) {
|
|
32
|
-
const [open, setOpen] = React.useState(defaultOpen);
|
|
33
|
-
const bodyId = React.useId();
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div className={cn("border-border rounded-lg border", className)}>
|
|
37
|
-
<button
|
|
38
|
-
type="button"
|
|
39
|
-
aria-expanded={open}
|
|
40
|
-
aria-controls={bodyId}
|
|
41
|
-
onClick={() => setOpen((current) => !current)}
|
|
42
|
-
className="hover:bg-muted/50 flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-semibold transition"
|
|
43
|
-
>
|
|
44
|
-
<ChevronDown
|
|
45
|
-
size={14}
|
|
46
|
-
aria-hidden
|
|
47
|
-
className={cn(
|
|
48
|
-
"shrink-0 transition-transform",
|
|
49
|
-
open ? "" : "-rotate-90",
|
|
50
|
-
)}
|
|
51
|
-
/>
|
|
52
|
-
{icon}
|
|
53
|
-
<span className="flex-1">{title}</span>
|
|
54
|
-
{right}
|
|
55
|
-
</button>
|
|
56
|
-
{/* Hidden rather than unmounted, so collapsing does not discard the
|
|
57
|
-
panel's state or refetch what it had already loaded. */}
|
|
58
|
-
<div id={bodyId} hidden={!open} className="space-y-3 px-3 pt-1 pb-3">
|
|
59
|
-
{children}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
}
|
package/src/confirm-dialog.tsx
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
|
|
5
|
-
import { Button } from "./button";
|
|
6
|
-
import { Modal } from "./modal";
|
|
7
|
-
|
|
8
|
-
interface ConfirmDialogProps {
|
|
9
|
-
isOpen: boolean;
|
|
10
|
-
title: string;
|
|
11
|
-
description?: React.ReactNode;
|
|
12
|
-
confirmLabel?: string;
|
|
13
|
-
cancelLabel?: string;
|
|
14
|
-
isConfirming?: boolean;
|
|
15
|
-
error?: string | null;
|
|
16
|
-
onConfirm: () => void;
|
|
17
|
-
onClose: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function ConfirmDialog({
|
|
21
|
-
isOpen,
|
|
22
|
-
title,
|
|
23
|
-
description,
|
|
24
|
-
confirmLabel = "Delete",
|
|
25
|
-
cancelLabel = "Cancel",
|
|
26
|
-
isConfirming,
|
|
27
|
-
error,
|
|
28
|
-
onConfirm,
|
|
29
|
-
onClose,
|
|
30
|
-
}: ConfirmDialogProps) {
|
|
31
|
-
const titleId = React.useId();
|
|
32
|
-
const descriptionId = React.useId();
|
|
33
|
-
const errorId = React.useId();
|
|
34
|
-
|
|
35
|
-
const describedBy = [
|
|
36
|
-
description ? descriptionId : null,
|
|
37
|
-
error ? errorId : null,
|
|
38
|
-
]
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
.join(" ");
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Modal
|
|
44
|
-
isOpen={isOpen}
|
|
45
|
-
role="alertdialog"
|
|
46
|
-
labelledBy={titleId}
|
|
47
|
-
describedBy={describedBy || undefined}
|
|
48
|
-
closeLabel="Close dialog"
|
|
49
|
-
onClose={onClose}
|
|
50
|
-
>
|
|
51
|
-
<h3 id={titleId} className="text-lg font-semibold">
|
|
52
|
-
{title}
|
|
53
|
-
</h3>
|
|
54
|
-
{description ? (
|
|
55
|
-
<div id={descriptionId} className="text-muted-foreground mt-2 text-sm">
|
|
56
|
-
{description}
|
|
57
|
-
</div>
|
|
58
|
-
) : null}
|
|
59
|
-
|
|
60
|
-
{error ? (
|
|
61
|
-
<div
|
|
62
|
-
id={errorId}
|
|
63
|
-
role="alert"
|
|
64
|
-
className="border-destructive/30 bg-destructive/5 text-destructive mt-4 rounded-lg border px-3 py-2 text-sm"
|
|
65
|
-
>
|
|
66
|
-
{error}
|
|
67
|
-
</div>
|
|
68
|
-
) : null}
|
|
69
|
-
|
|
70
|
-
<div className="mt-6 flex justify-end gap-2">
|
|
71
|
-
<Button variant="outline" onClick={onClose} disabled={isConfirming}>
|
|
72
|
-
{cancelLabel}
|
|
73
|
-
</Button>
|
|
74
|
-
<Button
|
|
75
|
-
variant="destructive"
|
|
76
|
-
onClick={onConfirm}
|
|
77
|
-
loading={isConfirming}
|
|
78
|
-
>
|
|
79
|
-
{confirmLabel}
|
|
80
|
-
</Button>
|
|
81
|
-
</div>
|
|
82
|
-
</Modal>
|
|
83
|
-
);
|
|
84
|
-
}
|
package/src/copy-button.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Check, Copy } from "lucide-react";
|
|
4
|
-
import * as React from "react";
|
|
5
|
-
|
|
6
|
-
import { writeToClipboard } from "./clipboard";
|
|
7
|
-
import { cn } from "./utils";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Copies one short string — an id, a title, a URL — and says so for a moment.
|
|
11
|
-
*
|
|
12
|
-
* The panel had this three times over, each with its own timeout and its own
|
|
13
|
-
* silent-failure story. `writeToClipboard` is used rather than
|
|
14
|
-
* `navigator.clipboard` directly because the latter is undefined on http
|
|
15
|
-
* origins, which is exactly where this panel runs in local dev.
|
|
16
|
-
*/
|
|
17
|
-
export function CopyButton({
|
|
18
|
-
value,
|
|
19
|
-
label,
|
|
20
|
-
className,
|
|
21
|
-
}: {
|
|
22
|
-
value: string;
|
|
23
|
-
/** What is being copied, for the accessible name: "card id", "title". */
|
|
24
|
-
label: string;
|
|
25
|
-
className?: string;
|
|
26
|
-
}) {
|
|
27
|
-
const [copied, setCopied] = React.useState(false);
|
|
28
|
-
|
|
29
|
-
React.useEffect(() => {
|
|
30
|
-
if (!copied) return;
|
|
31
|
-
const timer = window.setTimeout(() => setCopied(false), 1500);
|
|
32
|
-
return () => window.clearTimeout(timer);
|
|
33
|
-
}, [copied]);
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<button
|
|
37
|
-
type="button"
|
|
38
|
-
aria-label={copied ? `${label} copied` : `Copy ${label}`}
|
|
39
|
-
title={copied ? "Copied" : `Copy ${label}`}
|
|
40
|
-
className={cn(
|
|
41
|
-
"text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring shrink-0 cursor-pointer rounded p-1 transition focus-visible:ring-2 focus-visible:outline-none",
|
|
42
|
-
className,
|
|
43
|
-
)}
|
|
44
|
-
onClick={() => {
|
|
45
|
-
// A denied clipboard permission should not throw into the row.
|
|
46
|
-
void writeToClipboard(value)
|
|
47
|
-
.then(() => setCopied(true))
|
|
48
|
-
.catch(() => undefined);
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{copied ? (
|
|
52
|
-
<Check className="size-3.5 text-success" aria-hidden />
|
|
53
|
-
) : (
|
|
54
|
-
<Copy className="size-3.5" aria-hidden />
|
|
55
|
-
)}
|
|
56
|
-
</button>
|
|
57
|
-
);
|
|
58
|
-
}
|