@glyphui/ui 0.0.0-stage → 0.1.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -2
  3. package/dist/data-table.d.ts +110 -0
  4. package/dist/data-table.js +225 -0
  5. package/dist/field-DXZD0l8w.js +107 -0
  6. package/dist/field-context-B8cYheXz.js +321 -0
  7. package/dist/form.d.ts +79 -0
  8. package/dist/form.js +84 -0
  9. package/dist/index-BWK4czIG.d.ts +233 -0
  10. package/dist/index.d.ts +2155 -0
  11. package/dist/index.js +1529 -0
  12. package/dist/registry-BGSJoiLq.d.ts +9 -0
  13. package/dist/states-BxPjCUxz.js +287 -0
  14. package/dist/tv-DuMh7UH2.d.ts +6 -0
  15. package/package.json +113 -3
  16. package/src/components/avatar/avatar.styles.ts +22 -0
  17. package/src/components/avatar/avatar.tsx +62 -0
  18. package/src/components/avatar/index.ts +2 -0
  19. package/src/components/badge/badge.styles.ts +69 -0
  20. package/src/components/badge/badge.tsx +30 -0
  21. package/src/components/badge/index.ts +2 -0
  22. package/src/components/button/button.styles.ts +43 -0
  23. package/src/components/button/button.tsx +77 -0
  24. package/src/components/button/index.ts +2 -0
  25. package/src/components/card/card.styles.ts +32 -0
  26. package/src/components/card/card.tsx +91 -0
  27. package/src/components/card/index.ts +11 -0
  28. package/src/components/checkbox/checkbox.tsx +39 -0
  29. package/src/components/checkbox/index.ts +1 -0
  30. package/src/components/container/container.tsx +35 -0
  31. package/src/components/container/index.ts +1 -0
  32. package/src/components/eyebrow/eyebrow.styles.ts +14 -0
  33. package/src/components/eyebrow/eyebrow.tsx +19 -0
  34. package/src/components/eyebrow/index.ts +2 -0
  35. package/src/components/field/field-context.ts +40 -0
  36. package/src/components/field/field.styles.ts +23 -0
  37. package/src/components/field/field.tsx +109 -0
  38. package/src/components/field/index.ts +3 -0
  39. package/src/components/heading/heading.styles.ts +16 -0
  40. package/src/components/heading/heading.tsx +26 -0
  41. package/src/components/heading/index.ts +2 -0
  42. package/src/components/input/index.ts +2 -0
  43. package/src/components/input/input.styles.ts +36 -0
  44. package/src/components/input/input.tsx +33 -0
  45. package/src/components/kbd/index.ts +1 -0
  46. package/src/components/kbd/kbd.tsx +21 -0
  47. package/src/components/label/index.ts +1 -0
  48. package/src/components/label/label.tsx +21 -0
  49. package/src/components/section/index.ts +1 -0
  50. package/src/components/section/section.tsx +39 -0
  51. package/src/components/segmented-control/index.ts +6 -0
  52. package/src/components/segmented-control/segmented-control.tsx +79 -0
  53. package/src/components/select/index.ts +1 -0
  54. package/src/components/select/select.tsx +133 -0
  55. package/src/components/separator/index.ts +1 -0
  56. package/src/components/separator/separator.tsx +29 -0
  57. package/src/components/skeleton/index.ts +1 -0
  58. package/src/components/skeleton/skeleton.tsx +20 -0
  59. package/src/components/spinner/index.ts +2 -0
  60. package/src/components/spinner/spinner.styles.ts +19 -0
  61. package/src/components/spinner/spinner.tsx +42 -0
  62. package/src/components/stack/index.ts +1 -0
  63. package/src/components/stack/stack.tsx +62 -0
  64. package/src/components/switch/index.ts +1 -0
  65. package/src/components/switch/switch.tsx +48 -0
  66. package/src/components/table/index.ts +10 -0
  67. package/src/components/table/table.tsx +90 -0
  68. package/src/components/tabs/index.ts +1 -0
  69. package/src/components/tabs/tabs.tsx +92 -0
  70. package/src/components/text/index.ts +2 -0
  71. package/src/components/text/text.styles.ts +32 -0
  72. package/src/components/text/text.tsx +17 -0
  73. package/src/components/textarea/index.ts +1 -0
  74. package/src/components/textarea/textarea.tsx +27 -0
  75. package/src/components/tooltip/index.ts +1 -0
  76. package/src/components/tooltip/tooltip.tsx +42 -0
  77. package/src/hooks/index.ts +4 -0
  78. package/src/hooks/use-component-config.ts +42 -0
  79. package/src/hooks/use-controllable-state.ts +27 -0
  80. package/src/hooks/use-media-query.ts +14 -0
  81. package/src/hooks/use-scrolled.ts +14 -0
  82. package/src/index.ts +52 -0
  83. package/src/internal/icons.tsx +99 -0
  84. package/src/patterns/app-shell/app-shell-context.ts +27 -0
  85. package/src/patterns/app-shell/app-shell.styles.ts +127 -0
  86. package/src/patterns/app-shell/app-shell.tsx +81 -0
  87. package/src/patterns/app-shell/index.ts +16 -0
  88. package/src/patterns/app-shell/navbar.tsx +119 -0
  89. package/src/patterns/app-shell/page-header.tsx +30 -0
  90. package/src/patterns/app-shell/sidebar.tsx +163 -0
  91. package/src/patterns/async/async.tsx +83 -0
  92. package/src/patterns/async/index.ts +10 -0
  93. package/src/patterns/async/states.tsx +128 -0
  94. package/src/patterns/data-table/data-table.tsx +367 -0
  95. package/src/patterns/data-table/index.ts +2 -0
  96. package/src/patterns/form/form.tsx +150 -0
  97. package/src/patterns/form/index.ts +13 -0
  98. package/src/provider/color-mode.tsx +133 -0
  99. package/src/provider/glyph-context.ts +42 -0
  100. package/src/provider/glyph-provider.tsx +41 -0
  101. package/src/provider/index.ts +18 -0
  102. package/src/provider/theme-scope.tsx +36 -0
  103. package/src/provider/theme-style.tsx +19 -0
  104. package/src/register.ts +69 -0
  105. package/src/themes.ts +56 -0
  106. package/src/utils/cn.ts +12 -0
  107. package/src/utils/compose-refs.ts +11 -0
  108. package/src/utils/registry.ts +9 -0
  109. package/src/utils/styles.ts +13 -0
  110. package/src/utils/tv.ts +7 -0
  111. package/src/utils/tw-merge-config.ts +14 -0
  112. package/styles.css +178 -0
