@playfast/reform-better-hub-ui-primitives 0.2.2 → 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.
@@ -1,173 +1,175 @@
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";
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
5
 
6
- import { cn } from "../foundation/cn";
7
- import { Button } from "./button";
8
- import { Input } from "./input";
9
- import { Textarea } from "./textarea";
6
+ import { cn } from '../foundation/cn'
7
+ import { Button } from './button'
8
+ import { Input } from './input'
9
+ import { Textarea } from './textarea'
10
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",
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
25
 
26
- className,
27
- )}
28
- {...props}
29
- />
30
- );
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ )
31
31
  }
32
32
 
33
33
  type InputGroupAddonVariantsExternalApi = (
34
- props?:
35
- | ({
36
- align?: "inline-start" | "inline-end" | "block-start" | "block-end" | undefined;
37
- } & ClassProp)
38
- | undefined,
39
- ) => string;
34
+ props?:
35
+ | ({
36
+ align?: 'inline-start' | 'inline-end' | 'block-start' | 'block-end' | undefined
37
+ } & ClassProp)
38
+ | undefined,
39
+ ) => string
40
40
 
41
41
  const inputGroupAddonVariants: InputGroupAddonVariantsExternalApi = cva(
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":
47
- "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
48
- "inline-end":
49
- "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
50
- "block-start":
51
- "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
52
- "block-end":
53
- "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
54
- },
55
- },
56
- defaultVariants: {
57
- align: "inline-start",
58
- },
59
- },
60
- );
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
+ )
61
59
 
62
60
  function InputGroupAddon({
63
- className,
64
- align = "inline-start",
65
- ...props
66
- }: ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): React.JSX.Element {
67
- return (
68
- <div
69
- role="group"
70
- data-slot="input-group-addon"
71
- data-align={align}
72
- className={cn(inputGroupAddonVariants({ align }), className)}
73
- onClick={(event) => {
74
- const target = event.target;
75
- if (target instanceof HTMLElement && target.closest("button")) {
76
- return;
77
- }
78
- event.currentTarget.parentElement?.querySelector("input")?.focus();
79
- }}
80
- {...props}
81
- />
82
- );
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
+ )
83
81
  }
84
82
 
85
83
  type InputGroupButtonVariantsExternalApi = (
86
- props?:
87
- | ({
88
- size?: "xs" | "sm" | "icon-xs" | "icon-sm" | undefined;
89
- } & ClassProp)
90
- | undefined,
91
- ) => string;
84
+ props?:
85
+ | ({
86
+ size?: 'xs' | 'sm' | 'icon-xs' | 'icon-sm' | undefined
87
+ } & ClassProp)
88
+ | undefined,
89
+ ) => string
92
90
 
93
91
  const inputGroupButtonVariants: InputGroupButtonVariantsExternalApi = cva(
94
- "flex items-center gap-2 text-sm shadow-none",
95
- {
96
- variants: {
97
- size: {
98
- xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
99
- sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
100
- "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
101
- "icon-sm": "size-8 p-0 has-[>svg]:p-0",
102
- },
103
- },
104
- defaultVariants: {
105
- size: "xs",
106
- },
107
- });
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
+ )
108
107
 
109
108
  function InputGroupButton({
110
- className,
111
- type = "button",
112
- variant = "ghost",
113
- size = "xs",
114
- ...props
115
- }: Omit<ComponentProps<typeof Button>, "size"> &
116
- VariantProps<typeof inputGroupButtonVariants>): React.JSX.Element {
117
- return (
118
- <Button
119
- type={type}
120
- data-size={size}
121
- variant={variant}
122
- className={cn(inputGroupButtonVariants({ size }), className)}
123
- {...props}
124
- />
125
- );
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
+ )
126
125
  }
127
126
 
128
- function InputGroupText({ className, ...props }: ComponentProps<"span">): React.JSX.Element {
129
- return (
130
- <span
131
- className={cn(
132
- "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
133
- className,
134
- )}
135
- {...props}
136
- />
137
- );
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
+ )
138
137
  }
139
138
 
140
- function InputGroupInput({ className, ...props }: ComponentProps<"input">): React.JSX.Element {
141
- return (
142
- <Input
143
- data-slot="input-group-control"
144
- className={cn(
145
- "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
146
- className,
147
- )}
148
- {...props}
149
- />
150
- );
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
+ )
151
150
  }
152
151
 
153
- function InputGroupTextarea({ className, ...props }: ComponentProps<"textarea">): React.JSX.Element {
154
- return (
155
- <Textarea
156
- data-slot="input-group-control"
157
- className={cn(
158
- "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
159
- className,
160
- )}
161
- {...props}
162
- />
163
- );
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
+ )
164
166
  }
165
167
 
166
168
  export {
167
- InputGroup,
168
- InputGroupAddon,
169
- InputGroupButton,
170
- InputGroupText,
171
- InputGroupInput,
172
- InputGroupTextarea,
173
- };
169
+ InputGroup,
170
+ InputGroupAddon,
171
+ InputGroupButton,
172
+ InputGroupText,
173
+ InputGroupInput,
174
+ InputGroupTextarea,
175
+ }
@@ -1,22 +1,22 @@
1
- import type * as React from "react";
2
- import type { ComponentProps } from 'react';
1
+ import type * as React from 'react'
2
+ import type { ComponentProps } from 'react'
3
3
 
