@boyernick/standard-ui-react 0.1.1-canary.20 → 0.1.1-canary.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.1.1-canary.20",
3
+ "version": "0.1.1-canary.22",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -891,6 +891,31 @@ export {
891
891
  type TickerProps,
892
892
  type TickerItemProps,
893
893
  } from "./ticker";
894
+ export {
895
+ Timeline,
896
+ TimelineTrack,
897
+ TimelineItem,
898
+ TimelineMarker,
899
+ TimelineTime,
900
+ TimelineContent,
901
+ TimelineTitle,
902
+ TimelineDescription,
903
+ TimelineMedia,
904
+ timelineVariants,
905
+ timelineTrackVariants,
906
+ timelineItemVariants,
907
+ timelineMarkerVariants,
908
+ timelineTimeVariants,
909
+ type TimelineProps,
910
+ type TimelineTrackProps,
911
+ type TimelineItemProps,
912
+ type TimelineMarkerProps,
913
+ type TimelineTimeProps,
914
+ type TimelineContentProps,
915
+ type TimelineTitleProps,
916
+ type TimelineDescriptionProps,
917
+ type TimelineMediaProps,
918
+ } from "./timeline";
894
919
  export {
895
920
  Table,
896
921
  TableHeader,
@@ -0,0 +1,321 @@
1
+ "use client"
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import {
5
+ createContext,
6
+ type ComponentProps,
7
+ type KeyboardEvent,
8
+ type WheelEvent,
9
+ useContext,
10
+ } from "react"
11
+ import { cn } from "./lib/cn"
12
+
13
+ const timelineVariants = cva("group/timeline relative text-fg-primary", {
14
+ variants: {
15
+ orientation: {
16
+ vertical: "w-full",
17
+ horizontal:
18
+ "w-full snap-x snap-proximity overflow-x-auto overscroll-x-contain rounded-xl border border-border-primary bg-surface px-5 py-6 scroll-smooth focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 focus-visible:outline-none",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ orientation: "vertical",
23
+ },
24
+ })
25
+
26
+ const timelineTrackVariants = cva("relative m-0 list-none p-0", {
27
+ variants: {
28
+ orientation: {
29
+ vertical: "ml-1.5 border-l border-dashed border-border-primary",
30
+ horizontal:
31
+ "grid min-w-max auto-cols-64 grid-flow-col before:absolute before:inset-x-0 before:top-10 before:border-t before:border-dashed before:border-border-primary before:content-['']",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ orientation: "vertical",
36
+ },
37
+ })
38
+
39
+ const timelineItemVariants = cva("relative", {
40
+ variants: {
41
+ orientation: {
42
+ vertical: "pb-8 pl-7 last:pb-0",
43
+ horizontal: "w-64 snap-start pt-16 pr-8 last:pr-0",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ orientation: "vertical",
48
+ },
49
+ })
50
+
51
+ const timelineMarkerVariants = cva(
52
+ "absolute z-[1] size-2.5 rounded-full border-2 border-surface shadow-hairline",
53
+ {
54
+ variants: {
55
+ orientation: {
56
+ vertical: "top-1 left-0 -translate-x-1/2",
57
+ horizontal: "top-10 left-0 -translate-y-1/2",
58
+ },
59
+ tone: {
60
+ neutral: "bg-fg-tertiary",
61
+ accent: "bg-brand-primary",
62
+ info: "bg-status-info",
63
+ success: "bg-status-success",
64
+ warning: "bg-status-warning",
65
+ critical: "bg-status-critical",
66
+ },
67
+ },
68
+ defaultVariants: {
69
+ orientation: "vertical",
70
+ tone: "neutral",
71
+ },
72
+ },
73
+ )
74
+
75
+ const timelineTimeVariants = cva(
76
+ "text-xs-strong block text-fg-tertiary tabular-nums",
77
+ {
78
+ variants: {
79
+ orientation: {
80
+ vertical: "mb-1",
81
+ horizontal: "absolute top-0 left-0",
82
+ },
83
+ },
84
+ defaultVariants: {
85
+ orientation: "vertical",
86
+ },
87
+ },
88
+ )
89
+
90
+ type TimelineOrientation = "horizontal" | "vertical"
91
+
92
+ const TimelineContext = createContext<TimelineOrientation>("vertical")
93
+
94
+ export type TimelineProps = ComponentProps<"div"> &
95
+ VariantProps<typeof timelineVariants> & {
96
+ /** Distance moved by the horizontal timeline's arrow keys. */
97
+ scrollStep?: number
98
+ }
99
+
100
+ export type TimelineTrackProps = ComponentProps<"ol">
101
+ export type TimelineItemProps = ComponentProps<"li">
102
+ export type TimelineMarkerProps = ComponentProps<"span"> &
103
+ VariantProps<typeof timelineMarkerVariants>
104
+ export type TimelineTimeProps = ComponentProps<"time">
105
+ export type TimelineContentProps = ComponentProps<"div">
106
+ export type TimelineTitleProps = ComponentProps<"h3">
107
+ export type TimelineDescriptionProps = ComponentProps<"p">
108
+ export type TimelineMediaProps = ComponentProps<"div">
109
+
110
+ export const Timeline = ({
111
+ orientation = "vertical",
112
+ scrollStep = 256,
113
+ className,
114
+ children,
115
+ tabIndex,
116
+ onKeyDown,
117
+ onWheel,
118
+ "aria-label": ariaLabel = "Timeline",
119
+ ...props
120
+ }: TimelineProps) => {
121
+ const resolvedOrientation = orientation ?? "vertical"
122
+
123
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
124
+ onKeyDown?.(event)
125
+ if (
126
+ event.defaultPrevented ||
127
+ resolvedOrientation !== "horizontal" ||
128
+ event.target !== event.currentTarget
129
+ ) {
130
+ return
131
+ }
132
+
133
+ const node = event.currentTarget
134
+ const prefersReducedMotion = window.matchMedia(
135
+ "(prefers-reduced-motion: reduce)",
136
+ ).matches
137
+ const behavior = prefersReducedMotion ? "auto" : "smooth"
138
+
139
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
140
+ event.preventDefault()
141
+ node.scrollBy({
142
+ left: event.key === "ArrowRight" ? scrollStep : -scrollStep,
143
+ behavior,
144
+ })
145
+ }
146
+
147
+ if (event.key === "Home" || event.key === "End") {
148
+ event.preventDefault()
149
+ node.scrollTo({
150
+ left: event.key === "Home" ? 0 : node.scrollWidth,
151
+ behavior,
152
+ })
153
+ }
154
+ }
155
+
156
+ const handleWheel = (event: WheelEvent<HTMLDivElement>) => {
157
+ onWheel?.(event)
158
+ if (
159
+ event.defaultPrevented ||
160
+ resolvedOrientation !== "horizontal" ||
161
+ Math.abs(event.deltaX) >= Math.abs(event.deltaY)
162
+ ) {
163
+ return
164
+ }
165
+
166
+ const node = event.currentTarget
167
+ const maxScrollLeft = node.scrollWidth - node.clientWidth
168
+ const canScroll =
169
+ event.deltaY > 0 ? node.scrollLeft < maxScrollLeft : node.scrollLeft > 0
170
+
171
+ if (canScroll) {
172
+ event.preventDefault()
173
+ node.scrollLeft += event.deltaY
174
+ }
175
+ }
176
+
177
+ return (
178
+ <TimelineContext.Provider value={resolvedOrientation}>
179
+ <div
180
+ data-slot="timeline"
181
+ data-orientation={resolvedOrientation}
182
+ role="region"
183
+ aria-label={ariaLabel}
184
+ tabIndex={
185
+ tabIndex ?? (resolvedOrientation === "horizontal" ? 0 : undefined)
186
+ }
187
+ className={cn(
188
+ timelineVariants({ orientation: resolvedOrientation }),
189
+ className,
190
+ )}
191
+ onKeyDown={handleKeyDown}
192
+ onWheel={handleWheel}
193
+ {...props}
194
+ >
195
+ {children}
196
+ </div>
197
+ </TimelineContext.Provider>
198
+ )
199
+ }
200
+
201
+ export const TimelineTrack = ({
202
+ className,
203
+ ...props
204
+ }: TimelineTrackProps) => {
205
+ const orientation = useContext(TimelineContext)
206
+
207
+ return (
208
+ <ol
209
+ data-slot="timeline-track"
210
+ data-orientation={orientation}
211
+ className={cn(timelineTrackVariants({ orientation }), className)}
212
+ {...props}
213
+ />
214
+ )
215
+ }
216
+
217
+ export const TimelineItem = ({
218
+ className,
219
+ ...props
220
+ }: TimelineItemProps) => {
221
+ const orientation = useContext(TimelineContext)
222
+
223
+ return (
224
+ <li
225
+ data-slot="timeline-item"
226
+ data-orientation={orientation}
227
+ className={cn(timelineItemVariants({ orientation }), className)}
228
+ {...props}
229
+ />
230
+ )
231
+ }
232
+
233
+ export const TimelineMarker = ({
234
+ className,
235
+ tone,
236
+ ...props
237
+ }: TimelineMarkerProps) => {
238
+ const orientation = useContext(TimelineContext)
239
+
240
+ return (
241
+ <span
242
+ data-slot="timeline-marker"
243
+ aria-hidden
244
+ className={cn(
245
+ timelineMarkerVariants({ orientation, tone }),
246
+ className,
247
+ )}
248
+ {...props}
249
+ />
250
+ )
251
+ }
252
+
253
+ export const TimelineTime = ({
254
+ className,
255
+ ...props
256
+ }: TimelineTimeProps) => {
257
+ const orientation = useContext(TimelineContext)
258
+
259
+ return (
260
+ <time
261
+ data-slot="timeline-time"
262
+ className={cn(timelineTimeVariants({ orientation }), className)}
263
+ {...props}
264
+ />
265
+ )
266
+ }
267
+
268
+ export const TimelineContent = ({
269
+ className,
270
+ ...props
271
+ }: TimelineContentProps) => (
272
+ <div
273
+ data-slot="timeline-content"
274
+ className={cn("min-w-0", className)}
275
+ {...props}
276
+ />
277
+ )
278
+
279
+ export const TimelineTitle = ({
280
+ className,
281
+ ...props
282
+ }: TimelineTitleProps) => (
283
+ <h3
284
+ data-slot="timeline-title"
285
+ className={cn("text-sm-strong text-fg-primary", className)}
286
+ {...props}
287
+ />
288
+ )
289
+
290
+ export const TimelineDescription = ({
291
+ className,
292
+ ...props
293
+ }: TimelineDescriptionProps) => (
294
+ <p
295
+ data-slot="timeline-description"
296
+ className={cn("text-sm mt-1 leading-relaxed text-fg-secondary", className)}
297
+ {...props}
298
+ />
299
+ )
300
+
301
+ export const TimelineMedia = ({
302
+ className,
303
+ ...props
304
+ }: TimelineMediaProps) => (
305
+ <div
306
+ data-slot="timeline-media"
307
+ className={cn(
308
+ "mt-4 overflow-hidden rounded-xl border border-border-primary bg-background-secondary [&>img]:block [&>img]:aspect-4/3 [&>img]:w-full [&>img]:object-cover",
309
+ className,
310
+ )}
311
+ {...props}
312
+ />
313
+ )
314
+
315
+ export {
316
+ timelineVariants,
317
+ timelineItemVariants,
318
+ timelineMarkerVariants,
319
+ timelineTimeVariants,
320
+ timelineTrackVariants,
321
+ }