@lovett/ui 0.0.11 → 0.2.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/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +5574 -57
- package/dist/index.js +21650 -11206
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1570 -0
- package/dist/tokens.css +112 -0
- package/package.json +8 -1
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +307 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +156 -2
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/sortable.test.tsx +394 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +369 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +611 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +301 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +290 -19
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +303 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +43 -1
- package/src/lib/layer-stack.ts +30 -3
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/modal.tsx +42 -18
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/sortable.tsx +520 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1570 -0
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +742 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +967 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +232 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +112 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byline action cluster — `↩ Reply` and a `•••` overflow, at the inline-end
|
|
3
|
+
* of the byline row.
|
|
4
|
+
*
|
|
5
|
+
* WHY THESE ARE QUIET, AND WHY THAT IS A DELIBERATE EXCEPTION
|
|
6
|
+
*
|
|
7
|
+
* CLAUDE.md §2 bans `variant="ghost"` as the default lesser-button: "buttons
|
|
8
|
+
* must read as buttons, with at least a 1px border", with ghost "reserved for
|
|
9
|
+
* genuinely chrome-dense contexts — side-panel modals, inline icon toolbars".
|
|
10
|
+
* A per-comment byline action cluster is exactly that context, and there is a
|
|
11
|
+
* second, product reason on top of it:
|
|
12
|
+
*
|
|
13
|
+
* Owner direction, 2026-09-06: Reply sits slightly OUT OF THE WAY, so people
|
|
14
|
+
* reach for the main comment box instead of threading every remark. A
|
|
15
|
+
* bordered button beside a name is an invitation. A muted, borderless one is
|
|
16
|
+
* available without asking to be used.
|
|
17
|
+
*
|
|
18
|
+
* So this is intentional. Do not "fix" it back to bordered secondary buttons in
|
|
19
|
+
* an audit — that would reverse a product decision, not correct a slip.
|
|
20
|
+
*
|
|
21
|
+
* NO REFLOW. Two separate guarantees, because they fail differently:
|
|
22
|
+
*
|
|
23
|
+
* • The cluster is ALWAYS VISIBLE, never revealed on hover. A hover-revealed
|
|
24
|
+
* control changes the row's occupied width as the pointer crosses it, and
|
|
25
|
+
* with a flexible name beside it that re-truncates the name on every pass.
|
|
26
|
+
* • The cluster reserves a fixed `min-inline-size`, so a comment with no
|
|
27
|
+
* actions at all (a tombstone) truncates its name at the same column as a
|
|
28
|
+
* live one. The identity group beside it is `min-w-0 flex-1` with the name
|
|
29
|
+
* truncating, and the cluster is `shrink-0`, so a long name eats its own
|
|
30
|
+
* width rather than pushing the actions off the row.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { useCallback } from 'react'
|
|
34
|
+
import { Copy, CornerDownRight, Link2, MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
|
|
35
|
+
import type { CSSProperties } from 'react'
|
|
36
|
+
import { cn } from '../lib/utils'
|
|
37
|
+
import {
|
|
38
|
+
DropdownMenu,
|
|
39
|
+
DropdownMenuContent,
|
|
40
|
+
DropdownMenuItem,
|
|
41
|
+
DropdownMenuTrigger,
|
|
42
|
+
} from '../dropdown-menu'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Quiet: no border, no fill at rest, muted ink. The hover fill and the focus
|
|
46
|
+
* ring are what make it legible as a control — an interactive element still has
|
|
47
|
+
* to look interactive (better-layout §2), it just does not have to shout.
|
|
48
|
+
*/
|
|
49
|
+
const QUIET = cn(
|
|
50
|
+
'inline-flex items-center justify-center gap-[var(--space-1)] border-0 bg-transparent',
|
|
51
|
+
'text-[12px] font-semibold text-[rgb(var(--text-tertiary))]',
|
|
52
|
+
'transition-[background-color,color] duration-[var(--dur-fast)] ease-[var(--ease-out)]',
|
|
53
|
+
'motion-reduce:transition-none',
|
|
54
|
+
'hover:bg-[rgb(var(--surface-overlay))] hover:text-[rgb(var(--foreground))]',
|
|
55
|
+
'focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]',
|
|
56
|
+
'disabled:cursor-default disabled:opacity-60',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
/** 24x24 floor from the interface standards §6, met by padding rather than ink. */
|
|
60
|
+
const QUIET_BOX: CSSProperties = {
|
|
61
|
+
minBlockSize: 'var(--space-6)',
|
|
62
|
+
minInlineSize: 'var(--space-6)',
|
|
63
|
+
paddingInline: 'var(--space-2)',
|
|
64
|
+
borderRadius: 'var(--radius-xs)',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const QUIET_ICON_BOX: CSSProperties = {
|
|
68
|
+
...QUIET_BOX,
|
|
69
|
+
paddingInline: '0',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reserved so a byline with no actions truncates its name at the same column as
|
|
74
|
+
* one with both. Sized for the widest cluster this renders — `↩ Reply` plus the
|
|
75
|
+
* overflow — as a multiple of the spacing scale rather than a measured pixel
|
|
76
|
+
* count, because the exact width moves with the font and the reserve only has
|
|
77
|
+
* to be *at least* the real width.
|
|
78
|
+
*/
|
|
79
|
+
const CLUSTER: CSSProperties = {
|
|
80
|
+
minInlineSize: 'calc(var(--space-12) * 2)',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CommentActionsProps {
|
|
84
|
+
/** Author display name. Thirty "Reply" buttons need thirty distinct names. */
|
|
85
|
+
readonly label: string
|
|
86
|
+
readonly replyOpen: boolean
|
|
87
|
+
/** Omitted, no Reply is rendered — a read-only surface, or a tombstone. */
|
|
88
|
+
readonly onReplyToggle?: (() => void) | undefined
|
|
89
|
+
readonly onCopyLink?: (() => void) | undefined
|
|
90
|
+
/** The body to put on the clipboard. Omitted, the item is not rendered. */
|
|
91
|
+
readonly copyText?: string | undefined
|
|
92
|
+
/** Reports the clipboard outcome so a host can toast either way. */
|
|
93
|
+
readonly onCopyText?: ((ok: boolean) => void) | undefined
|
|
94
|
+
readonly onEdit?: (() => void) | undefined
|
|
95
|
+
readonly onDelete?: (() => void) | undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `navigator.clipboard` is absent in an insecure context and in some embedded
|
|
100
|
+
* webviews. Checked before the item is RENDERED, so we never offer an
|
|
101
|
+
* affordance that cannot work — and the write is still guarded, because
|
|
102
|
+
* permission can be denied at the moment of the call.
|
|
103
|
+
*/
|
|
104
|
+
function clipboardAvailable(): boolean {
|
|
105
|
+
return typeof navigator !== 'undefined' && 'clipboard' in navigator
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function CommentActions({
|
|
109
|
+
label,
|
|
110
|
+
replyOpen,
|
|
111
|
+
onReplyToggle,
|
|
112
|
+
onCopyLink,
|
|
113
|
+
copyText,
|
|
114
|
+
onCopyText,
|
|
115
|
+
onEdit,
|
|
116
|
+
onDelete,
|
|
117
|
+
}: CommentActionsProps) {
|
|
118
|
+
const canCopyText = copyText !== undefined && copyText !== '' && clipboardAvailable()
|
|
119
|
+
|
|
120
|
+
const copy = useCallback(() => {
|
|
121
|
+
if (copyText === undefined) return
|
|
122
|
+
// Never assume it resolves: a denied permission, a document that is not
|
|
123
|
+
// focused, and a webview with no clipboard all reject here. The host is
|
|
124
|
+
// told which happened so it can say so rather than silently doing nothing.
|
|
125
|
+
navigator.clipboard.writeText(copyText).then(
|
|
126
|
+
() => {
|
|
127
|
+
onCopyText?.(true)
|
|
128
|
+
},
|
|
129
|
+
() => {
|
|
130
|
+
onCopyText?.(false)
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
}, [copyText, onCopyText])
|
|
134
|
+
|
|
135
|
+
const hasMenu = onCopyLink !== undefined || canCopyText || onEdit !== undefined || onDelete !== undefined
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
className="flex shrink-0 items-center justify-end gap-[var(--space-1)]"
|
|
140
|
+
style={CLUSTER}
|
|
141
|
+
>
|
|
142
|
+
{onReplyToggle === undefined ? null : (
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={onReplyToggle}
|
|
146
|
+
aria-label={`Reply to ${label}`}
|
|
147
|
+
aria-expanded={replyOpen}
|
|
148
|
+
className={QUIET}
|
|
149
|
+
style={QUIET_BOX}
|
|
150
|
+
>
|
|
151
|
+
<CornerDownRight size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
152
|
+
Reply
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{hasMenu ? (
|
|
157
|
+
<DropdownMenu>
|
|
158
|
+
<DropdownMenuTrigger
|
|
159
|
+
unstyled
|
|
160
|
+
aria-label={`More actions for ${label}'s comment`}
|
|
161
|
+
className={QUIET}
|
|
162
|
+
style={QUIET_ICON_BOX}
|
|
163
|
+
>
|
|
164
|
+
<MoreHorizontal size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
165
|
+
</DropdownMenuTrigger>
|
|
166
|
+
<DropdownMenuContent align="end">
|
|
167
|
+
{onCopyLink === undefined ? null : (
|
|
168
|
+
<DropdownMenuItem onClick={onCopyLink}>
|
|
169
|
+
<Link2 size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
170
|
+
Copy link
|
|
171
|
+
</DropdownMenuItem>
|
|
172
|
+
)}
|
|
173
|
+
{canCopyText ? (
|
|
174
|
+
<DropdownMenuItem onClick={copy}>
|
|
175
|
+
<Copy size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
176
|
+
Copy text
|
|
177
|
+
</DropdownMenuItem>
|
|
178
|
+
) : null}
|
|
179
|
+
{onEdit === undefined ? null : (
|
|
180
|
+
<DropdownMenuItem onClick={onEdit}>
|
|
181
|
+
<Pencil size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
182
|
+
Edit
|
|
183
|
+
</DropdownMenuItem>
|
|
184
|
+
)}
|
|
185
|
+
{onDelete === undefined ? null : (
|
|
186
|
+
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
|
187
|
+
<Trash2 size={12} strokeWidth={1.75} aria-hidden="true" />
|
|
188
|
+
Delete
|
|
189
|
+
</DropdownMenuItem>
|
|
190
|
+
)}
|
|
191
|
+
</DropdownMenuContent>
|
|
192
|
+
</DropdownMenu>
|
|
193
|
+
) : null}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|