@@ -0,0 +1,21 @@
1
+ import { Label as LabelPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { tv } from '../../utils/tv'
5
+
6
+ export const labelStyles = tv({
7
+ slots: {
8
+ base: 'inline-flex select-none items-center gap-1 text-small font-medium text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-60',
9
+ },
10
+ })
11
+
12
+ export function Label(inProps: ComponentProps<typeof LabelPrimitive.Root>) {
13
+ const [{ className, ...rest }, slots] = useComponentConfig('Label', inProps)
14
+ return (
15
+ <LabelPrimitive.Root
16
+ data-slot="label"
17
+ className={labelStyles().base({ class: [slots.base, className] })}
18
+ {...rest}
19
+ />
20
+ )
21
+ }
@@ -0,0 +1 @@
1
+ export { Section, type SectionProps, sectionStyles } from './section'
@@ -0,0 +1,39 @@
1
+ import { Slot } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const sectionStyles = tv({
6
+ base: 'w-full',
7
+ variants: {
8
+ spacing: {
9
+ none: 'py-0',
10
+ sm: 'py-10 sm:py-12',
11
+ md: 'py-16 sm:py-20',
12
+ lg: 'py-20 sm:py-28',
13
+ },
14
+ tone: {
15
+ default: '',
16
+ subtle: 'bg-subtle',
17
+ },
18
+ divided: { true: 'border-t-(length:--glyph-border-width) border-border' },
19
+ },
20
+ defaultVariants: { spacing: 'md', tone: 'default' },
21
+ })
22
+
23
+ export interface SectionProps
24
+ extends ComponentProps<'section'>,
25
+ VariantProps<typeof sectionStyles> {
26
+ asChild?: boolean
27
+ }
28
+
29
+ /** A full-width page band with consistent vertical rhythm. Put a Container inside. */
30
+ export function Section({ spacing, tone, divided, asChild, className, ...props }: SectionProps) {
31
+ const Comp = asChild ? Slot.Root : 'section'
32
+ return (
33
+ <Comp
34
+ data-slot="section"
35
+ className={sectionStyles({ spacing, tone, divided, class: className })}
36
+ {...props}
37
+ />
38
+ )
39
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ SegmentedControl,
3
+ type SegmentedControlProps,
4
+ type SegmentedOption,
5
+ segmentedControlStyles,
6
+ } from './segmented-control'
@@ -0,0 +1,79 @@
1
+ import { ToggleGroup } from 'radix-ui'
2
+ import type { ReactNode } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
5
+ import { tv, type VariantProps } from '../../utils/tv'
6
+
7
+ export const segmentedControlStyles = tv({
8
+ slots: {
9
+ root: [
10
+ 'inline-flex items-center gap-0.5 rounded-md border-border bg-muted p-0.5',
11
+ themedBorder,
12
+ ],
13
+ item: [
14
+ 'inline-flex h-full cursor-pointer select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-[calc(var(--radius-md)-2px)] px-3 font-medium text-muted-foreground',
15
+ transition,
16
+ focusRing,
17
+ 'hover:text-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-xs',
18
+ 'disabled:pointer-events-none disabled:opacity-50 [&>svg]:size-4',
19
+ ],
20
+ },
21
+ variants: {
22
+ size: {
23
+ sm: { root: 'h-(--glyph-control-sm)', item: 'px-2.5 text-caption' },
24
+ md: { root: 'h-(--glyph-control-md)', item: 'text-small' },
25
+ },
26
+ fullWidth: { true: { root: 'flex w-full', item: 'flex-1' } },
27
+ },
28
+ defaultVariants: { size: 'md' },
29
+ })
30
+
31
+ export interface SegmentedOption<T extends string> {
32
+ value: T
33
+ label: ReactNode
34
+ disabled?: boolean
35
+ }
36
+
37
+ export interface SegmentedControlProps<T extends string>
38
+ extends VariantProps<typeof segmentedControlStyles> {
39
+ options: readonly SegmentedOption<T>[]
40
+ value?: T
41
+ defaultValue?: T
42
+ onValueChange?: (value: T) => void
43
+ 'aria-label': string
44
+ className?: string
45
+ id?: string
46
+ }
47
+
48
+ /**
49
+ * Pick exactly one of a few options. `T` is inferred from `options`, so `onValueChange`
50
+ * receives your union type, not `string`.
51
+ */
52
+ export function SegmentedControl<T extends string>(inProps: SegmentedControlProps<T>) {
53
+ const [props, slots] = useComponentConfig('SegmentedControl', inProps)
54
+ const { options, value, defaultValue, onValueChange, size, fullWidth, className, ...rest } = props
55
+ const s = segmentedControlStyles({ size, fullWidth })
56
+ return (
57
+ <ToggleGroup.Root
58
+ type="single"
59
+ data-slot="segmented-control"
60
+ {...(value !== undefined && { value })}
61
+ {...(defaultValue !== undefined && { defaultValue })}
62
+ // Radix emits '' when the active item is clicked again; a segmented control never deselects.
63
+ onValueChange={(next: string) => next && onValueChange?.(next as T)}
64
+ className={s.root({ class: [slots.root, className] })}
65
+ {...rest}
66
+ >
67
+ {options.map((option) => (
68
+ <ToggleGroup.Item
69
+ key={option.value}
70
+ value={option.value}
71
+ disabled={option.disabled}
72
+ className={s.item({ class: slots.item })}
73
+ >
74
+ {option.label}
75
+ </ToggleGroup.Item>
76
+ ))}
77
+ </ToggleGroup.Root>
78
+ )
79
+ }
@@ -0,0 +1 @@
1
+ export { Select, SelectItem, type SelectItemProps, type SelectProps, selectStyles } from './select'
@@ -0,0 +1,133 @@
1
+ import { Select as SelectPrimitive } from 'radix-ui'
2
+ import type { ComponentProps, ReactNode } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { CheckIcon, ChevronDownIcon } from '../../internal/icons'
5
+ import { popover } from '../../utils/styles'
6
+ import { tv, type VariantProps } from '../../utils/tv'
7
+ import { useFieldControl } from '../field/field-context'
8
+ import { fieldControl } from '../input/input.styles'
9
+
10
+ export const selectStyles = tv({
11
+ slots: {
12
+ trigger: [
13
+ ...fieldControl,
14
+ 'flex cursor-pointer items-center justify-between gap-2 px-3 text-left outline-none',
15
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20',
16
+ 'data-placeholder:text-faint-foreground disabled:cursor-not-allowed disabled:opacity-50',
17
+ '[&>span]:truncate',
18
+ ],
19
+ icon: 'size-4 shrink-0 text-faint-foreground',
20
+ content: [
21
+ popover,
22
+ 'max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width)',
23
+ ],
24
+ viewport: 'p-1',
25
+ item: [
26
+ 'relative flex h-8 cursor-pointer select-none items-center gap-2 rounded-sm pr-8 pl-2 text-small outline-none',
27
+ 'data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50',
28
+ ],
29
+ indicator: 'absolute right-2 flex items-center text-primary [&>svg]:size-4',
30
+ },
31
+ variants: {
32
+ size: {
33
+ sm: { trigger: 'h-(--glyph-control-sm) text-small' },
34
+ md: { trigger: 'h-(--glyph-control-md) text-small' },
35
+ lg: { trigger: 'h-(--glyph-control-lg) text-body' },
36
+ },
37
+ },
38
+ defaultVariants: { size: 'md' },
39
+ })
40
+
41
+ type RootProps<T extends string> = Omit<
42
+ ComponentProps<typeof SelectPrimitive.Root>,
43
+ 'value' | 'defaultValue' | 'onValueChange'
44
+ > & {
45
+ value?: T
46
+ defaultValue?: T
47
+ onValueChange?: (value: T) => void
48
+ }
49
+
50
+ export interface SelectProps<T extends string = string>
51
+ extends RootProps<T>,
52
+ VariantProps<typeof selectStyles> {
53
+ placeholder?: ReactNode
54
+ id?: string
55
+ className?: string
56
+ contentClassName?: string
57
+ 'aria-label'?: string
58
+ 'aria-describedby'?: string
59
+ 'aria-invalid'?: boolean
60
+ }
61
+
62
+ /**
63
+ * A dropdown select. `value` is typed by the `T` you use:
64
+ * `<Select<Plan> value={plan} onValueChange={setPlan}>` only accepts `Plan` values.
65
+ */
66
+ export function Select<T extends string = string>(inProps: SelectProps<T>) {
67
+ const [props, slots] = useComponentConfig('Select', useFieldControl(inProps))
68
+ const {
69
+ size,
70
+ placeholder,
71
+ id,
72
+ className,
73
+ contentClassName,
74
+ children,
75
+ onValueChange,
76
+ 'aria-label': ariaLabel,
77
+ 'aria-describedby': ariaDescribedBy,
78
+ 'aria-invalid': ariaInvalid,
79
+ ...rootProps
80
+ } = props
81
+ const s = selectStyles({ size })
82
+ return (
83
+ <SelectPrimitive.Root
84
+ {...rootProps}
85
+ {...(onValueChange && { onValueChange: (v: string) => onValueChange(v as T) })}
86
+ >
87
+ <SelectPrimitive.Trigger
88
+ id={id}
89
+ data-slot="select-trigger"
90
+ aria-label={ariaLabel}
91
+ aria-describedby={ariaDescribedBy}
92
+ aria-invalid={ariaInvalid}
93
+ className={s.trigger({ class: [slots.trigger, className] })}
94
+ >
95
+ <SelectPrimitive.Value placeholder={placeholder} />
96
+ <SelectPrimitive.Icon asChild>
97
+ <ChevronDownIcon className={s.icon({ class: slots.icon })} />
98
+ </SelectPrimitive.Icon>
99
+ </SelectPrimitive.Trigger>
100
+ <SelectPrimitive.Portal>
101
+ <SelectPrimitive.Content
102
+ position="popper"
103
+ sideOffset={4}
104
+ data-slot="select-content"
105
+ className={s.content({ class: [slots.content, contentClassName] })}
106
+ >
107
+ <SelectPrimitive.Viewport className={s.viewport({ class: slots.viewport })}>
108
+ {children}
109
+ </SelectPrimitive.Viewport>
110
+ </SelectPrimitive.Content>
111
+ </SelectPrimitive.Portal>
112
+ </SelectPrimitive.Root>
113
+ )
114
+ }
115
+
116
+ export interface SelectItemProps extends ComponentProps<typeof SelectPrimitive.Item> {}
117
+
118
+ export function SelectItem({ className, children, ...props }: SelectItemProps) {
119
+ const [, slots] = useComponentConfig('Select', {})
120
+ const s = selectStyles()
121
+ return (
122
+ <SelectPrimitive.Item
123
+ data-slot="select-item"
124
+ className={s.item({ class: [slots.item, className] })}
125
+ {...props}
126
+ >
127
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
128
+ <SelectPrimitive.ItemIndicator className={s.indicator({ class: slots.indicator })}>
129
+ <CheckIcon />
130
+ </SelectPrimitive.ItemIndicator>
131
+ </SelectPrimitive.Item>
132
+ )
133
+ }
@@ -0,0 +1 @@
1
+ export { Separator, separatorStyles } from './separator'
@@ -0,0 +1,29 @@
1
+ import { Separator as SeparatorPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { tv } from '../../utils/tv'
5
+
6
+ export const separatorStyles = tv({
7
+ slots: { base: 'shrink-0 bg-border' },
8
+ variants: {
9
+ orientation: {
10
+ horizontal: { base: 'h-px w-full' },
11
+ vertical: { base: 'h-full w-px self-stretch' },
12
+ },
13
+ },
14
+ defaultVariants: { orientation: 'horizontal' },
15
+ })
16
+
17
+ export function Separator(inProps: ComponentProps<typeof SeparatorPrimitive.Root>) {
18
+ const [props, slots] = useComponentConfig('Separator', inProps)
19
+ const { orientation = 'horizontal', decorative = true, className, ...rest } = props
20
+ return (
21
+ <SeparatorPrimitive.Root
22
+ data-slot="separator"
23
+ orientation={orientation}
24
+ decorative={decorative}
25
+ className={separatorStyles({ orientation }).base({ class: [slots.base, className] })}
26
+ {...rest}
27
+ />
28
+ )
29
+ }
@@ -0,0 +1 @@
1
+ export { Skeleton, skeletonStyles } from './skeleton'
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { tv } from '../../utils/tv'
4
+
5
+ export const skeletonStyles = tv({
6
+ slots: { base: 'animate-pulse rounded-sm bg-muted' },
7
+ })
8
+
9
+ /** Placeholder block shown while content loads. Size it with classes: `h-4 w-32`. */
10
+ export function Skeleton(inProps: ComponentProps<'div'>) {
11
+ const [{ className, ...rest }, slots] = useComponentConfig('Skeleton', inProps)
12
+ return (
13
+ <div
14
+ data-slot="skeleton"
15
+ aria-hidden="true"
16
+ className={skeletonStyles().base({ class: [slots.base, className] })}
17
+ {...rest}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,2 @@
1
+ export { Spinner, type SpinnerProps } from './spinner'
2
+ export { type SpinnerVariants, spinnerStyles } from './spinner.styles'
@@ -0,0 +1,19 @@
1
+ import { tv, type VariantProps } from '../../utils/tv'
2
+
3
+ export const spinnerStyles = tv({
4
+ slots: {
5
+ base: 'inline-block shrink-0 animate-spin',
6
+ track: 'opacity-20',
7
+ arc: '',
8
+ },
9
+ variants: {
10
+ size: {
11
+ sm: { base: 'size-3.5' },
12
+ md: { base: 'size-4' },
13
+ lg: { base: 'size-6' },
14
+ },
15
+ },
16
+ defaultVariants: { size: 'md' },
17
+ })
18
+
19
+ export type SpinnerVariants = VariantProps<typeof spinnerStyles>
@@ -0,0 +1,42 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { type SpinnerVariants, spinnerStyles } from './spinner.styles'
4
+
5
+ export interface SpinnerProps extends Omit<ComponentProps<'svg'>, 'children'>, SpinnerVariants {
6
+ /** Accessible label. Omit when the spinner sits inside an element that already says it's busy. */
7
+ label?: string
8
+ }
9
+
10
+ export function Spinner(inProps: SpinnerProps) {
11
+ const [props, slots] = useComponentConfig('Spinner', inProps)
12
+ const { size, label, className, ...rest } = props
13
+ const s = spinnerStyles({ size })
14
+ return (
15
+ <svg
16
+ data-slot="spinner"
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ role={label ? 'status' : undefined}
20
+ aria-label={label}
21
+ aria-hidden={label ? undefined : true}
22
+ className={s.base({ class: [slots.base, className] })}
23
+ {...rest}
24
+ >
25
+ <circle
26
+ className={s.track({ class: slots.track })}
27
+ cx="12"
28
+ cy="12"
29
+ r="9"
30
+ stroke="currentColor"
31
+ strokeWidth="3"
32
+ />
33
+ <path
34
+ className={s.arc({ class: slots.arc })}
35
+ d="M21 12a9 9 0 0 0-9-9"
36
+ stroke="currentColor"
37
+ strokeWidth="3"
38
+ strokeLinecap="round"
39
+ />
40
+ </svg>
41
+ )
42
+ }
@@ -0,0 +1 @@
1
+ export { Stack, type StackProps, stackStyles } from './stack'
@@ -0,0 +1,62 @@
1
+ import { Slot } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const stackStyles = tv({
6
+ base: 'flex',
7
+ variants: {
8
+ direction: { column: 'flex-col', row: 'flex-row' },
9
+ gap: {
10
+ 0: 'gap-0',
11
+ 1: 'gap-1',
12
+ 2: 'gap-2',
13
+ 3: 'gap-3',
14
+ 4: 'gap-4',
15
+ 5: 'gap-5',
16
+ 6: 'gap-6',
17
+ 8: 'gap-8',
18
+ 10: 'gap-10',
19
+ 12: 'gap-12',
20
+ },
21
+ align: {
22
+ start: 'items-start',
23
+ center: 'items-center',
24
+ end: 'items-end',
25
+ stretch: 'items-stretch',
26
+ baseline: 'items-baseline',
27
+ },
28
+ justify: {
29
+ start: 'justify-start',
30
+ center: 'justify-center',
31
+ end: 'justify-end',
32
+ between: 'justify-between',
33
+ },
34
+ wrap: { true: 'flex-wrap' },
35
+ },
36
+ defaultVariants: { direction: 'column', gap: 4 },
37
+ })
38
+
39
+ export interface StackProps extends ComponentProps<'div'>, VariantProps<typeof stackStyles> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ /** Flex layout with spacing from the scale. `direction="row"` for horizontal. */
44
+ export function Stack({
45
+ direction,
46
+ gap,
47
+ align,
48
+ justify,
49
+ wrap,
50
+ asChild,
51
+ className,
52
+ ...props
53
+ }: StackProps) {
54
+ const Comp = asChild ? Slot.Root : 'div'
55
+ return (
56
+ <Comp
57
+ data-slot="stack"
58
+ className={stackStyles({ direction, gap, align, justify, wrap, class: className })}
59
+ {...props}
60
+ />
61
+ )
62
+ }
@@ -0,0 +1 @@
1
+ export { Switch, type SwitchProps, switchStyles } from './switch'
@@ -0,0 +1,48 @@
1
+ import { Switch as SwitchPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
5
+ import { tv, type VariantProps } from '../../utils/tv'
6
+ import { useFieldControl } from '../field/field-context'
7
+
8
+ export const switchStyles = tv({
9
+ slots: {
10
+ root: [
11
+ 'peer inline-flex shrink-0 cursor-pointer items-center rounded-pill border-transparent bg-input p-px',
12
+ themedBorder,
13
+ transition,
14
+ focusRing,
15
+ 'data-[state=checked]:bg-primary disabled:cursor-not-allowed disabled:opacity-50',
16
+ ],
17
+ thumb:
18
+ 'pointer-events-none block rounded-pill bg-white shadow-sm transition-transform duration-(--glyph-duration-base) ease-emphasized data-[state=unchecked]:translate-x-0',
19
+ },
20
+ variants: {
21
+ size: {
22
+ sm: { root: 'h-4 w-7', thumb: 'size-3 data-[state=checked]:translate-x-3' },
23
+ md: { root: 'h-5 w-9', thumb: 'size-4 data-[state=checked]:translate-x-4' },
24
+ },
25
+ },
26
+ defaultVariants: { size: 'md' },
27
+ })
28
+
29
+ export interface SwitchProps
30
+ extends ComponentProps<typeof SwitchPrimitive.Root>,
31
+ VariantProps<typeof switchStyles> {}
32
+
33
+ export function Switch(inProps: SwitchProps) {
34
+ const [{ size, className, ...rest }, slots] = useComponentConfig(
35
+ 'Switch',
36
+ useFieldControl(inProps),
37
+ )
38
+ const s = switchStyles({ size })
39
+ return (
40
+ <SwitchPrimitive.Root
41
+ data-slot="switch"
42
+ className={s.root({ class: [slots.root, className] })}
43
+ {...rest}
44
+ >
45
+ <SwitchPrimitive.Thumb className={s.thumb({ class: slots.thumb })} />
46
+ </SwitchPrimitive.Root>
47
+ )
48
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ Table,
3
+ TableBody,
4
+ TableCaption,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ tableStyles,
10
+ } from './table'
@@ -0,0 +1,90 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { tv } from '../../utils/tv'
4
+
5
+ export const tableStyles = tv({
6
+ slots: {
7
+ wrapper: 'relative w-full overflow-x-auto',
8
+ table: 'w-full caption-bottom border-collapse text-small tabular-nums',
9
+ header: '[&_tr]:border-b [&_tr]:border-border',
10
+ body: '[&_tr:last-child]:border-0',
11
+ row: 'border-b border-border transition-colors duration-(--glyph-duration-fast) hover:bg-muted/50 data-[state=selected]:bg-primary/6',
12
+ head: 'h-10 whitespace-nowrap px-3 first:pl-4 last:pr-4 text-left align-middle text-caption font-medium text-muted-foreground [&:has([role=checkbox])]:w-10 [&:has([role=checkbox])]:pr-0',
13
+ cell: 'px-3 py-2.5 align-middle first:pl-4 last:pr-4 [&:has([role=checkbox])]:pr-0',
14
+ caption: 'mt-3 text-small text-faint-foreground',
15
+ },
16
+ })
17
+
18
+ const s = tableStyles()
19
+
20
+ function useSlots() {
21
+ return useComponentConfig('Table', {})[1]
22
+ }
23
+
24
+ /** Semantic table primitives styled by the theme. For sorting and pagination use DataTable. */
25
+ export function Table({ className, ...props }: ComponentProps<'table'>) {
26
+ const slots = useSlots()
27
+ return (
28
+ <div data-slot="table-wrapper" className={s.wrapper({ class: slots.wrapper })}>
29
+ <table
30
+ data-slot="table"
31
+ className={s.table({ class: [slots.table, className] })}
32
+ {...props}
33
+ />
34
+ </div>
35
+ )
36
+ }
37
+
38
+ export function TableHeader({ className, ...props }: ComponentProps<'thead'>) {
39
+ const slots = useSlots()
40
+ return (
41
+ <thead
42
+ data-slot="table-header"
43
+ className={s.header({ class: [slots.header, className] })}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ export function TableBody({ className, ...props }: ComponentProps<'tbody'>) {
50
+ const slots = useSlots()
51
+ return (
52
+ <tbody
53
+ data-slot="table-body"
54
+ className={s.body({ class: [slots.body, className] })}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export function TableRow({ className, ...props }: ComponentProps<'tr'>) {
61
+ const slots = useSlots()
62
+ return (
63
+ <tr data-slot="table-row" className={s.row({ class: [slots.row, className] })} {...props} />
64
+ )
65
+ }
66
+
67
+ export function TableHead({ className, ...props }: ComponentProps<'th'>) {
68
+ const slots = useSlots()
69
+ return (
70
+ <th data-slot="table-head" className={s.head({ class: [slots.head, className] })} {...props} />
71
+ )
72
+ }
73
+
74
+ export function TableCell({ className, ...props }: ComponentProps<'td'>) {
75
+ const slots = useSlots()
76
+ return (
77
+ <td data-slot="table-cell" className={s.cell({ class: [slots.cell, className] })} {...props} />
78
+ )
79
+ }
80
+
81
+ export function TableCaption({ className, ...props }: ComponentProps<'caption'>) {
82
+ const slots = useSlots()
83
+ return (
84
+ <caption
85
+ data-slot="table-caption"
86
+ className={s.caption({ class: [slots.caption, className] })}
87
+ {...props}
88
+ />
89
+ )
90
+ }
@@ -0,0 +1 @@
1
+ export { Tabs, TabsContent, TabsList, type TabsProps, TabsTrigger, tabsStyles } from './tabs'