@playfast/reform-better-hub-ui-primitives 0.2.1 → 0.3.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/README.md +9 -9
- package/package.json +23 -23
- package/src/components/agent-icon.tsx +16 -19
- package/src/components/autocomplete.tsx +193 -0
- package/src/components/badge.tsx +46 -47
- package/src/components/button.tsx +56 -65
- package/src/components/combobox.tsx +233 -231
- package/src/components/command.tsx +135 -142
- package/src/components/dialog.tsx +106 -101
- package/src/components/dropdown-menu.tsx +118 -118
- package/src/components/github-background.tsx +224 -239
- package/src/components/halftone-background.tsx +164 -176
- package/src/components/halftone-config.ts +14 -0
- package/src/components/input-group.tsx +152 -159
- package/src/components/input.tsx +17 -20
- package/src/components/live-duration.tsx +71 -72
- package/src/components/logo.tsx +9 -15
- package/src/components/popover.tsx +60 -65
- package/src/components/resize-handle.tsx +73 -87
- package/src/components/sheet.tsx +120 -115
- package/src/components/textarea.tsx +15 -17
- package/src/components/time-ago.tsx +64 -66
- package/src/components/tooltip.tsx +48 -45
- package/src/foundation/cn.ts +0 -4
- package/src/foundation/styles.css +6 -4
- package/src/index.ts +21 -24
|
@@ -1,182 +1,175 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import type
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Error state.
|
|
33
|
-
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
34
|
-
|
|
35
|
-
className,
|
|
36
|
-
)}
|
|
37
|
-
{...props}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import type { ClassProp } from 'class-variance-authority/types'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../foundation/cn'
|
|
7
|
+
import { Button } from './button'
|
|
8
|
+
import { Input } from './input'
|
|
9
|
+
import { Textarea } from './textarea'
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: ComponentProps<'div'>): React.JSX.Element {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30',
|
|
18
|
+
'h-9 min-w-0 has-[>textarea]:h-auto',
|
|
19
|
+
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
|
20
|
+
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
|
21
|
+
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
|
22
|
+
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
|
23
|
+
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',
|
|
24
|
+
'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
|
25
|
+
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
40
31
|
}
|
|
41
32
|
|
|
42
33
|
type InputGroupAddonVariantsExternalApi = (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
) => string
|
|
34
|
+
props?:
|
|
35
|
+
| ({
|
|
36
|
+
align?: 'inline-start' | 'inline-end' | 'block-start' | 'block-end' | undefined
|
|
37
|
+
} & ClassProp)
|
|
38
|
+
| undefined,
|
|
39
|
+
) => string
|
|
49
40
|
|
|
50
41
|
const inputGroupAddonVariants: InputGroupAddonVariantsExternalApi = cva(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
);
|
|
42
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
43
|
+
{
|
|
44
|
+
variants: {
|
|
45
|
+
align: {
|
|
46
|
+
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
|
47
|
+
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
|
|
48
|
+
'block-start':
|
|
49
|
+
'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
|
|
50
|
+
'block-end':
|
|
51
|
+
'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
align: 'inline-start',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
70
59
|
|
|
71
60
|
function InputGroupAddon({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}: ComponentProps<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
61
|
+
className,
|
|
62
|
+
align = 'inline-start',
|
|
63
|
+
...props
|
|
64
|
+
}: ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>): React.JSX.Element {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="group"
|
|
68
|
+
data-slot="input-group-addon"
|
|
69
|
+
data-align={align}
|
|
70
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
71
|
+
onClick={(event) => {
|
|
72
|
+
const target = event.target
|
|
73
|
+
if (target instanceof HTMLElement && target.closest('button')) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
event.currentTarget.parentElement?.querySelector('input')?.focus()
|
|
77
|
+
}}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
92
81
|
}
|
|
93
82
|
|
|
94
83
|
type InputGroupButtonVariantsExternalApi = (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
) => string
|
|
84
|
+
props?:
|
|
85
|
+
| ({
|
|
86
|
+
size?: 'xs' | 'sm' | 'icon-xs' | 'icon-sm' | undefined
|
|
87
|
+
} & ClassProp)
|
|
88
|
+
| undefined,
|
|
89
|
+
) => string
|
|
101
90
|
|
|
102
91
|
const inputGroupButtonVariants: InputGroupButtonVariantsExternalApi = cva(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
92
|
+
'flex items-center gap-2 text-sm shadow-none',
|
|
93
|
+
{
|
|
94
|
+
variants: {
|
|
95
|
+
size: {
|
|
96
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
97
|
+
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
|
|
98
|
+
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
|
|
99
|
+
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
defaultVariants: {
|
|
103
|
+
size: 'xs',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
)
|
|
117
107
|
|
|
118
108
|
function InputGroupButton({
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}: Omit<ComponentProps<typeof Button>,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
109
|
+
className,
|
|
110
|
+
type = 'button',
|
|
111
|
+
variant = 'ghost',
|
|
112
|
+
size = 'xs',
|
|
113
|
+
...props
|
|
114
|
+
}: Omit<ComponentProps<typeof Button>, 'size'> &
|
|
115
|
+
VariantProps<typeof inputGroupButtonVariants>): React.JSX.Element {
|
|
116
|
+
return (
|
|
117
|
+
<Button
|
|
118
|
+
type={type}
|
|
119
|
+
data-size={size}
|
|
120
|
+
variant={variant}
|
|
121
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
135
125
|
}
|
|
136
126
|
|
|
137
|
-
function InputGroupText({ className, ...props }: ComponentProps<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
127
|
+
function InputGroupText({ className, ...props }: ComponentProps<'span'>): React.JSX.Element {
|
|
128
|
+
return (
|
|
129
|
+
<span
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
function InputGroupInput({ className, ...props }: ComponentProps<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
139
|
+
function InputGroupInput({ className, ...props }: ComponentProps<'input'>): React.JSX.Element {
|
|
140
|
+
return (
|
|
141
|
+
<Input
|
|
142
|
+
data-slot="input-group-control"
|
|
143
|
+
className={cn(
|
|
144
|
+
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
|
145
|
+
className,
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
)
|
|
160
150
|
}
|
|
161
151
|
|
|
162
|
-
function InputGroupTextarea({
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
152
|
+
function InputGroupTextarea({
|
|
153
|
+
className,
|
|
154
|
+
...props
|
|
155
|
+
}: ComponentProps<'textarea'>): React.JSX.Element {
|
|
156
|
+
return (
|
|
157
|
+
<Textarea
|
|
158
|
+
data-slot="input-group-control"
|
|
159
|
+
className={cn(
|
|
160
|
+
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
|
161
|
+
className,
|
|
162
|
+
)}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
)
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
export {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
169
|
+
InputGroup,
|
|
170
|
+
InputGroupAddon,
|
|
171
|
+
InputGroupButton,
|
|
172
|
+
InputGroupText,
|
|
173
|
+
InputGroupInput,
|
|
174
|
+
InputGroupTextarea,
|
|
175
|
+
}
|
package/src/components/input.tsx
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import type { ComponentProps } from 'react';
|
|
6
|
-
|
|
7
|
-
import { cn } from '../foundation/cn';
|
|
4
|
+
import { cn } from '../foundation/cn'
|
|
8
5
|
|
|
9
6
|
function Input({ className, type, ...props }: ComponentProps<'input'>): React.JSX.Element {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type={type}
|
|
10
|
+
data-slot="input"
|
|
11
|
+
className={cn(
|
|
12
|
+
'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
|
|
13
|
+
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
14
|
+
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
export { Input }
|
|
22
|
+
export { Input }
|
|
@@ -1,93 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { DateTime, Option } from 'effect'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const MS_PER_SECOND = 1000;
|
|
9
|
-
const SECONDS_PER_HOUR = 3600;
|
|
10
|
-
const SECONDS_PER_MINUTE = 60;
|
|
11
|
-
const TICK_INTERVAL_MS = 1000;
|
|
5
|
+
const MS_PER_SECOND = 1000
|
|
6
|
+
const SECONDS_PER_HOUR = 3600
|
|
7
|
+
const SECONDS_PER_MINUTE = 60
|
|
8
|
+
const TICK_INTERVAL_MS = 1000
|
|
12
9
|
|
|
13
10
|
interface FormatDurationInputExternalApi {
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
startedAt: string | null
|
|
12
|
+
completedAt: string | null
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
// Inlined from "@/lib/utils" (formatDuration) — only this helper is used here.
|
|
19
15
|
function formatDuration({ startedAt, completedAt }: FormatDurationInputExternalApi): string {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
if (!startedAt) {
|
|
17
|
+
return ''
|
|
18
|
+
}
|
|
19
|
+
const start = DateTime.unsafeMake(startedAt)
|
|
20
|
+
const end = completedAt ? DateTime.unsafeMake(completedAt) : DateTime.unsafeNow()
|
|
21
|
+
const elapsedMs = DateTime.toEpochMillis(end) - DateTime.toEpochMillis(start)
|
|
22
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / MS_PER_SECOND))
|
|
23
|
+
const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR)
|
|
24
|
+
const minutes = Math.floor((totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE)
|
|
25
|
+
const seconds = totalSeconds % SECONDS_PER_MINUTE
|
|
26
|
+
if (hours > 0) {
|
|
27
|
+
return `${hours}h ${minutes}m`
|
|
28
|
+
}
|
|
29
|
+
if (minutes > 0) {
|
|
30
|
+
return `${minutes}m ${seconds}s`
|
|
31
|
+
}
|
|
32
|
+
return `${seconds}s`
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// instead of each component running its own 1s interval.
|
|
42
|
-
const callbacks = new Set<() => void>();
|
|
35
|
+
// shared tick: one interval drives all LiveDuration subscribers
|
|
36
|
+
const callbacks = new Set<() => void>()
|
|
43
37
|
const tickState: { intervalId: Option.Option<ReturnType<typeof setInterval>> } = {
|
|
44
|
-
|
|
45
|
-
}
|
|
38
|
+
intervalId: Option.none(),
|
|
39
|
+
}
|
|
46
40
|
|
|
47
41
|
function subscribeLiveTick(callback: () => void): () => void {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
42
|
+
callbacks.add(callback)
|
|
43
|
+
if (Option.isNone(tickState.intervalId)) {
|
|
44
|
+
tickState.intervalId = Option.some(
|
|
45
|
+
// oxlint-disable-next-line reform-rules/no-set-timeout-interval -- browser-timer seam: shared render ticker for React, no Effect runtime here
|
|
46
|
+
setInterval(() => {
|
|
47
|
+
callbacks.forEach((cb) => cb())
|
|
48
|
+
}, TICK_INTERVAL_MS),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
callbacks.delete(callback)
|
|
53
|
+
if (callbacks.size === 0 && Option.isSome(tickState.intervalId)) {
|
|
54
|
+
clearInterval(tickState.intervalId.value)
|
|
55
|
+
tickState.intervalId = Option.none()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
interface LiveDurationPropsExternalApi {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
startedAt: string | null
|
|
62
|
+
completedAt?: string | null
|
|
63
|
+
className?: string
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
export function LiveDuration({
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
export function LiveDuration({
|
|
67
|
+
startedAt,
|
|
68
|
+
completedAt,
|
|
69
|
+
className,
|
|
70
|
+
}: LiveDurationPropsExternalApi): React.ReactNode {
|
|
71
|
+
const [, setTick] = useState(0)
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!startedAt || completedAt != null) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
setTick(0)
|
|
78
|
+
return subscribeLiveTick(() => setTick((prev) => prev + 1))
|
|
79
|
+
}, [startedAt, completedAt])
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if (!startedAt) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
const formatted = formatDuration({ startedAt, completedAt: completedAt ?? null })
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
return (
|
|
88
|
+
<span className={className} suppressHydrationWarning>
|
|
89
|
+
{formatted}
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
93
92
|
}
|
package/src/components/logo.tsx
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type * as React from "react";
|
|
4
|
-
import { cn } from '../foundation/cn';
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import { cn } from '../foundation/cn'
|
|
5
3
|
|
|
6
4
|
interface LogoPropsExternalApi {
|
|
7
|
-
|
|
5
|
+
className?: string
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
export function Logo({ className }: LogoPropsExternalApi): React.JSX.Element {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
return (
|
|
10
|
+
<span className={cn('font-mono text-sm font-medium tracking-tight', className)}>
|
|
11
|
+
BETTER-HUB.
|
|
12
|
+
</span>
|
|
13
|
+
)
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export function LogoMark({ className }: LogoPropsExternalApi): React.JSX.Element {
|
|
19
|
-
|
|
20
|
-
<span className={cn("font-mono text-sm font-bold tracking-tight", className)}>
|
|
21
|
-
b.
|
|
22
|
-
</span>
|
|
23
|
-
);
|
|
17
|
+
return <span className={cn('font-mono text-sm font-bold tracking-tight', className)}>b.</span>
|
|
24
18
|
}
|