@cosxai/ui 1.0.0-alpha.1 → 1.0.0-alpha.10
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/CHANGELOG.md +89 -0
- package/package.json +10 -4
- package/src/brand.css +1 -1
- package/src/components/ActionBar.tsx +607 -0
- package/src/components/ActivityTimeline.tsx +77 -0
- package/src/components/AsyncSelect.tsx +205 -0
- package/src/components/AuthLayout.tsx +127 -0
- package/src/components/BrandLoader.tsx +122 -0
- package/src/components/Breadcrumb.tsx +129 -0
- package/src/components/Button.tsx +48 -40
- package/src/components/ChoiceCards.tsx +68 -0
- package/src/components/CodeInput.tsx +150 -0
- package/src/components/CommandPalette.tsx +228 -0
- package/src/components/ConfirmDialog.tsx +294 -0
- package/src/components/CopyField.tsx +104 -0
- package/src/components/DateInput.tsx +366 -0
- package/src/components/Dialog.tsx +27 -3
- package/src/components/Dropzone.tsx +187 -0
- package/src/components/FileCard.tsx +233 -0
- package/src/components/FileRow.tsx +159 -0
- package/src/components/FolderTree.tsx +377 -0
- package/src/components/ListToolbar.tsx +177 -0
- package/src/components/MentionInput.tsx +233 -0
- package/src/components/Menu.tsx +109 -0
- package/src/components/PageHeader.tsx +62 -0
- package/src/components/PageState.tsx +84 -0
- package/src/components/Progress.tsx +97 -0
- package/src/components/PulseDot.tsx +31 -0
- package/src/components/RecipientsInput.tsx +252 -0
- package/src/components/SearchField.tsx +137 -0
- package/src/components/SegmentedControl.tsx +95 -0
- package/src/components/SidePanel.tsx +130 -0
- package/src/components/Skeleton.tsx +73 -0
- package/src/components/StageProgress.tsx +51 -0
- package/src/components/Steps.tsx +58 -0
- package/src/components/SyncStatus.tsx +56 -0
- package/src/components/ThemeSwitch.tsx +99 -0
- package/src/components/Toast.tsx +45 -8
- package/src/components/UploadCheck.tsx +118 -0
- package/src/components/UploadList.tsx +176 -0
- package/src/components/VirtualList.tsx +90 -0
- package/src/components/WindowDrop.tsx +131 -0
- package/src/components/WorkspaceRow.tsx +80 -0
- package/src/components/inputs-aria.ts +5 -0
- package/src/components/inputs-date.ts +92 -0
- package/src/components/inputs-listbox.tsx +154 -0
- package/src/components/inputs-search.ts +60 -0
- package/src/components/status-files.ts +124 -0
- package/src/components/status-motion.ts +30 -0
- package/src/components/useSelection.ts +142 -0
- package/src/index.ts +45 -1
- package/src/theme-base.css +123 -0
- package/src/theme-reset.css +14 -0
- package/src/theme.css +2 -125
- package/src/components/core.test.tsx +0 -242
- package/src/components/forms.test.tsx +0 -280
- package/src/components/overlays.test.tsx +0 -245
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
2
|
+
import { Check, FileText, FileX, Folder, Link2, MessageSquare, Star, type LucideIcon } from 'lucide-react';
|
|
3
|
+
import type { ComponentProps, MouseEvent, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn';
|
|
6
|
+
import { Badge, type BadgeStatus } from './Badge';
|
|
7
|
+
import { Spinner } from './Spinner';
|
|
8
|
+
import { Tag } from './Tag';
|
|
9
|
+
|
|
10
|
+
export type FileCardState = 'ready' | 'converting' | 'failed';
|
|
11
|
+
|
|
12
|
+
export type FileCardProps = Omit<ComponentProps<'div'>, 'title' | 'onClick'> & {
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
/** file · folder · link (a card that points elsewhere: dashed edge). @default "file" */
|
|
15
|
+
kind?: 'file' | 'folder' | 'link' | undefined;
|
|
16
|
+
/** The thumbnail. Without one the icon sits on linen. */
|
|
17
|
+
preview?: ReactNode;
|
|
18
|
+
/** Icon when there is no preview. Defaults by kind. */
|
|
19
|
+
icon?: LucideIcon | undefined;
|
|
20
|
+
/** Format tag: PDF, Word. */
|
|
21
|
+
type?: ReactNode;
|
|
22
|
+
/** Status in words; `status` picks the badge. */
|
|
23
|
+
statusLabel?: ReactNode;
|
|
24
|
+
status?: BadgeStatus | undefined;
|
|
25
|
+
/** The last line: "Updated 2 hours ago", "in Harbour / Site / 2026". */
|
|
26
|
+
meta?: ReactNode;
|
|
27
|
+
/** Comment count after the status; hidden at zero. */
|
|
28
|
+
comments?: number | undefined;
|
|
29
|
+
/** A small source label on the preview: "Dropbox". */
|
|
30
|
+
source?: ReactNode;
|
|
31
|
+
/** ready · converting (spinner and label) · failed (red wash, a Re-render action). */
|
|
32
|
+
state?: FileCardState | undefined;
|
|
33
|
+
stateLabel?: ReactNode;
|
|
34
|
+
onRerender?: (() => void) | undefined;
|
|
35
|
+
/** @default "Re-render" */
|
|
36
|
+
rerenderLabel?: string | undefined;
|
|
37
|
+
/** Link cards: the Remove link action beside the target. */
|
|
38
|
+
onRemoveLink?: (() => void) | undefined;
|
|
39
|
+
/** @default "Remove link" */
|
|
40
|
+
removeLinkLabel?: string | undefined;
|
|
41
|
+
|
|
42
|
+
selected?: boolean | undefined;
|
|
43
|
+
/** In selection mode the whole card toggles instead of opening. */
|
|
44
|
+
selectionMode?: boolean | undefined;
|
|
45
|
+
onSelectedChange?: ((selected: boolean) => void) | undefined;
|
|
46
|
+
/** Accessible name of the tick box. @default "Select" */
|
|
47
|
+
selectLabel?: string | undefined;
|
|
48
|
+
|
|
49
|
+
starred?: boolean | undefined;
|
|
50
|
+
onStarredChange?: ((starred: boolean) => void) | undefined;
|
|
51
|
+
/** @default "Star" */
|
|
52
|
+
starLabel?: string | undefined;
|
|
53
|
+
|
|
54
|
+
/** Opens the item (not in selection mode). */
|
|
55
|
+
onOpen?: (() => void) | undefined;
|
|
56
|
+
/** Every press of the card, with its modifiers — wire useSelection.handleClick here; replaces the default open / toggle. */
|
|
57
|
+
onPress?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const KIND_ICON: Record<NonNullable<FileCardProps['kind']>, LucideIcon> = { file: FileText, folder: Folder, link: Link2 };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* FileCard — the library card shared by every grid. The title is the one
|
|
64
|
+
* real button, stretched over the card; the tick box, the star and inline
|
|
65
|
+
* actions sit above it and never open the card. In selection mode the
|
|
66
|
+
* whole card is the checkbox.
|
|
67
|
+
*/
|
|
68
|
+
export function FileCard({
|
|
69
|
+
title,
|
|
70
|
+
kind = 'file',
|
|
71
|
+
preview,
|
|
72
|
+
icon,
|
|
73
|
+
type,
|
|
74
|
+
statusLabel,
|
|
75
|
+
status,
|
|
76
|
+
meta,
|
|
77
|
+
comments,
|
|
78
|
+
source,
|
|
79
|
+
state = 'ready',
|
|
80
|
+
stateLabel,
|
|
81
|
+
onRerender,
|
|
82
|
+
rerenderLabel = 'Re-render',
|
|
83
|
+
onRemoveLink,
|
|
84
|
+
removeLinkLabel = 'Remove link',
|
|
85
|
+
selected = false,
|
|
86
|
+
selectionMode = false,
|
|
87
|
+
onSelectedChange,
|
|
88
|
+
selectLabel = 'Select',
|
|
89
|
+
starred,
|
|
90
|
+
onStarredChange,
|
|
91
|
+
starLabel = 'Star',
|
|
92
|
+
onOpen,
|
|
93
|
+
onPress,
|
|
94
|
+
className,
|
|
95
|
+
...rest
|
|
96
|
+
}: FileCardProps) {
|
|
97
|
+
const Glyph = icon ?? KIND_ICON[kind];
|
|
98
|
+
const press = (e: MouseEvent<HTMLButtonElement>) => {
|
|
99
|
+
if (onPress) onPress(e);
|
|
100
|
+
else if (selectionMode) onSelectedChange?.(!selected);
|
|
101
|
+
else onOpen?.();
|
|
102
|
+
};
|
|
103
|
+
const showTick = Boolean(onSelectedChange) && (selectionMode || selected);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
data-selected={selected || undefined}
|
|
108
|
+
className={cn(
|
|
109
|
+
'group relative flex flex-col overflow-hidden rounded-lg bg-page text-fg',
|
|
110
|
+
'transition-[border-color] duration-[120ms] ease-standard',
|
|
111
|
+
kind === 'link' ? 'border border-dashed border-rule' : 'border border-rule',
|
|
112
|
+
selected ? 'border-2 border-fg' : 'hover:border-fg focus-within:border-fg',
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
{...rest}
|
|
116
|
+
>
|
|
117
|
+
{/* Preview */}
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
'relative grid h-[180px] place-items-center text-fg',
|
|
121
|
+
selected ? 'bg-brand-field text-ink' : state === 'failed' ? 'bg-error-wash text-ink ink:bg-error/20 ink:text-fg' : kind === 'link' ? 'bg-page' : 'bg-sunk',
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{state === 'converting' ? (
|
|
125
|
+
<div className="flex flex-col items-center gap-2 text-small text-fg-secondary">
|
|
126
|
+
<Spinner size={16} />
|
|
127
|
+
{stateLabel}
|
|
128
|
+
</div>
|
|
129
|
+
) : state === 'failed' ? (
|
|
130
|
+
<div className="flex flex-col items-center gap-2 text-small">
|
|
131
|
+
<FileX size={20} strokeWidth={1.75} aria-hidden />
|
|
132
|
+
<span>{stateLabel}</span>
|
|
133
|
+
{onRerender && (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={onRerender}
|
|
137
|
+
className="relative z-10 cursor-pointer font-semibold underline underline-offset-2 outline-none focus-visible:shadow-(--focus-ring)"
|
|
138
|
+
>
|
|
139
|
+
{rerenderLabel}
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
(preview ?? <Glyph size={32} strokeWidth={1.5} aria-hidden />)
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{source && !showTick && (
|
|
148
|
+
<span className="absolute top-2.5 left-2.5 inline-flex items-center gap-1.5 rounded-sm bg-page px-1.5 py-1 text-[11px] font-semibold text-fg">
|
|
149
|
+
<span aria-hidden className="size-1.5 rounded-pill bg-brand-mark" />
|
|
150
|
+
{source}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{onSelectedChange && (
|
|
155
|
+
<CheckboxPrimitive.Root
|
|
156
|
+
checked={selected}
|
|
157
|
+
onCheckedChange={(v) => onSelectedChange(v === true)}
|
|
158
|
+
aria-label={selectLabel}
|
|
159
|
+
className={cn(
|
|
160
|
+
'absolute top-2.5 left-2.5 z-10 grid size-5 cursor-pointer place-items-center rounded-xs border border-rule bg-page outline-none',
|
|
161
|
+
'data-[state=checked]:border-fg data-[state=checked]:bg-fg data-[state=checked]:text-page focus-visible:shadow-(--focus-ring)',
|
|
162
|
+
showTick ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
<CheckboxPrimitive.Indicator>
|
|
166
|
+
<Check size={13} strokeWidth={2.5} aria-hidden />
|
|
167
|
+
</CheckboxPrimitive.Indicator>
|
|
168
|
+
</CheckboxPrimitive.Root>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{onStarredChange && (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
aria-label={starLabel}
|
|
175
|
+
aria-pressed={Boolean(starred)}
|
|
176
|
+
onClick={() => onStarredChange(!starred)}
|
|
177
|
+
className={cn(
|
|
178
|
+
'absolute top-2 right-2 z-10 grid size-7 cursor-pointer place-items-center rounded-md text-fg outline-none hover:bg-hover focus-visible:shadow-(--focus-ring)',
|
|
179
|
+
starred ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
<Star size={15} strokeWidth={1.75} aria-hidden className={starred ? 'fill-brand-mark text-fg' : undefined} />
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Body */}
|
|
188
|
+
<div className="flex min-w-0 flex-col gap-1.5 px-3.5 pt-3 pb-3.5">
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={press}
|
|
192
|
+
aria-pressed={selectionMode ? selected : undefined}
|
|
193
|
+
className={cn(
|
|
194
|
+
'min-w-0 cursor-pointer truncate text-left text-ui font-medium text-fg outline-none',
|
|
195
|
+
// Stretched: the whole card is this button's target.
|
|
196
|
+
'after:absolute after:inset-0 after:content-[""] focus-visible:after:rounded-lg focus-visible:after:shadow-(--focus-ring)',
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{title}
|
|
200
|
+
</button>
|
|
201
|
+
{(type || statusLabel || (comments ?? 0) > 0) && (
|
|
202
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
203
|
+
{type && <Tag className="px-1.5 py-0.5 font-semibold text-fg">{type}</Tag>}
|
|
204
|
+
{statusLabel && <Badge status={status}>{statusLabel}</Badge>}
|
|
205
|
+
{(comments ?? 0) > 0 && (
|
|
206
|
+
<span className="ml-auto inline-flex items-center gap-1 text-meta text-fg-secondary">
|
|
207
|
+
<MessageSquare size={13} strokeWidth={1.75} aria-hidden />
|
|
208
|
+
<span className="tabular-nums">{comments}</span>
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
{(meta || onRemoveLink) && (
|
|
214
|
+
<div className="flex min-w-0 items-center gap-1 text-meta text-fg-secondary">
|
|
215
|
+
{meta && <span className="truncate">{meta}</span>}
|
|
216
|
+
{onRemoveLink && (
|
|
217
|
+
<>
|
|
218
|
+
{meta && <span aria-hidden>·</span>}
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onRemoveLink}
|
|
222
|
+
className="relative z-10 cursor-pointer text-fg-secondary outline-none hover:text-fg hover:underline focus-visible:shadow-(--focus-ring)"
|
|
223
|
+
>
|
|
224
|
+
{removeLinkLabel}
|
|
225
|
+
</button>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
2
|
+
import { Check, FileText, Star, type LucideIcon } from 'lucide-react';
|
|
3
|
+
import type { ComponentProps, MouseEvent, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn';
|
|
6
|
+
import { Badge, type BadgeStatus } from './Badge';
|
|
7
|
+
|
|
8
|
+
/** The shared column layout of a file list: [tick] icon · name · state · updated · [actions]. */
|
|
9
|
+
const GRID = 'grid grid-cols-[20px_minmax(0,1fr)_minmax(96px,120px)_minmax(96px,160px)_32px] items-center gap-x-3';
|
|
10
|
+
const GRID_SELECTABLE = 'grid grid-cols-[20px_20px_minmax(0,1fr)_minmax(96px,120px)_minmax(96px,160px)_32px] items-center gap-x-3';
|
|
11
|
+
|
|
12
|
+
export type FileListProps = ComponentProps<'div'> & {
|
|
13
|
+
/** Column headings: name, state, updated. Omit for a headless list. */
|
|
14
|
+
columns?: { name: ReactNode; state: ReactNode; updated: ReactNode } | undefined;
|
|
15
|
+
/** Leave room for the tick column. */
|
|
16
|
+
selectable?: boolean | undefined;
|
|
17
|
+
/** Accessible name of the list. */
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** FileList — the row container: optional headings, then FileRows. */
|
|
22
|
+
export function FileList({ columns, selectable = false, label, className, children, ...rest }: FileListProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('flex flex-col', className)} {...rest}>
|
|
25
|
+
{columns && (
|
|
26
|
+
<div aria-hidden className={cn(selectable ? GRID_SELECTABLE : GRID, 'border-b border-rule px-3 pb-2 text-meta font-medium text-fg-secondary')}>
|
|
27
|
+
{selectable && <span />}
|
|
28
|
+
<span />
|
|
29
|
+
<span>{columns.name}</span>
|
|
30
|
+
<span>{columns.state}</span>
|
|
31
|
+
<span>{columns.updated}</span>
|
|
32
|
+
<span />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<ul aria-label={label} className="m-0 flex list-none flex-col p-0">
|
|
36
|
+
{children}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FileRowProps = Omit<ComponentProps<'li'>, 'title' | 'onClick'> & {
|
|
43
|
+
name: ReactNode;
|
|
44
|
+
icon?: LucideIcon | undefined;
|
|
45
|
+
statusLabel?: ReactNode;
|
|
46
|
+
status?: BadgeStatus | undefined;
|
|
47
|
+
/** "2 hours ago" — or, in search results, the location: "in Legal / 2026". */
|
|
48
|
+
updated?: ReactNode;
|
|
49
|
+
/** A wash for rows that need someone (attention) or are overdue (error). */
|
|
50
|
+
tone?: 'attention' | 'error' | undefined;
|
|
51
|
+
/** Match the FileList's selectable. */
|
|
52
|
+
selectable?: boolean | undefined;
|
|
53
|
+
selected?: boolean | undefined;
|
|
54
|
+
selectionMode?: boolean | undefined;
|
|
55
|
+
onSelectedChange?: ((selected: boolean) => void) | undefined;
|
|
56
|
+
/** @default "Select" */
|
|
57
|
+
selectLabel?: string | undefined;
|
|
58
|
+
starred?: boolean | undefined;
|
|
59
|
+
onStarredChange?: ((starred: boolean) => void) | undefined;
|
|
60
|
+
/** @default "Star" */
|
|
61
|
+
starLabel?: string | undefined;
|
|
62
|
+
onOpen?: (() => void) | undefined;
|
|
63
|
+
/** Every press with its modifiers — wire useSelection.handleClick; replaces the default open / toggle. */
|
|
64
|
+
onPress?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* FileRow — a list row: icon, name, state, updated or location, a star on
|
|
69
|
+
* hover. Like FileCard, the name is the one button stretched over the row;
|
|
70
|
+
* the tick box and the star sit above it and never open the row.
|
|
71
|
+
*/
|
|
72
|
+
export function FileRow({
|
|
73
|
+
name,
|
|
74
|
+
icon: Glyph = FileText,
|
|
75
|
+
statusLabel,
|
|
76
|
+
status,
|
|
77
|
+
updated,
|
|
78
|
+
tone,
|
|
79
|
+
selectable = false,
|
|
80
|
+
selected = false,
|
|
81
|
+
selectionMode = false,
|
|
82
|
+
onSelectedChange,
|
|
83
|
+
selectLabel = 'Select',
|
|
84
|
+
starred,
|
|
85
|
+
onStarredChange,
|
|
86
|
+
starLabel = 'Star',
|
|
87
|
+
onOpen,
|
|
88
|
+
onPress,
|
|
89
|
+
className,
|
|
90
|
+
...rest
|
|
91
|
+
}: FileRowProps) {
|
|
92
|
+
const press = (e: MouseEvent<HTMLButtonElement>) => {
|
|
93
|
+
if (onPress) onPress(e);
|
|
94
|
+
else if (selectionMode) onSelectedChange?.(!selected);
|
|
95
|
+
else onOpen?.();
|
|
96
|
+
};
|
|
97
|
+
return (
|
|
98
|
+
<li
|
|
99
|
+
data-selected={selected || undefined}
|
|
100
|
+
className={cn(
|
|
101
|
+
selectable ? GRID_SELECTABLE : GRID,
|
|
102
|
+
'group relative min-h-[42px] border-b border-rule-soft px-3 py-2 text-ui text-fg',
|
|
103
|
+
selected
|
|
104
|
+
? 'bg-brand-field text-ink'
|
|
105
|
+
: tone === 'attention'
|
|
106
|
+
? 'bg-yellow-wash-32 ink:bg-yellow-accent/15'
|
|
107
|
+
: tone === 'error'
|
|
108
|
+
? 'bg-error-wash ink:bg-error/20'
|
|
109
|
+
: 'hover:bg-hover',
|
|
110
|
+
className,
|
|
111
|
+
)}
|
|
112
|
+
{...rest}
|
|
113
|
+
>
|
|
114
|
+
{selectable && (
|
|
115
|
+
<CheckboxPrimitive.Root
|
|
116
|
+
checked={selected}
|
|
117
|
+
onCheckedChange={(v) => onSelectedChange?.(v === true)}
|
|
118
|
+
aria-label={selectLabel}
|
|
119
|
+
className={cn(
|
|
120
|
+
'relative z-10 grid size-4 cursor-pointer place-items-center rounded-xs border border-rule bg-page outline-none',
|
|
121
|
+
'data-[state=checked]:border-fg data-[state=checked]:bg-fg data-[state=checked]:text-page focus-visible:shadow-(--focus-ring)',
|
|
122
|
+
selectionMode || selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
<CheckboxPrimitive.Indicator>
|
|
126
|
+
<Check size={12} strokeWidth={2.5} aria-hidden />
|
|
127
|
+
</CheckboxPrimitive.Indicator>
|
|
128
|
+
</CheckboxPrimitive.Root>
|
|
129
|
+
)}
|
|
130
|
+
<Glyph size={16} strokeWidth={1.75} aria-hidden className="text-fg" />
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={press}
|
|
134
|
+
aria-pressed={selectionMode ? selected : undefined}
|
|
135
|
+
className="min-w-0 cursor-pointer truncate text-left outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:shadow-(--focus-ring)"
|
|
136
|
+
>
|
|
137
|
+
{name}
|
|
138
|
+
</button>
|
|
139
|
+
<span className="min-w-0">{statusLabel ? <Badge status={status}>{statusLabel}</Badge> : null}</span>
|
|
140
|
+
<span className={cn('min-w-0 truncate text-small', selected ? 'text-fg-on-yellow-secondary' : 'text-fg-secondary')}>{updated}</span>
|
|
141
|
+
<span className="flex justify-end">
|
|
142
|
+
{onStarredChange && (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
aria-label={starLabel}
|
|
146
|
+
aria-pressed={Boolean(starred)}
|
|
147
|
+
onClick={() => onStarredChange(!starred)}
|
|
148
|
+
className={cn(
|
|
149
|
+
'relative z-10 grid size-7 cursor-pointer place-items-center rounded-md outline-none hover:bg-hover focus-visible:shadow-(--focus-ring)',
|
|
150
|
+
starred ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<Star size={15} strokeWidth={1.75} aria-hidden className={starred ? 'fill-brand-mark' : undefined} />
|
|
154
|
+
</button>
|
|
155
|
+
)}
|
|
156
|
+
</span>
|
|
157
|
+
</li>
|
|
158
|
+
);
|
|
159
|
+
}
|