@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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useThread — the headless core. It renders nothing.
|
|
3
|
+
*
|
|
4
|
+
* ADR-147 T3. Everything that is genuinely thread LOGIC lives here: building
|
|
5
|
+
* the tree, sorting it, collapse state, the depth cap, "show N more replies",
|
|
6
|
+
* the root draft, and which comment (if any) has an open reply composer.
|
|
7
|
+
* `<Thread>` is one skin over this; a consumer that wants different chrome
|
|
8
|
+
* uses the hook and draws its own.
|
|
9
|
+
*
|
|
10
|
+
* What this hook deliberately does NOT own:
|
|
11
|
+
*
|
|
12
|
+
* • No fetch client, no base URL, no query key, no brand id. It takes
|
|
13
|
+
* already-shaped data and reports intent through callbacks. T4 wires it.
|
|
14
|
+
* • No persistence. The draft is in-memory here; ADR-147 D17 routes it
|
|
15
|
+
* through `useResumableDraft` with a brand-partitioned key at the call
|
|
16
|
+
* site, because a comment draft is EDITOR state — a bare `key={brand.id}`
|
|
17
|
+
* on the thread would destroy an unsent comment on a brand switch.
|
|
18
|
+
* • No reaction or vote state. Correction 3 to the port: the reference held
|
|
19
|
+
* vote state in local `useState`, so a vote was lost the moment the dialog
|
|
20
|
+
* closed. Engagement is props in, callbacks out, always.
|
|
21
|
+
*
|
|
22
|
+
* Every piece of local state here is genuinely local VIEW state — what is
|
|
23
|
+
* collapsed, what is expanded, what is being typed — and losing it on unmount
|
|
24
|
+
* is correct. That is the line.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
28
|
+
import { buildCommentTree, countCommentTree, sortCommentTree } from './comment-tree'
|
|
29
|
+
import { useNowTick } from './use-now'
|
|
30
|
+
import type {
|
|
31
|
+
CommentNode,
|
|
32
|
+
ThreadComment,
|
|
33
|
+
ThreadEngagement,
|
|
34
|
+
ThreadSort,
|
|
35
|
+
} from './types'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Nesting is UNCAPPED by default.
|
|
39
|
+
*
|
|
40
|
+
* The port inherited a cap of 2 plus "Continue this thread", and that cap was
|
|
41
|
+
* paying for a rendering problem rather than solving a reading one: when every
|
|
42
|
+
* level cost a 24px gutter and its own collapse button, depth was expensive and
|
|
43
|
+
* had to be rationed. Depth is now an indent and a hairline (rail.tsx), so it
|
|
44
|
+
* is cheap, and a dead-end control that hides half a conversation behind a
|
|
45
|
+
* click is a worse answer than simply showing it. The INDENT is what gets
|
|
46
|
+
* clamped instead (`indentClampDepth`), which is the thing that actually breaks
|
|
47
|
+
* in a narrow column.
|
|
48
|
+
*
|
|
49
|
+
* The cap machinery stays for a consumer that genuinely wants one — a preview
|
|
50
|
+
* strip on a card, a permalink view that pages by subtree. Set a finite
|
|
51
|
+
* `maxDepth` and "Continue this thread" comes back.
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_MAX_DEPTH = Number.POSITIVE_INFINITY
|
|
54
|
+
/** Three replies visible, then "Show N more replies". */
|
|
55
|
+
export const DEFAULT_REPLIES_VISIBLE = 3
|
|
56
|
+
|
|
57
|
+
export interface UseThreadOptions {
|
|
58
|
+
readonly comments: readonly ThreadComment[]
|
|
59
|
+
/**
|
|
60
|
+
* Epoch ms. Omit and the hook subscribes to the shared 60s ticker; pass one
|
|
61
|
+
* and the clock is frozen and no timer runs (tests, SSR, a replay view).
|
|
62
|
+
*/
|
|
63
|
+
readonly now?: number | undefined
|
|
64
|
+
/** Controlled sort. Omit for uncontrolled. */
|
|
65
|
+
readonly sort?: ThreadSort | undefined
|
|
66
|
+
readonly defaultSort?: ThreadSort | undefined
|
|
67
|
+
readonly onSortChange?: ((sort: ThreadSort) => void) | undefined
|
|
68
|
+
/** Render depth cap. Storage depth is always true (D3). */
|
|
69
|
+
readonly maxDepth?: number | undefined
|
|
70
|
+
readonly repliesVisible?: number | undefined
|
|
71
|
+
readonly engagement?: ThreadEngagement | undefined
|
|
72
|
+
/** Controlled collapse set. Omit for uncontrolled. */
|
|
73
|
+
readonly collapsed?: readonly string[] | undefined
|
|
74
|
+
readonly onCollapse?: ((commentId: string, collapsed: boolean) => void) | undefined
|
|
75
|
+
/** Controlled root draft — the seam T4 hands `useResumableDraft`. */
|
|
76
|
+
readonly draft?: string | undefined
|
|
77
|
+
readonly defaultDraft?: string | undefined
|
|
78
|
+
readonly onDraftChange?: ((value: string) => void) | undefined
|
|
79
|
+
/**
|
|
80
|
+
* Verified attachment ticket ids are the third argument so the existing
|
|
81
|
+
* `(body, parentId?)` seam stays source-compatible (ADR-151 D1).
|
|
82
|
+
*/
|
|
83
|
+
readonly onSubmit?:
|
|
84
|
+
| ((body: string, parentId?: string, attachmentIds?: readonly string[]) => void)
|
|
85
|
+
| undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface VisibleReplies {
|
|
89
|
+
readonly visible: readonly CommentNode[]
|
|
90
|
+
readonly hidden: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface UseThreadResult {
|
|
94
|
+
readonly now: number
|
|
95
|
+
readonly roots: readonly CommentNode[]
|
|
96
|
+
/** Every comment at every depth, tombstones included. */
|
|
97
|
+
readonly totalCount: number
|
|
98
|
+
readonly maxDepth: number
|
|
99
|
+
readonly repliesVisible: number
|
|
100
|
+
readonly engagement: ThreadEngagement
|
|
101
|
+
|
|
102
|
+
readonly sort: ThreadSort
|
|
103
|
+
readonly setSort: (sort: ThreadSort) => void
|
|
104
|
+
|
|
105
|
+
readonly isCollapsed: (commentId: string) => boolean
|
|
106
|
+
readonly setCollapsed: (commentId: string, collapsed: boolean) => void
|
|
107
|
+
readonly toggleCollapse: (commentId: string) => void
|
|
108
|
+
|
|
109
|
+
readonly isRepliesExpanded: (commentId: string) => boolean
|
|
110
|
+
readonly expandReplies: (commentId: string) => void
|
|
111
|
+
readonly visibleReplies: (node: CommentNode) => VisibleReplies
|
|
112
|
+
|
|
113
|
+
/** Whether the surface accepts new comments at all (an `onSubmit` exists). */
|
|
114
|
+
readonly canReply: boolean
|
|
115
|
+
readonly draft: string
|
|
116
|
+
readonly setDraft: (value: string) => void
|
|
117
|
+
readonly canSubmit: boolean
|
|
118
|
+
readonly submit: (attachmentIds?: readonly string[]) => void
|
|
119
|
+
|
|
120
|
+
readonly replyingTo: string | null
|
|
121
|
+
readonly isReplyingTo: (commentId: string) => boolean
|
|
122
|
+
readonly startReply: (commentId: string) => void
|
|
123
|
+
readonly cancelReply: () => void
|
|
124
|
+
readonly replyDraft: string
|
|
125
|
+
readonly setReplyDraft: (value: string) => void
|
|
126
|
+
readonly canSubmitReply: boolean
|
|
127
|
+
readonly submitReply: (attachmentIds?: readonly string[]) => void
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useThread(options: UseThreadOptions): UseThreadResult {
|
|
131
|
+
const {
|
|
132
|
+
comments,
|
|
133
|
+
now: injectedNow,
|
|
134
|
+
sort: controlledSort,
|
|
135
|
+
defaultSort = 'top',
|
|
136
|
+
onSortChange,
|
|
137
|
+
maxDepth = DEFAULT_MAX_DEPTH,
|
|
138
|
+
repliesVisible = DEFAULT_REPLIES_VISIBLE,
|
|
139
|
+
engagement = 'reactions',
|
|
140
|
+
collapsed: controlledCollapsed,
|
|
141
|
+
onCollapse,
|
|
142
|
+
draft: controlledDraft,
|
|
143
|
+
defaultDraft = '',
|
|
144
|
+
onDraftChange,
|
|
145
|
+
onSubmit,
|
|
146
|
+
} = options
|
|
147
|
+
|
|
148
|
+
const tick = useNowTick(injectedNow === undefined)
|
|
149
|
+
const now = injectedNow ?? tick
|
|
150
|
+
|
|
151
|
+
const [uncontrolledSort, setUncontrolledSort] = useState<ThreadSort>(defaultSort)
|
|
152
|
+
const sort = controlledSort ?? uncontrolledSort
|
|
153
|
+
|
|
154
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState<ReadonlySet<string>>(
|
|
155
|
+
() => new Set<string>(),
|
|
156
|
+
)
|
|
157
|
+
const collapsedSet = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
controlledCollapsed === undefined
|
|
160
|
+
? uncontrolledCollapsed
|
|
161
|
+
: new Set(controlledCollapsed),
|
|
162
|
+
[controlledCollapsed, uncontrolledCollapsed],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(() => new Set<string>())
|
|
166
|
+
|
|
167
|
+
const [uncontrolledDraft, setUncontrolledDraft] = useState(defaultDraft)
|
|
168
|
+
const draft = controlledDraft ?? uncontrolledDraft
|
|
169
|
+
|
|
170
|
+
const [replyingTo, setReplyingTo] = useState<string | null>(null)
|
|
171
|
+
const [replyDraft, setReplyDraft] = useState('')
|
|
172
|
+
|
|
173
|
+
const tree = useMemo(() => buildCommentTree(comments), [comments])
|
|
174
|
+
const roots = useMemo(() => sortCommentTree(tree, sort), [tree, sort])
|
|
175
|
+
const totalCount = useMemo(() => countCommentTree(tree), [tree])
|
|
176
|
+
|
|
177
|
+
const setSort = useCallback(
|
|
178
|
+
(next: ThreadSort) => {
|
|
179
|
+
if (controlledSort === undefined) setUncontrolledSort(next)
|
|
180
|
+
onSortChange?.(next)
|
|
181
|
+
},
|
|
182
|
+
[controlledSort, onSortChange],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const isCollapsed = useCallback(
|
|
186
|
+
(commentId: string) => collapsedSet.has(commentId),
|
|
187
|
+
[collapsedSet],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const setCollapsed = useCallback(
|
|
191
|
+
(commentId: string, next: boolean) => {
|
|
192
|
+
if (controlledCollapsed === undefined) {
|
|
193
|
+
setUncontrolledCollapsed((prev) => {
|
|
194
|
+
if (prev.has(commentId) === next) return prev
|
|
195
|
+
const copy = new Set(prev)
|
|
196
|
+
if (next) copy.add(commentId)
|
|
197
|
+
else copy.delete(commentId)
|
|
198
|
+
return copy
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
onCollapse?.(commentId, next)
|
|
202
|
+
},
|
|
203
|
+
[controlledCollapsed, onCollapse],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const toggleCollapse = useCallback(
|
|
207
|
+
(commentId: string) => {
|
|
208
|
+
setCollapsed(commentId, !collapsedSet.has(commentId))
|
|
209
|
+
},
|
|
210
|
+
[collapsedSet, setCollapsed],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const isRepliesExpanded = useCallback(
|
|
214
|
+
(commentId: string) => expanded.has(commentId),
|
|
215
|
+
[expanded],
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const expandReplies = useCallback((commentId: string) => {
|
|
219
|
+
setExpanded((prev) => {
|
|
220
|
+
if (prev.has(commentId)) return prev
|
|
221
|
+
const copy = new Set(prev)
|
|
222
|
+
copy.add(commentId)
|
|
223
|
+
return copy
|
|
224
|
+
})
|
|
225
|
+
}, [])
|
|
226
|
+
|
|
227
|
+
const visibleReplies = useCallback(
|
|
228
|
+
(node: CommentNode): VisibleReplies => {
|
|
229
|
+
const all = node.replies
|
|
230
|
+
if (expanded.has(node.id) || all.length <= repliesVisible) {
|
|
231
|
+
return { visible: all, hidden: 0 }
|
|
232
|
+
}
|
|
233
|
+
const visible = all.slice(0, repliesVisible)
|
|
234
|
+
return { visible, hidden: all.length - visible.length }
|
|
235
|
+
},
|
|
236
|
+
[expanded, repliesVisible],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const setDraft = useCallback(
|
|
240
|
+
(value: string) => {
|
|
241
|
+
if (controlledDraft === undefined) setUncontrolledDraft(value)
|
|
242
|
+
onDraftChange?.(value)
|
|
243
|
+
},
|
|
244
|
+
[controlledDraft, onDraftChange],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const canReply = onSubmit !== undefined
|
|
248
|
+
const canSubmit = draft.trim() !== '' && canReply
|
|
249
|
+
|
|
250
|
+
const submit = useCallback((attachmentIds?: readonly string[]) => {
|
|
251
|
+
const body = draft.trim()
|
|
252
|
+
if (body === '' || onSubmit === undefined) return
|
|
253
|
+
if (attachmentIds === undefined || attachmentIds.length === 0) onSubmit(body)
|
|
254
|
+
else onSubmit(body, undefined, attachmentIds)
|
|
255
|
+
setDraft('')
|
|
256
|
+
}, [draft, onSubmit, setDraft])
|
|
257
|
+
|
|
258
|
+
const isReplyingTo = useCallback(
|
|
259
|
+
(commentId: string) => replyingTo === commentId,
|
|
260
|
+
[replyingTo],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const startReply = useCallback((commentId: string) => {
|
|
264
|
+
setReplyingTo(commentId)
|
|
265
|
+
// A reply draft belongs to the comment it answers. Switching targets
|
|
266
|
+
// clears it rather than silently re-aiming half a sentence at someone else.
|
|
267
|
+
setReplyDraft('')
|
|
268
|
+
}, [])
|
|
269
|
+
|
|
270
|
+
const cancelReply = useCallback(() => {
|
|
271
|
+
setReplyingTo(null)
|
|
272
|
+
setReplyDraft('')
|
|
273
|
+
}, [])
|
|
274
|
+
|
|
275
|
+
const canSubmitReply =
|
|
276
|
+
replyingTo !== null && replyDraft.trim() !== '' && onSubmit !== undefined
|
|
277
|
+
|
|
278
|
+
const submitReply = useCallback((attachmentIds?: readonly string[]) => {
|
|
279
|
+
const body = replyDraft.trim()
|
|
280
|
+
if (body === '' || replyingTo === null || onSubmit === undefined) return
|
|
281
|
+
if (attachmentIds === undefined || attachmentIds.length === 0) onSubmit(body, replyingTo)
|
|
282
|
+
else onSubmit(body, replyingTo, attachmentIds)
|
|
283
|
+
setReplyDraft('')
|
|
284
|
+
setReplyingTo(null)
|
|
285
|
+
}, [replyDraft, replyingTo, onSubmit])
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
now,
|
|
289
|
+
roots,
|
|
290
|
+
totalCount,
|
|
291
|
+
maxDepth,
|
|
292
|
+
repliesVisible,
|
|
293
|
+
engagement,
|
|
294
|
+
sort,
|
|
295
|
+
setSort,
|
|
296
|
+
isCollapsed,
|
|
297
|
+
setCollapsed,
|
|
298
|
+
toggleCollapse,
|
|
299
|
+
isRepliesExpanded,
|
|
300
|
+
expandReplies,
|
|
301
|
+
visibleReplies,
|
|
302
|
+
canReply,
|
|
303
|
+
draft,
|
|
304
|
+
setDraft,
|
|
305
|
+
canSubmit,
|
|
306
|
+
submit,
|
|
307
|
+
replyingTo,
|
|
308
|
+
isReplyingTo,
|
|
309
|
+
startReply,
|
|
310
|
+
cancelReply,
|
|
311
|
+
replyDraft,
|
|
312
|
+
setReplyDraft,
|
|
313
|
+
canSubmitReply,
|
|
314
|
+
submitReply,
|
|
315
|
+
}
|
|
316
|
+
}
|
package/src/tokens.css
CHANGED
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
* --destructive(-bg, -foreground)
|
|
34
34
|
* • Folder: --folder-red/amber/emerald/teal/blue/violet/pink/
|
|
35
35
|
* slate/ink (categorical palette for folder accents)
|
|
36
|
+
* • Series: --series-1…8 (categorical CHART palette; ADR-146 D2.
|
|
37
|
+
* theme-INDEPENDENT, and the SLOT ORDER IS LOAD-BEARING
|
|
38
|
+
* — see the block near the end of this file before
|
|
39
|
+
* "tidying" it)
|
|
40
|
+
* • Chart: --chart-grid, --chart-axis, --chart-band (chart
|
|
41
|
+
* structure — gridlines, the darker baseline rule, and
|
|
42
|
+
* the hover band; ADR-146 D3. Theme-tuned, and all
|
|
43
|
+
* three ALREADY CARRY ALPHA — never add a second one)
|
|
36
44
|
* • Spacing: --space-1…12 (4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 48)
|
|
37
45
|
* • Radius: --radius-xs/sm/md/lg/xl/2xl/full, --radius (legacy)
|
|
38
46
|
* • Motion: --ease-out, --ease-spring, --ease-in-out,
|
|
@@ -188,6 +196,34 @@
|
|
|
188
196
|
|
|
189
197
|
--border: 255 255 255 / 0.08;
|
|
190
198
|
--border-strong: 255 255 255 / 0.14;
|
|
199
|
+
|
|
200
|
+
/* ============================================================
|
|
201
|
+
CHART STRUCTURE — PUBLIC, theme-tuned (ADR-146 D3).
|
|
202
|
+
The greys a chart needs that are NOT a card border, plus the
|
|
203
|
+
hover-band fill. This is the "structure is required, not banned"
|
|
204
|
+
layer of the data-display recipe (CLAUDE.md §2): gridlines and a
|
|
205
|
+
baseline are what make a plot legible, and deleting them is the
|
|
206
|
+
flat/floating defect, not a cleanup.
|
|
207
|
+
|
|
208
|
+
--chart-axis IS DELIBERATELY DARKER THAN --chart-grid. The
|
|
209
|
+
baseline rule must outrank the gridlines. Every chart in the
|
|
210
|
+
Metoric kit draws it that way (#111111 baseline vs #dfe1e6 grid)
|
|
211
|
+
and it is the single detail that does most for chart legibility.
|
|
212
|
+
It lives as its own token precisely so it cannot drift back to
|
|
213
|
+
"same grey as the grid".
|
|
214
|
+
|
|
215
|
+
ALL THREE ALREADY CARRY AN ALPHA — they composite onto whichever
|
|
216
|
+
panel they sit on, so the panel stays opaque and the structure
|
|
217
|
+
stays translucent. Consume as `rgb(var(--chart-grid))` and NEVER
|
|
218
|
+
as `rgb(var(--chart-grid) / 0.5)`: a second alpha expands to
|
|
219
|
+
`rgb(255 255 255 / 0.07 / 0.5)`, which is invalid CSS and is
|
|
220
|
+
SILENTLY DISCARDED. `__tests__/token-shape.test.ts` fails on
|
|
221
|
+
exactly that shape.
|
|
222
|
+
============================================================ */
|
|
223
|
+
--chart-grid: 255 255 255 / 0.07;
|
|
224
|
+
--chart-axis: 255 255 255 / 0.22;
|
|
225
|
+
--chart-band: 255 255 255 / 0.05;
|
|
226
|
+
|
|
191
227
|
/* Semantic — PUBLIC */
|
|
192
228
|
--success: 16 185 129;
|
|
193
229
|
--success-bg: 16 185 129 / 0.12;
|
|
@@ -527,6 +563,14 @@
|
|
|
527
563
|
|
|
528
564
|
--border: 0 0 0 / 0.12;
|
|
529
565
|
--border-strong: 0 0 0 / 0.20;
|
|
566
|
+
|
|
567
|
+
/* Chart structure — see the :root block for the reasoning. The same
|
|
568
|
+
two invariants hold in light: --chart-axis stays darker than
|
|
569
|
+
--chart-grid, and all three already carry their alpha. */
|
|
570
|
+
--chart-grid: 0 0 0 / 0.08;
|
|
571
|
+
--chart-axis: 0 0 0 / 0.24;
|
|
572
|
+
--chart-band: 0 0 0 / 0.04;
|
|
573
|
+
|
|
530
574
|
/* Bumped from 0.16 → 0.22 — inputs on a white --bg-input (above) sitting
|
|
531
575
|
* inside a Card (--bg-card = 255 / same as input) need a stronger border
|
|
532
576
|
* to delineate. The 0.16 value worked when --bg-input was slate-tinted; on
|
|
@@ -813,3 +857,71 @@
|
|
|
813
857
|
--media-on-scrim: 255 255 255 / 0.88;
|
|
814
858
|
--media-overlay-btn: 255 255 255 / 0.14;
|
|
815
859
|
}
|
|
860
|
+
|
|
861
|
+
/* ============================================================
|
|
862
|
+
CATEGORICAL SERIES PALETTE (theme-INDEPENDENT) — PUBLIC
|
|
863
|
+
------------------------------------------------------------
|
|
864
|
+
The chart series palette (ADR-146 D2). Hues carried over from
|
|
865
|
+
`theme-v2.css`, which designed them at constant lightness ~0.62
|
|
866
|
+
and then never shipped a consumer.
|
|
867
|
+
|
|
868
|
+
THEME-INDEPENDENT ON PURPOSE, and verified rather than assumed:
|
|
869
|
+
lightness 0.62 sits inside BOTH the light band (0.43-0.77) and the
|
|
870
|
+
dark band (0.48-0.67), and all eight clear 3:1 against #ffffff,
|
|
871
|
+
against --surface-frame (#f7f7f9) and against dark --surface-card
|
|
872
|
+
(#1b1b1e). Same precedent as --hierarchy-* and --platform-* above:
|
|
873
|
+
one block, no light/dark override.
|
|
874
|
+
|
|
875
|
+
BARE TRIPLES. The call site owns the alpha — a line is
|
|
876
|
+
`rgb(var(--series-1))` and the area under it is
|
|
877
|
+
`rgb(var(--series-1) / 0.12)`. A baked-in alpha would expand to
|
|
878
|
+
`rgb(2 137 234 / 0.3 / 0.12)`, which is invalid and silently
|
|
879
|
+
dropped, and it would make the token un-shareable between the two
|
|
880
|
+
uses.
|
|
881
|
+
|
|
882
|
+
------------------------------------------------------------
|
|
883
|
+
THE SLOT ORDER IS LOAD-BEARING. DO NOT "TIDY" IT.
|
|
884
|
+
------------------------------------------------------------
|
|
885
|
+
The order was chosen by measurement (the dataviz skill's
|
|
886
|
+
validate_palette.js, adjacent-pair list, both modes), not by eye.
|
|
887
|
+
The order declared in theme-v2.css FAILS — teal against pink is
|
|
888
|
+
ΔE 1.2 for a deuteranope. This order passes all five checks in
|
|
889
|
+
both themes: worst adjacent pair ΔE 13.2 deutan / 21.0 normal,
|
|
890
|
+
identical light and dark. Re-ordering the slots breaks that, and
|
|
891
|
+
nothing in the build will tell you.
|
|
892
|
+
|
|
893
|
+
Three placements are deliberate:
|
|
894
|
+
• BLUE IS SLOT 1, not red. Slot 1 is the single-series default,
|
|
895
|
+
so a red slot 1 would paint every one-series chart in the
|
|
896
|
+
brand accent and destroy accent scarcity. Blue carries no
|
|
897
|
+
product meaning.
|
|
898
|
+
• RED IS SLOT 8. Red is reserved for --accent and
|
|
899
|
+
--destructive. It only enters a chart at eight series, where
|
|
900
|
+
the reader has bigger problems. It stays in the palette so
|
|
901
|
+
the eight-slot contract holds rather than being deleted.
|
|
902
|
+
• GREEN (slot 2) AND RED (slot 8) ARE FAR APART, so a
|
|
903
|
+
two-series chart never accidentally reads as pass/fail.
|
|
904
|
+
|
|
905
|
+
THE SCATTER / ALL-PAIRS CAP. Forms where any two marks can touch
|
|
906
|
+
— scatter, bubble, choropleth, small multiples — take the harder
|
|
907
|
+
all-pairs test, which no ordering of these eight can pass. Those
|
|
908
|
+
forms use ONLY slots 1, 6 and 7 (blue / amber / teal: verified
|
|
909
|
+
all-pairs in both modes, worst ΔE 14.8, all five checks PASS) and
|
|
910
|
+
CAP AT THREE SERIES. A fourth folds into "Other" or facets into
|
|
911
|
+
small multiples. Never generate a ninth hue.
|
|
912
|
+
|
|
913
|
+
Resolution is a frozen literal lookup in the primitive, never a
|
|
914
|
+
constructed name (ADR-146 D8): `--series-${i}` that misses yields
|
|
915
|
+
an undefined property, which makes rgb() invalid, which the
|
|
916
|
+
browser discards in silence with every gate still green.
|
|
917
|
+
============================================================ */
|
|
918
|
+
:root {
|
|
919
|
+
--series-1: 2 137 234; /* blue */
|
|
920
|
+
--series-2: 9 160 93; /* green */
|
|
921
|
+
--series-3: 153 100 229; /* violet */
|
|
922
|
+
--series-4: 113 149 3; /* olive */
|
|
923
|
+
--series-5: 212 73 150; /* pink */
|
|
924
|
+
--series-6: 186 116 4; /* amber */
|
|
925
|
+
--series-7: 14 154 148; /* teal */
|
|
926
|
+
--series-8: 226 74 62; /* red */
|
|
927
|
+
}
|