@flamingo-stack/openframe-frontend-core 0.0.327 → 0.0.328-snapshot.20260626183749
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/{chunk-CWXO6VCD.js → chunk-2UVIHKQQ.js} +4146 -3950
- package/dist/chunk-2UVIHKQQ.js.map +1 -0
- package/dist/{chunk-CRHOAKG5.cjs → chunk-5BEJ74WU.cjs} +70 -70
- package/dist/{chunk-CRHOAKG5.cjs.map → chunk-5BEJ74WU.cjs.map} +1 -1
- package/dist/{chunk-A6NBGRGZ.js → chunk-65VXW6GZ.js} +2 -2
- package/dist/{chunk-J4HHXHMC.js → chunk-67MYKAAK.js} +6 -6
- package/dist/{chunk-MWAEG3XK.cjs → chunk-6B7YS7O6.cjs} +12 -12
- package/dist/{chunk-MWAEG3XK.cjs.map → chunk-6B7YS7O6.cjs.map} +1 -1
- package/dist/{chunk-HQ6BILVP.cjs → chunk-6CYMYELP.cjs} +31 -31
- package/dist/{chunk-HQ6BILVP.cjs.map → chunk-6CYMYELP.cjs.map} +1 -1
- package/dist/{chunk-GABTFTDE.cjs → chunk-C34M45FI.cjs} +822 -626
- package/dist/chunk-C34M45FI.cjs.map +1 -0
- package/dist/{chunk-GXRWE7NB.cjs → chunk-CCG4DARM.cjs} +2 -2
- package/dist/{chunk-GXRWE7NB.cjs.map → chunk-CCG4DARM.cjs.map} +1 -1
- package/dist/{chunk-27IO7V3P.cjs → chunk-DCIB6AK3.cjs} +9 -9
- package/dist/{chunk-27IO7V3P.cjs.map → chunk-DCIB6AK3.cjs.map} +1 -1
- package/dist/{chunk-LESTX5DM.js → chunk-EUMMHRR4.js} +3 -3
- package/dist/{chunk-UHHJIHUZ.js → chunk-HT67OPIE.js} +6 -6
- package/dist/{chunk-PGFQ6WQR.cjs → chunk-HWFVPOHJ.cjs} +13 -13
- package/dist/{chunk-PGFQ6WQR.cjs.map → chunk-HWFVPOHJ.cjs.map} +1 -1
- package/dist/{chunk-U7GDY7Y3.js → chunk-IYPVN3DV.js} +5 -5
- package/dist/{chunk-3UVR2JIB.js → chunk-KDHZVMH5.js} +3 -3
- package/dist/{chunk-3AXH7BDM.cjs → chunk-KMIYWEPB.cjs} +29 -29
- package/dist/{chunk-3AXH7BDM.cjs.map → chunk-KMIYWEPB.cjs.map} +1 -1
- package/dist/{chunk-I4OFXYJR.cjs → chunk-MKLDE5OZ.cjs} +19 -19
- package/dist/{chunk-I4OFXYJR.cjs.map → chunk-MKLDE5OZ.cjs.map} +1 -1
- package/dist/{chunk-XDNQJQGP.cjs → chunk-NNP323TB.cjs} +5 -5
- package/dist/{chunk-XDNQJQGP.cjs.map → chunk-NNP323TB.cjs.map} +1 -1
- package/dist/{chunk-QF4XERYJ.js → chunk-OUUA36U2.js} +2 -2
- package/dist/{chunk-62O2VTR2.js → chunk-OZK7XALE.js} +2 -2
- package/dist/{chunk-4ZHA24HS.cjs → chunk-QCVM3ZSD.cjs} +40 -40
- package/dist/{chunk-4ZHA24HS.cjs.map → chunk-QCVM3ZSD.cjs.map} +1 -1
- package/dist/{chunk-46GJFN4V.js → chunk-QGBSE4JV.js} +5 -5
- package/dist/{chunk-BWWUHOE3.cjs → chunk-S2Z6HDRF.cjs} +73 -73
- package/dist/{chunk-BWWUHOE3.cjs.map → chunk-S2Z6HDRF.cjs.map} +1 -1
- package/dist/{chunk-7FY2OM5W.cjs → chunk-SH6X376N.cjs} +39 -39
- package/dist/{chunk-7FY2OM5W.cjs.map → chunk-SH6X376N.cjs.map} +1 -1
- package/dist/{chunk-7KZJV4MP.js → chunk-SWGXCGQ3.js} +2 -2
- package/dist/{chunk-K5HBVPJ5.cjs → chunk-SY37FQHF.cjs} +3 -3
- package/dist/{chunk-K5HBVPJ5.cjs.map → chunk-SY37FQHF.cjs.map} +1 -1
- package/dist/{chunk-5WDRXBM3.js → chunk-TH65GPIK.js} +3 -3
- package/dist/{chunk-MJUYWNQB.js → chunk-UNL2RMOC.js} +3 -3
- package/dist/{chunk-4MD2TPFY.js → chunk-UZTGDV6N.js} +2 -2
- package/dist/{chunk-NROSNR5J.cjs → chunk-V6LUP7PN.cjs} +15 -15
- package/dist/{chunk-NROSNR5J.cjs.map → chunk-V6LUP7PN.cjs.map} +1 -1
- package/dist/{chunk-IDMM6DVL.js → chunk-YDUV3VQ2.js} +4 -4
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/icons-v2-generated/index.cjs +2 -2
- package/dist/components/icons-v2-generated/index.js +1 -1
- package/dist/components/index.cjs +127 -125
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/autocomplete.d.ts +5 -0
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/file-upload.d.ts +3 -1
- package/dist/components/ui/file-upload.d.ts.map +1 -1
- package/dist/components/ui/image-uploader.d.ts +6 -1
- package/dist/components/ui/image-uploader.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/components/ui/tag-select-dropdown.d.ts +36 -0
- package/dist/components/ui/tag-select-dropdown.d.ts.map +1 -0
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/src/components/ui/autocomplete.tsx +50 -18
- package/src/components/ui/file-upload.tsx +74 -76
- package/src/components/ui/image-uploader.tsx +7 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/tag-select-dropdown.tsx +220 -0
- package/dist/chunk-CWXO6VCD.js.map +0 -1
- package/dist/chunk-GABTFTDE.cjs.map +0 -1
- /package/dist/{chunk-A6NBGRGZ.js.map → chunk-65VXW6GZ.js.map} +0 -0
- /package/dist/{chunk-J4HHXHMC.js.map → chunk-67MYKAAK.js.map} +0 -0
- /package/dist/{chunk-LESTX5DM.js.map → chunk-EUMMHRR4.js.map} +0 -0
- /package/dist/{chunk-UHHJIHUZ.js.map → chunk-HT67OPIE.js.map} +0 -0
- /package/dist/{chunk-U7GDY7Y3.js.map → chunk-IYPVN3DV.js.map} +0 -0
- /package/dist/{chunk-3UVR2JIB.js.map → chunk-KDHZVMH5.js.map} +0 -0
- /package/dist/{chunk-QF4XERYJ.js.map → chunk-OUUA36U2.js.map} +0 -0
- /package/dist/{chunk-62O2VTR2.js.map → chunk-OZK7XALE.js.map} +0 -0
- /package/dist/{chunk-46GJFN4V.js.map → chunk-QGBSE4JV.js.map} +0 -0
- /package/dist/{chunk-7KZJV4MP.js.map → chunk-SWGXCGQ3.js.map} +0 -0
- /package/dist/{chunk-5WDRXBM3.js.map → chunk-TH65GPIK.js.map} +0 -0
- /package/dist/{chunk-MJUYWNQB.js.map → chunk-UNL2RMOC.js.map} +0 -0
- /package/dist/{chunk-4MD2TPFY.js.map → chunk-UZTGDV6N.js.map} +0 -0
- /package/dist/{chunk-IDMM6DVL.js.map → chunk-YDUV3VQ2.js.map} +0 -0
|
@@ -19,6 +19,7 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
|
19
19
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
20
20
|
|
|
21
21
|
import { CheckIcon } from "../icons-v2-generated/signs-and-symbols/check-icon"
|
|
22
|
+
import { TrashIcon } from "../icons-v2-generated/interface/trash-icon"
|
|
22
23
|
import { XmarkCircleIcon } from "../icons-v2-generated/signs-and-symbols/xmark-circle-icon"
|
|
23
24
|
import { Chevron02DownIcon } from "../icons-v2-generated/arrows/chevron-02-down-icon"
|
|
24
25
|
|
|
@@ -77,6 +78,11 @@ interface AutocompleteBaseProps<T = string> {
|
|
|
77
78
|
creatable?: boolean
|
|
78
79
|
/** Callback fired after a new option is created via creatable. Use it to persist the new option server-side, etc. */
|
|
79
80
|
onCreateOption?: (inputValue: string) => void
|
|
81
|
+
/** Max length for a created option. When exceeded, creation is blocked and a hint is shown. Omit for no limit. */
|
|
82
|
+
maxCreateLength?: number
|
|
83
|
+
/** When set, each unselected option shows a hover trash button that calls this. Omit to hide delete. */
|
|
84
|
+
onDeleteOption?: (value: T) => void
|
|
85
|
+
isDeletingOption?: boolean
|
|
80
86
|
/** When true, disables built-in client-side filtering (useful when options are filtered server-side via onInputChange) */
|
|
81
87
|
disableClientFilter?: boolean
|
|
82
88
|
/** Whether to show the chevron icon. Default true */
|
|
@@ -145,6 +151,9 @@ function AutocompleteInner<T = string>(
|
|
|
145
151
|
loadingText = "Loading...",
|
|
146
152
|
creatable = false,
|
|
147
153
|
onCreateOption,
|
|
154
|
+
maxCreateLength,
|
|
155
|
+
onDeleteOption,
|
|
156
|
+
isDeletingOption = false,
|
|
148
157
|
disableClientFilter = false,
|
|
149
158
|
showChevron = true,
|
|
150
159
|
clearOnOpen = true,
|
|
@@ -269,11 +278,12 @@ function AutocompleteInner<T = string>(
|
|
|
269
278
|
|
|
270
279
|
// Show "+ Create" option when creatable is on, user typed something, and nothing matched
|
|
271
280
|
const showCreateOption = creatable && inputValue.trim().length > 0 && filteredOptions.length === 0
|
|
281
|
+
const isCreateTooLong = maxCreateLength != null && inputValue.trim().length > maxCreateLength
|
|
272
282
|
|
|
273
283
|
// Handle creating a new option
|
|
274
284
|
const handleCreate = () => {
|
|
275
285
|
const trimmed = inputValue.trim()
|
|
276
|
-
if (!trimmed) return
|
|
286
|
+
if (!trimmed || (maxCreateLength != null && trimmed.length > maxCreateLength)) return
|
|
277
287
|
|
|
278
288
|
const newValue = trimmed as T
|
|
279
289
|
|
|
@@ -564,18 +574,24 @@ function AutocompleteInner<T = string>(
|
|
|
564
574
|
</div>
|
|
565
575
|
) : filteredOptions.length === 0 ? (
|
|
566
576
|
showCreateOption ? (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
"
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
577
|
+
isCreateTooLong ? (
|
|
578
|
+
<div className="flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] text-h5 text-ods-error min-w-0">
|
|
579
|
+
Maximum {maxCreateLength} characters
|
|
580
|
+
</div>
|
|
581
|
+
) : (
|
|
582
|
+
<div
|
|
583
|
+
role="option"
|
|
584
|
+
aria-selected={false}
|
|
585
|
+
className={cn(
|
|
586
|
+
"flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] cursor-pointer transition-colors min-w-0",
|
|
587
|
+
"text-h4 text-ods-accent",
|
|
588
|
+
"hover:bg-ods-bg-hover"
|
|
589
|
+
)}
|
|
590
|
+
onClick={handleCreate}
|
|
591
|
+
>
|
|
592
|
+
<span className="truncate" title={`+ Create "${inputValue.trim()}"`}>+ Create "{inputValue.trim()}"</span>
|
|
593
|
+
</div>
|
|
594
|
+
)
|
|
579
595
|
) : (
|
|
580
596
|
<div className="px-3 py-2 text-ods-text-secondary text-[14px]">
|
|
581
597
|
{freeSolo && inputValue.trim() ? (
|
|
@@ -596,7 +612,7 @@ function AutocompleteInner<T = string>(
|
|
|
596
612
|
role="option"
|
|
597
613
|
aria-selected={isSelected}
|
|
598
614
|
className={cn(
|
|
599
|
-
"flex items-center h-11 md:h-12 px-
|
|
615
|
+
"group/item flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] cursor-pointer transition-colors border-b border-ods-border last:border-b-0",
|
|
600
616
|
"text-h4",
|
|
601
617
|
isHighlighted && "bg-ods-bg-surface",
|
|
602
618
|
isSelected ? "text-ods-accent" : "text-ods-text-primary",
|
|
@@ -606,11 +622,27 @@ function AutocompleteInner<T = string>(
|
|
|
606
622
|
onMouseEnter={() => setHighlightedIndex(index)}
|
|
607
623
|
>
|
|
608
624
|
{renderOption ? renderOption(option, isSelected) : (
|
|
609
|
-
<div className="flex items-center justify-between w-full min-w-0">
|
|
625
|
+
<div className="flex items-center justify-between gap-[var(--spacing-system-xsf)] w-full min-w-0">
|
|
610
626
|
<span className="truncate" title={option.label}>{option.label}</span>
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
627
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)] shrink-0">
|
|
628
|
+
{isSelected && (
|
|
629
|
+
<CheckIcon className="text-ods-accent" size={20} />
|
|
630
|
+
)}
|
|
631
|
+
{onDeleteOption && !isSelected && (
|
|
632
|
+
<button
|
|
633
|
+
type="button"
|
|
634
|
+
aria-label={`Delete ${option.label}`}
|
|
635
|
+
onClick={e => {
|
|
636
|
+
e.stopPropagation()
|
|
637
|
+
onDeleteOption(option.value)
|
|
638
|
+
}}
|
|
639
|
+
disabled={isDeletingOption}
|
|
640
|
+
className="opacity-0 transition-opacity group-hover/item:opacity-100 focus-visible:opacity-100 disabled:opacity-50 disabled:pointer-events-none"
|
|
641
|
+
>
|
|
642
|
+
<TrashIcon className="size-4 text-ods-error" />
|
|
643
|
+
</button>
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
614
646
|
</div>
|
|
615
647
|
)}
|
|
616
648
|
</div>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { Loader2
|
|
4
|
+
import { Loader2 } from "lucide-react"
|
|
5
5
|
import { ImagePlusIcon } from "../icons-v2-generated/audio-and-visual/image-plus-icon"
|
|
6
|
+
import { Download02Icon } from "../icons-v2-generated/interface/download-02-icon"
|
|
7
|
+
import { TrashIcon } from "../icons-v2-generated/interface/trash-icon"
|
|
8
|
+
import { FileIcon } from "../icons-v2-generated/documents/file-icon"
|
|
6
9
|
import { cn } from "../../utils/cn"
|
|
7
10
|
import { FieldWrapper } from "./field-wrapper"
|
|
8
11
|
|
|
@@ -33,6 +36,8 @@ export interface FileUploadProps {
|
|
|
33
36
|
managedFiles?: ManagedFileEntry[]
|
|
34
37
|
/** Callback to remove a managed file by its id. Required when `managedFiles` is provided. */
|
|
35
38
|
onRemoveManagedFile?: (id: string) => void
|
|
39
|
+
/** When provided, an uploaded managed file shows a download action that calls this with its id. */
|
|
40
|
+
onDownloadManagedFile?: (id: string) => void
|
|
36
41
|
/** Accepted MIME types (e.g., "image/*", ".pdf,.doc") */
|
|
37
42
|
accept?: string
|
|
38
43
|
/** Maximum file size in bytes. Default: 10MB */
|
|
@@ -103,6 +108,7 @@ export function FileUpload({
|
|
|
103
108
|
onChange,
|
|
104
109
|
managedFiles,
|
|
105
110
|
onRemoveManagedFile,
|
|
111
|
+
onDownloadManagedFile,
|
|
106
112
|
accept = "*/*",
|
|
107
113
|
maxSize = 10 * 1024 * 1024,
|
|
108
114
|
maxFiles = 10,
|
|
@@ -283,8 +289,9 @@ export function FileUpload({
|
|
|
283
289
|
|
|
284
290
|
const displayError = error || validationError || undefined
|
|
285
291
|
const hasFiles = isManaged ? managedFiles.length > 0 : files.length > 0
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
// Single-file mode hides the dropzone once a file is picked (no more to add);
|
|
293
|
+
// multiple keeps it visible to keep attaching.
|
|
294
|
+
const showDropzone = multiple || !hasFiles
|
|
288
295
|
|
|
289
296
|
return (
|
|
290
297
|
<FieldWrapper label={fieldLabel} error={displayError} className={className}>
|
|
@@ -298,42 +305,14 @@ export function FileUpload({
|
|
|
298
305
|
disabled={disabled}
|
|
299
306
|
/>
|
|
300
307
|
|
|
301
|
-
{/*
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
type="button"
|
|
305
|
-
onDragEnter={handleDrag}
|
|
306
|
-
onDragLeave={handleDrag}
|
|
307
|
-
onDragOver={handleDrag}
|
|
308
|
-
onDrop={handleDrop}
|
|
309
|
-
onClick={openDialog}
|
|
310
|
-
disabled={disabled}
|
|
311
|
-
className={cn(
|
|
312
|
-
"flex w-full items-center gap-2 p-3 rounded-[6px] border border-dashed cursor-pointer text-left",
|
|
313
|
-
"transition-colors duration-200",
|
|
314
|
-
"bg-ods-card border-ods-border",
|
|
315
|
-
dragActive && "border-ods-accent bg-ods-accent/5",
|
|
316
|
-
!dragActive && "hover:border-ods-accent/30",
|
|
317
|
-
disabled && "opacity-50 cursor-not-allowed hover:border-ods-border",
|
|
318
|
-
)}
|
|
319
|
-
>
|
|
320
|
-
<div className="flex items-center p-3 rounded-full bg-ods-card border border-ods-border shrink-0">
|
|
321
|
-
{icon || <ImagePlusIcon className="size-6 text-ods-text-primary" />}
|
|
322
|
-
</div>
|
|
323
|
-
<div className="flex flex-1 flex-col font-['DM_Sans'] font-medium min-w-0">
|
|
324
|
-
<span className="text-heading-4 text-ods-text-primary">{label}</span>
|
|
325
|
-
<span className="text-heading-5 text-ods-text-secondary">{description}</span>
|
|
326
|
-
</div>
|
|
327
|
-
</button>
|
|
328
|
-
)}
|
|
329
|
-
|
|
330
|
-
{/* File list */}
|
|
331
|
-
{hasFiles && (
|
|
332
|
-
<div className="flex flex-col gap-2">
|
|
308
|
+
{/* File list (above) + upload dropzone — single bordered list, file-type icon + name/size + actions per row */}
|
|
309
|
+
<div className="flex flex-col gap-[var(--spacing-system-xsf)]">
|
|
310
|
+
{hasFiles && (
|
|
333
311
|
<div
|
|
334
312
|
className={cn(
|
|
335
|
-
"flex flex-col
|
|
336
|
-
|
|
313
|
+
"flex flex-col rounded-[6px] border border-ods-border bg-ods-card transition-colors duration-200",
|
|
314
|
+
!maxListHeight && "overflow-hidden",
|
|
315
|
+
dragActive && "border-ods-accent",
|
|
337
316
|
)}
|
|
338
317
|
style={maxListHeight ? { maxHeight: typeof maxListHeight === "number" ? `${maxListHeight}px` : maxListHeight, overflowY: "auto" } : undefined}
|
|
339
318
|
>
|
|
@@ -342,39 +321,52 @@ export function FileUpload({
|
|
|
342
321
|
<div
|
|
343
322
|
key={entry.id}
|
|
344
323
|
className={cn(
|
|
345
|
-
"flex items-center gap-
|
|
346
|
-
entry.status === "error"
|
|
324
|
+
"flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] border-b border-ods-border last:border-b-0",
|
|
325
|
+
entry.status === "error" && "bg-ods-error/5",
|
|
347
326
|
)}
|
|
348
327
|
>
|
|
328
|
+
<div className="flex items-center justify-center size-10 rounded-[6px] border border-ods-border bg-ods-card shrink-0">
|
|
329
|
+
<FileIcon className="size-6 text-ods-text-secondary" />
|
|
330
|
+
</div>
|
|
349
331
|
<div className="flex-1 min-w-0">
|
|
350
|
-
<p className="text-heading-
|
|
332
|
+
<p className="text-heading-4 font-medium text-ods-text-primary truncate" title={entry.fileName}>
|
|
351
333
|
{entry.fileName}
|
|
352
334
|
</p>
|
|
353
|
-
<div className="flex items-center gap-
|
|
354
|
-
<span className="text-heading-
|
|
335
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
336
|
+
<span className="text-heading-5 text-ods-text-secondary">
|
|
355
337
|
{formatFileSize(entry.fileSize)}
|
|
356
338
|
</span>
|
|
357
339
|
{entry.status === "uploading" && (
|
|
358
|
-
<span className="flex items-center gap-
|
|
340
|
+
<span className="flex items-center gap-[var(--spacing-system-xxs)] text-heading-5 text-ods-text-secondary">
|
|
359
341
|
<Loader2 className="size-3 animate-spin" />
|
|
360
342
|
Uploading...
|
|
361
343
|
</span>
|
|
362
344
|
)}
|
|
363
345
|
{entry.status === "error" && (
|
|
364
|
-
<span className="text-heading-
|
|
346
|
+
<span className="text-heading-5 text-ods-error">
|
|
365
347
|
{entry.error || "Upload failed"}
|
|
366
348
|
</span>
|
|
367
349
|
)}
|
|
368
350
|
</div>
|
|
369
351
|
</div>
|
|
352
|
+
{onDownloadManagedFile && entry.status === "uploaded" && (
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
onClick={() => onDownloadManagedFile(entry.id)}
|
|
356
|
+
className="shrink-0 text-ods-text-secondary hover:text-ods-text-primary transition-colors"
|
|
357
|
+
aria-label={`Download ${entry.fileName}`}
|
|
358
|
+
>
|
|
359
|
+
<Download02Icon className="size-6" />
|
|
360
|
+
</button>
|
|
361
|
+
)}
|
|
370
362
|
{!disabled && (
|
|
371
363
|
<button
|
|
372
364
|
type="button"
|
|
373
365
|
onClick={() => onRemoveManagedFile?.(entry.id)}
|
|
374
|
-
className="shrink-0
|
|
366
|
+
className="shrink-0 text-ods-error hover:opacity-80 transition-opacity"
|
|
375
367
|
aria-label={`Remove ${entry.fileName}`}
|
|
376
368
|
>
|
|
377
|
-
<
|
|
369
|
+
<TrashIcon className="size-6" />
|
|
378
370
|
</button>
|
|
379
371
|
)}
|
|
380
372
|
</div>
|
|
@@ -382,13 +374,16 @@ export function FileUpload({
|
|
|
382
374
|
: files.map((file, index) => (
|
|
383
375
|
<div
|
|
384
376
|
key={`${file.name}-${index}`}
|
|
385
|
-
className="flex items-center gap-
|
|
377
|
+
className="flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] border-b border-ods-border last:border-b-0"
|
|
386
378
|
>
|
|
379
|
+
<div className="flex items-center justify-center size-10 rounded-[6px] border border-ods-border bg-ods-card shrink-0">
|
|
380
|
+
<FileIcon className="size-6 text-ods-text-secondary" />
|
|
381
|
+
</div>
|
|
387
382
|
<div className="flex-1 min-w-0">
|
|
388
|
-
<p className="text-heading-
|
|
383
|
+
<p className="text-heading-4 font-medium text-ods-text-primary truncate" title={file.name}>
|
|
389
384
|
{file.name}
|
|
390
385
|
</p>
|
|
391
|
-
<p className="text-heading-
|
|
386
|
+
<p className="text-heading-5 text-ods-text-secondary">
|
|
392
387
|
{formatFileSize(file.size)}
|
|
393
388
|
</p>
|
|
394
389
|
</div>
|
|
@@ -396,43 +391,46 @@ export function FileUpload({
|
|
|
396
391
|
<button
|
|
397
392
|
type="button"
|
|
398
393
|
onClick={() => handleRemoveFile(index)}
|
|
399
|
-
className="shrink-0
|
|
394
|
+
className="shrink-0 text-ods-error hover:opacity-80 transition-opacity"
|
|
400
395
|
aria-label={`Remove ${file.name}`}
|
|
401
396
|
>
|
|
402
|
-
<
|
|
397
|
+
<TrashIcon className="size-6" />
|
|
403
398
|
</button>
|
|
404
399
|
)}
|
|
405
400
|
</div>
|
|
406
401
|
))
|
|
407
402
|
}
|
|
408
403
|
</div>
|
|
404
|
+
)}
|
|
409
405
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
? "border-ods-accent bg-ods-accent/5 text-ods-text-primary"
|
|
427
|
-
: "border-ods-border text-ods-text-secondary hover:border-ods-accent/30 hover:text-ods-text-primary",
|
|
428
|
-
disabled && "opacity-50 cursor-not-allowed",
|
|
429
|
-
)}
|
|
430
|
-
>
|
|
431
|
-
Add more files
|
|
432
|
-
</button>
|
|
406
|
+
{showDropzone && (
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
onDragEnter={handleDrag}
|
|
410
|
+
onDragLeave={handleDrag}
|
|
411
|
+
onDragOver={handleDrag}
|
|
412
|
+
onDrop={handleDrop}
|
|
413
|
+
onClick={openDialog}
|
|
414
|
+
disabled={disabled}
|
|
415
|
+
className={cn(
|
|
416
|
+
"flex w-full items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-sf)] rounded-[6px] border border-dashed cursor-pointer text-left",
|
|
417
|
+
"transition-colors duration-200",
|
|
418
|
+
"bg-ods-card border-ods-border",
|
|
419
|
+
dragActive && "border-ods-accent bg-ods-accent/5",
|
|
420
|
+
!dragActive && "hover:border-ods-accent/30",
|
|
421
|
+
disabled && "opacity-50 cursor-not-allowed hover:border-ods-border",
|
|
433
422
|
)}
|
|
434
|
-
|
|
435
|
-
|
|
423
|
+
>
|
|
424
|
+
<div className="flex items-center p-[var(--spacing-system-sf)] rounded-full bg-ods-card border border-ods-border shrink-0">
|
|
425
|
+
{icon || <ImagePlusIcon className="size-6 text-ods-text-primary" />}
|
|
426
|
+
</div>
|
|
427
|
+
<div className="flex flex-1 flex-col font-['DM_Sans'] font-medium min-w-0">
|
|
428
|
+
<span className="text-heading-4 text-ods-text-primary">{label}</span>
|
|
429
|
+
<span className="text-heading-5 text-ods-text-secondary">{description}</span>
|
|
430
|
+
</div>
|
|
431
|
+
</button>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
436
434
|
</FieldWrapper>
|
|
437
435
|
)
|
|
438
436
|
}
|
|
@@ -41,6 +41,11 @@ export interface ImageUploaderProps {
|
|
|
41
41
|
alt?: string
|
|
42
42
|
/** Extra className on the FieldWrapper root. */
|
|
43
43
|
className?: string
|
|
44
|
+
/**
|
|
45
|
+
* Extra className on the dropzone element itself. Merged after the defaults, so it can
|
|
46
|
+
* override the built-in sizing (e.g. pass `h-[148px]` to replace the default `h-44`).
|
|
47
|
+
*/
|
|
48
|
+
dropzoneClassName?: string
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const DEFAULT_MAX_SIZE = 25 * 1024 * 1024
|
|
@@ -85,6 +90,7 @@ export function ImageUploader({
|
|
|
85
90
|
objectFit = "cover",
|
|
86
91
|
alt = "Uploaded image",
|
|
87
92
|
className,
|
|
93
|
+
dropzoneClassName,
|
|
88
94
|
}: ImageUploaderProps) {
|
|
89
95
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
90
96
|
const [dragActive, setDragActive] = React.useState(false)
|
|
@@ -183,6 +189,7 @@ export function ImageUploader({
|
|
|
183
189
|
!hasImage && interactive && !isActionState && "cursor-pointer hover:bg-ods-bg-hover hover:border-ods-border-hover",
|
|
184
190
|
!interactive && "opacity-60",
|
|
185
191
|
!hasImage && !interactive && "cursor-not-allowed",
|
|
192
|
+
dropzoneClassName,
|
|
186
193
|
)}
|
|
187
194
|
>
|
|
188
195
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { cn } from '../../utils/cn'
|
|
6
|
+
import { CheckIcon, PlusIcon, SearchIcon, TrashIcon } from '../icons-v2-generated'
|
|
7
|
+
import { Button } from './button'
|
|
8
|
+
import { Input } from './input'
|
|
9
|
+
|
|
10
|
+
export interface TagSelectOption {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TagSelectDropdownProps {
|
|
16
|
+
/** All selectable tags. */
|
|
17
|
+
options: TagSelectOption[]
|
|
18
|
+
/** IDs of the currently selected tags. */
|
|
19
|
+
selectedIds: string[]
|
|
20
|
+
/** Called with the next full selection whenever a tag is toggled. */
|
|
21
|
+
onChange: (ids: string[]) => void
|
|
22
|
+
/** Called when the inline "Create" row is chosen. Omit to disable creation. */
|
|
23
|
+
onCreate?: (name: string) => void
|
|
24
|
+
/** Max length for a newly created tag name. Omit for no limit. */
|
|
25
|
+
maxCreateLength?: number
|
|
26
|
+
/** Called when a row's hover trash button is clicked. Omit to hide delete. */
|
|
27
|
+
onDelete?: (id: string) => void
|
|
28
|
+
isCreating?: boolean
|
|
29
|
+
isDeleting?: boolean
|
|
30
|
+
/** Trigger button label. */
|
|
31
|
+
triggerLabel?: string
|
|
32
|
+
searchPlaceholder?: string
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
align?: 'start' | 'center' | 'end'
|
|
35
|
+
/** Forwarded to the trigger button. */
|
|
36
|
+
className?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Multi-select tag picker rendered as a button-triggered dropdown (the
|
|
41
|
+
* multi-select sibling of the compact `AssigneeDropdown`): a trigger opens a
|
|
42
|
+
* popover with an internal search field, a checkable option list, and an
|
|
43
|
+
* inline "Create" row. Selected chips are rendered by the caller.
|
|
44
|
+
*/
|
|
45
|
+
export function TagSelectDropdown({
|
|
46
|
+
options,
|
|
47
|
+
selectedIds,
|
|
48
|
+
onChange,
|
|
49
|
+
onCreate,
|
|
50
|
+
maxCreateLength,
|
|
51
|
+
onDelete,
|
|
52
|
+
isCreating = false,
|
|
53
|
+
isDeleting = false,
|
|
54
|
+
triggerLabel = 'Add Tags',
|
|
55
|
+
searchPlaceholder = 'Search or create tags...',
|
|
56
|
+
disabled = false,
|
|
57
|
+
align = 'start',
|
|
58
|
+
className,
|
|
59
|
+
}: TagSelectDropdownProps) {
|
|
60
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
61
|
+
const [search, setSearch] = React.useState('')
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!isOpen) setSearch('')
|
|
65
|
+
}, [isOpen])
|
|
66
|
+
|
|
67
|
+
const query = search.trim()
|
|
68
|
+
const filtered = React.useMemo(() => {
|
|
69
|
+
const q = query.toLowerCase()
|
|
70
|
+
if (!q) return options
|
|
71
|
+
return options.filter(o => o.label.toLowerCase().includes(q))
|
|
72
|
+
}, [options, query])
|
|
73
|
+
|
|
74
|
+
// Selected tags float to the top of the list.
|
|
75
|
+
const ordered = React.useMemo(() => {
|
|
76
|
+
const selected = filtered.filter(o => selectedIds.includes(o.id))
|
|
77
|
+
const rest = filtered.filter(o => !selectedIds.includes(o.id))
|
|
78
|
+
return [...selected, ...rest]
|
|
79
|
+
}, [filtered, selectedIds])
|
|
80
|
+
|
|
81
|
+
const showCreate = !!onCreate && !!query && !options.some(o => o.label.toLowerCase() === query.toLowerCase())
|
|
82
|
+
const isCreateTooLong = maxCreateLength != null && query.length > maxCreateLength
|
|
83
|
+
const canCreate = showCreate && !isCreateTooLong
|
|
84
|
+
|
|
85
|
+
const toggle = (id: string) => {
|
|
86
|
+
onChange(selectedIds.includes(id) ? selectedIds.filter(i => i !== id) : [...selectedIds, id])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleCreate = () => {
|
|
90
|
+
if (!onCreate || !canCreate) return
|
|
91
|
+
onCreate(query)
|
|
92
|
+
setSearch('')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PopoverPrimitive.Root open={isOpen} onOpenChange={setIsOpen} modal={false}>
|
|
97
|
+
<PopoverPrimitive.Trigger asChild>
|
|
98
|
+
<Button
|
|
99
|
+
type="button"
|
|
100
|
+
variant="outline"
|
|
101
|
+
size="small"
|
|
102
|
+
leftIcon={<PlusIcon />}
|
|
103
|
+
disabled={disabled}
|
|
104
|
+
className={cn('w-fit focus-visible:ring-0', className)}
|
|
105
|
+
>
|
|
106
|
+
{triggerLabel}
|
|
107
|
+
</Button>
|
|
108
|
+
</PopoverPrimitive.Trigger>
|
|
109
|
+
<PopoverPrimitive.Portal>
|
|
110
|
+
<PopoverPrimitive.Content
|
|
111
|
+
align={align}
|
|
112
|
+
sideOffset={6}
|
|
113
|
+
className={cn(
|
|
114
|
+
'z-50 w-72 bg-ods-card border border-ods-border rounded-[6px] shadow-lg overflow-hidden',
|
|
115
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
116
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
117
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
118
|
+
'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<div className="border-b border-ods-border">
|
|
122
|
+
<Input
|
|
123
|
+
autoFocus
|
|
124
|
+
value={search}
|
|
125
|
+
onChange={e => setSearch(e.target.value)}
|
|
126
|
+
onKeyDown={e => {
|
|
127
|
+
if (e.key === 'Enter' && canCreate) {
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
handleCreate()
|
|
130
|
+
}
|
|
131
|
+
}}
|
|
132
|
+
placeholder={searchPlaceholder}
|
|
133
|
+
startAdornment={<SearchIcon className="size-4 text-ods-text-secondary" />}
|
|
134
|
+
className="rounded-none border-0"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="max-h-72 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
138
|
+
{ordered.map(opt => {
|
|
139
|
+
const isSelected = selectedIds.includes(opt.id)
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
key={opt.id}
|
|
143
|
+
role="option"
|
|
144
|
+
aria-selected={isSelected}
|
|
145
|
+
tabIndex={0}
|
|
146
|
+
onClick={() => toggle(opt.id)}
|
|
147
|
+
onKeyDown={e => {
|
|
148
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
toggle(opt.id)
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
className={cn(
|
|
154
|
+
'group/item flex items-center gap-[var(--spacing-system-xs)] w-full cursor-pointer text-left',
|
|
155
|
+
'px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)]',
|
|
156
|
+
'hover:bg-ods-bg-hover transition-colors',
|
|
157
|
+
isSelected && 'bg-ods-bg-hover',
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
<span
|
|
161
|
+
className={cn('flex-1 truncate text-h4', isSelected ? 'text-ods-accent' : 'text-ods-text-primary')}
|
|
162
|
+
title={opt.label}
|
|
163
|
+
>
|
|
164
|
+
{opt.label}
|
|
165
|
+
</span>
|
|
166
|
+
{isSelected && <CheckIcon className="size-4 shrink-0 text-ods-accent" />}
|
|
167
|
+
{onDelete && !isSelected && (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
aria-label={`Delete tag ${opt.label}`}
|
|
171
|
+
onClick={e => {
|
|
172
|
+
e.stopPropagation()
|
|
173
|
+
onDelete(opt.id)
|
|
174
|
+
}}
|
|
175
|
+
disabled={isDeleting}
|
|
176
|
+
className={cn(
|
|
177
|
+
'shrink-0 opacity-0 transition-opacity group-hover/item:opacity-100 focus-visible:opacity-100',
|
|
178
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
<TrashIcon className="size-4 text-ods-error" />
|
|
182
|
+
</button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
})}
|
|
187
|
+
|
|
188
|
+
{showCreate &&
|
|
189
|
+
(isCreateTooLong ? (
|
|
190
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-h5 text-ods-error">
|
|
191
|
+
Maximum {maxCreateLength} characters
|
|
192
|
+
</div>
|
|
193
|
+
) : (
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={handleCreate}
|
|
197
|
+
disabled={isCreating}
|
|
198
|
+
className={cn(
|
|
199
|
+
'flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
200
|
+
'hover:bg-ods-bg-hover transition-colors disabled:opacity-50 disabled:pointer-events-none',
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
<PlusIcon className="size-4 shrink-0 text-ods-accent" />
|
|
204
|
+
<span className="flex-1 truncate text-h4 text-ods-accent" title={`Create "${query}"`}>
|
|
205
|
+
Create “{query}”
|
|
206
|
+
</span>
|
|
207
|
+
</button>
|
|
208
|
+
))}
|
|
209
|
+
|
|
210
|
+
{filtered.length === 0 && !showCreate && (
|
|
211
|
+
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
212
|
+
No tags found
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</PopoverPrimitive.Content>
|
|
217
|
+
</PopoverPrimitive.Portal>
|
|
218
|
+
</PopoverPrimitive.Root>
|
|
219
|
+
)
|
|
220
|
+
}
|