4
- import { cn } from '../foundation/cn';
4
+ import { cn } from '../foundation/cn'
5
5
 
6
6
  function Input({ className, type, ...props }: ComponentProps<'input'>): React.JSX.Element {
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
- );
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
+ )
20
20
  }
21
21
 
22
- export { Input };
22
+ export { Input }
@@ -1,86 +1,92 @@
1
- import { DateTime, Option } from "effect";
2
- import type * as React from "react";
3
- import { useState, useEffect } from "react";
1
+ import { DateTime, Option } from 'effect'
2
+ import type * as React from 'react'
3
+ import { useState, useEffect } from 'react'
4
4
 
5
- const MS_PER_SECOND = 1000;
6
- const SECONDS_PER_HOUR = 3600;
7
- const SECONDS_PER_MINUTE = 60;
8
- 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
9
9
 
10
10
  interface FormatDurationInputExternalApi {
11
- startedAt: string | null;
12
- completedAt: string | null;
11
+ startedAt: string | null
12
+ completedAt: string | null
13
13
  }
14
14
 
15
15
  function formatDuration({ startedAt, completedAt }: FormatDurationInputExternalApi): string {
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`;
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`
33
33
  }
34
34
 
35
35
  // shared tick: one interval drives all LiveDuration subscribers
36
- const callbacks = new Set<() => void>();
36
+ const callbacks = new Set<() => void>()
37
37
  const tickState: { intervalId: Option.Option<ReturnType<typeof setInterval>> } = {
38
- intervalId: Option.none(),
39
- };
38
+ intervalId: Option.none(),
39
+ }
40
40
 
41
41
  function subscribeLiveTick(callback: () => void): () => void {
42
- callbacks.add(callback);
43
- if (Option.isNone(tickState.intervalId)) {
44
- // oxlint-disable-next-line reform-rules/no-set-timeout-interval -- browser-timer seam: shared render ticker for React, no Effect runtime here
45
- tickState.intervalId = Option.some(setInterval(() => {
46
- callbacks.forEach((cb) => cb());
47
- }, TICK_INTERVAL_MS));
48
- }
49
- return () => {
50
- callbacks.delete(callback);
51
- if (callbacks.size === 0 && Option.isSome(tickState.intervalId)) {
52
- clearInterval(tickState.intervalId.value);
53
- tickState.intervalId = Option.none();
54
- }
55
- };
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
+ }
56
58
  }
57
59
 
58
60
  interface LiveDurationPropsExternalApi {
59
- startedAt: string | null;
60
- completedAt?: string | null;
61
- className?: string;
61
+ startedAt: string | null
62
+ completedAt?: string | null
63
+ className?: string
62
64
  }
63
65
 
64
- export function LiveDuration({ startedAt, completedAt, className }: LiveDurationPropsExternalApi): React.ReactNode {
65
- const [, setTick] = useState(0);
66
+ export function LiveDuration({
67
+ startedAt,
68
+ completedAt,
69
+ className,
70
+ }: LiveDurationPropsExternalApi): React.ReactNode {
71
+ const [, setTick] = useState(0)
66
72
 
67
- useEffect(() => {
68
- if (!startedAt || completedAt != null) {
69
- return;
70
- }
71
- setTick(0);
72
- return subscribeLiveTick(() => setTick((prev) => prev + 1));
73
- }, [startedAt, completedAt]);
73
+ useEffect(() => {
74
+ if (!startedAt || completedAt != null) {
75
+ return
76
+ }
77
+ setTick(0)
78
+ return subscribeLiveTick(() => setTick((prev) => prev + 1))
79
+ }, [startedAt, completedAt])
74
80
 
75
- if (!startedAt) {
76
- return null;
77
- }
81
+ if (!startedAt) {
82
+ return null
83
+ }
78
84
 
79
- const formatted = formatDuration({ startedAt, completedAt: completedAt ?? null });
85
+ const formatted = formatDuration({ startedAt, completedAt: completedAt ?? null })
80
86
 
81
- return (
82
- <span className={className} suppressHydrationWarning>
83
- {formatted}
84
- </span>
85
- );
87
+ return (
88
+ <span className={className} suppressHydrationWarning>
89
+ {formatted}
90
+ </span>
91
+ )
86
92
  }
@@ -1,22 +1,18 @@
1
- import type * as React from "react";
2
- import { cn } from '../foundation/cn';
1
+ import type * as React from 'react'
2
+ import { cn } from '../foundation/cn'
3
3
 
4
4
  interface LogoPropsExternalApi {
5
- className?: string;
5
+ className?: string
6
6
  }
7
7
 
8
8
  export function Logo({ className }: LogoPropsExternalApi): React.JSX.Element {
9
- return (
10
- <span className={cn("font-mono text-sm font-medium tracking-tight", className)}>
11
- BETTER-HUB.
12
- </span>
13
- );
9
+ return (
10
+ <span className={cn('font-mono text-sm font-medium tracking-tight', className)}>
11
+ BETTER-HUB.
12
+ </span>
13
+ )
14
14
  }
15
15
 
16
16
  export function LogoMark({ className }: LogoPropsExternalApi): React.JSX.Element {
17
- return (
18
- <span className={cn("font-mono text-sm font-bold tracking-tight", className)}>
19
- b.
20
- </span>
21
- );
17
+ return <span className={cn('font-mono text-sm font-bold tracking-tight', className)}>b.</span>
22
18
  }