@glyphui/ui 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/data-table.d.ts +110 -0
- package/dist/data-table.js +225 -0
- package/dist/field-DXZD0l8w.js +107 -0
- package/dist/field-context-B8cYheXz.js +321 -0
- package/dist/form.d.ts +79 -0
- package/dist/form.js +84 -0
- package/dist/index-BWK4czIG.d.ts +233 -0
- package/dist/index.d.ts +2155 -0
- package/dist/index.js +1529 -0
- package/dist/registry-BGSJoiLq.d.ts +9 -0
- package/dist/states-BxPjCUxz.js +287 -0
- package/dist/tv-DuMh7UH2.d.ts +6 -0
- package/package.json +113 -3
- package/src/components/avatar/avatar.styles.ts +22 -0
- package/src/components/avatar/avatar.tsx +62 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/badge/badge.styles.ts +69 -0
- package/src/components/badge/badge.tsx +30 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/button/button.styles.ts +43 -0
- package/src/components/button/button.tsx +77 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.styles.ts +32 -0
- package/src/components/card/card.tsx +91 -0
- package/src/components/card/index.ts +11 -0
- package/src/components/checkbox/checkbox.tsx +39 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/container/container.tsx +35 -0
- package/src/components/container/index.ts +1 -0
- package/src/components/eyebrow/eyebrow.styles.ts +14 -0
- package/src/components/eyebrow/eyebrow.tsx +19 -0
- package/src/components/eyebrow/index.ts +2 -0
- package/src/components/field/field-context.ts +40 -0
- package/src/components/field/field.styles.ts +23 -0
- package/src/components/field/field.tsx +109 -0
- package/src/components/field/index.ts +3 -0
- package/src/components/heading/heading.styles.ts +16 -0
- package/src/components/heading/heading.tsx +26 -0
- package/src/components/heading/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.styles.ts +36 -0
- package/src/components/input/input.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.tsx +21 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +21 -0
- package/src/components/section/index.ts +1 -0
- package/src/components/section/section.tsx +39 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.tsx +79 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +133 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +29 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +20 -0
- package/src/components/spinner/index.ts +2 -0
- package/src/components/spinner/spinner.styles.ts +19 -0
- package/src/components/spinner/spinner.tsx +42 -0
- package/src/components/stack/index.ts +1 -0
- package/src/components/stack/stack.tsx +62 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +48 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.tsx +90 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +92 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.styles.ts +32 -0
- package/src/components/text/text.tsx +17 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +27 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +42 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-component-config.ts +42 -0
- package/src/hooks/use-controllable-state.ts +27 -0
- package/src/hooks/use-media-query.ts +14 -0
- package/src/hooks/use-scrolled.ts +14 -0
- package/src/index.ts +52 -0
- package/src/internal/icons.tsx +99 -0
- package/src/patterns/app-shell/app-shell-context.ts +27 -0
- package/src/patterns/app-shell/app-shell.styles.ts +127 -0
- package/src/patterns/app-shell/app-shell.tsx +81 -0
- package/src/patterns/app-shell/index.ts +16 -0
- package/src/patterns/app-shell/navbar.tsx +119 -0
- package/src/patterns/app-shell/page-header.tsx +30 -0
- package/src/patterns/app-shell/sidebar.tsx +163 -0
- package/src/patterns/async/async.tsx +83 -0
- package/src/patterns/async/index.ts +10 -0
- package/src/patterns/async/states.tsx +128 -0
- package/src/patterns/data-table/data-table.tsx +367 -0
- package/src/patterns/data-table/index.ts +2 -0
- package/src/patterns/form/form.tsx +150 -0
- package/src/patterns/form/index.ts +13 -0
- package/src/provider/color-mode.tsx +133 -0
- package/src/provider/glyph-context.ts +42 -0
- package/src/provider/glyph-provider.tsx +41 -0
- package/src/provider/index.ts +18 -0
- package/src/provider/theme-scope.tsx +36 -0
- package/src/provider/theme-style.tsx +19 -0
- package/src/register.ts +69 -0
- package/src/themes.ts +56 -0
- package/src/utils/cn.ts +12 -0
- package/src/utils/compose-refs.ts +11 -0
- package/src/utils/registry.ts +9 -0
- package/src/utils/styles.ts +13 -0
- package/src/utils/tv.ts +7 -0
- package/src/utils/tw-merge-config.ts +14 -0
- package/styles.css +178 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from 'radix-ui'
|
|
2
|
+
import { type ComponentProps, createContext, useContext } 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 tabsStyles = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
root: 'flex flex-col gap-4',
|
|
10
|
+
list: 'flex items-center',
|
|
11
|
+
trigger: [
|
|
12
|
+
'relative inline-flex cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap text-small font-medium text-muted-foreground',
|
|
13
|
+
transition,
|
|
14
|
+
focusRing,
|
|
15
|
+
'hover:text-foreground data-[state=active]:text-foreground disabled:pointer-events-none disabled:opacity-50',
|
|
16
|
+
'[&>svg]:size-4',
|
|
17
|
+
],
|
|
18
|
+
content: 'outline-none',
|
|
19
|
+
},
|
|
20
|
+
variants: {
|
|
21
|
+
variant: {
|
|
22
|
+
underline: {
|
|
23
|
+
list: 'gap-5 border-b border-border',
|
|
24
|
+
trigger:
|
|
25
|
+
'h-9 after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:rounded-pill after:bg-transparent data-[state=active]:after:bg-foreground',
|
|
26
|
+
},
|
|
27
|
+
pill: {
|
|
28
|
+
list: ['inline-flex w-fit gap-0.5 rounded-md border-border bg-muted p-0.5', themedBorder],
|
|
29
|
+
trigger:
|
|
30
|
+
'h-7 rounded-[calc(var(--radius-md)-2px)] px-3 data-[state=active]:bg-raised data-[state=active]:shadow-xs',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: { variant: 'underline' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
type TabsVariants = VariantProps<typeof tabsStyles>
|
|
38
|
+
const TabsContext = createContext<TabsVariants>({})
|
|
39
|
+
|
|
40
|
+
export interface TabsProps extends ComponentProps<typeof TabsPrimitive.Root>, TabsVariants {}
|
|
41
|
+
|
|
42
|
+
export function Tabs(inProps: TabsProps) {
|
|
43
|
+
const [{ variant, className, ...rest }, slots] = useComponentConfig('Tabs', inProps)
|
|
44
|
+
return (
|
|
45
|
+
<TabsContext.Provider value={{ variant }}>
|
|
46
|
+
<TabsPrimitive.Root
|
|
47
|
+
data-slot="tabs"
|
|
48
|
+
className={tabsStyles({ variant }).root({ class: [slots.root, className] })}
|
|
49
|
+
{...rest}
|
|
50
|
+
/>
|
|
51
|
+
</TabsContext.Provider>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function useTabsParts() {
|
|
56
|
+
const { variant } = useContext(TabsContext)
|
|
57
|
+
const [, slots] = useComponentConfig('Tabs', {})
|
|
58
|
+
return { s: tabsStyles({ variant }), slots }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function TabsList({ className, ...props }: ComponentProps<typeof TabsPrimitive.List>) {
|
|
62
|
+
const { s, slots } = useTabsParts()
|
|
63
|
+
return (
|
|
64
|
+
<TabsPrimitive.List
|
|
65
|
+
data-slot="tabs-list"
|
|
66
|
+
className={s.list({ class: [slots.list, className] })}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function TabsTrigger({ className, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
73
|
+
const { s, slots } = useTabsParts()
|
|
74
|
+
return (
|
|
75
|
+
<TabsPrimitive.Trigger
|
|
76
|
+
data-slot="tabs-trigger"
|
|
77
|
+
className={s.trigger({ class: [slots.trigger, className] })}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function TabsContent({ className, ...props }: ComponentProps<typeof TabsPrimitive.Content>) {
|
|
84
|
+
const { s, slots } = useTabsParts()
|
|
85
|
+
return (
|
|
86
|
+
<TabsPrimitive.Content
|
|
87
|
+
data-slot="tabs-content"
|
|
88
|
+
className={s.content({ class: [slots.content, className] })}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
2
|
+
|
|
3
|
+
export const textStyles = tv({
|
|
4
|
+
slots: { base: '' },
|
|
5
|
+
variants: {
|
|
6
|
+
size: {
|
|
7
|
+
lead: { base: 'text-lead' },
|
|
8
|
+
body: { base: 'text-body' },
|
|
9
|
+
small: { base: 'text-small' },
|
|
10
|
+
caption: { base: 'text-caption' },
|
|
11
|
+
},
|
|
12
|
+
tone: {
|
|
13
|
+
default: { base: 'text-foreground' },
|
|
14
|
+
muted: { base: 'text-muted-foreground' },
|
|
15
|
+
faint: { base: 'text-faint-foreground' },
|
|
16
|
+
primary: { base: 'text-primary' },
|
|
17
|
+
success: { base: 'text-success' },
|
|
18
|
+
warning: { base: 'text-warning' },
|
|
19
|
+
danger: { base: 'text-danger' },
|
|
20
|
+
},
|
|
21
|
+
weight: {
|
|
22
|
+
regular: { base: 'font-normal' },
|
|
23
|
+
medium: { base: 'font-medium' },
|
|
24
|
+
semibold: { base: 'font-semibold' },
|
|
25
|
+
},
|
|
26
|
+
mono: { true: { base: 'font-mono' } },
|
|
27
|
+
truncate: { true: { base: 'truncate' } },
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { size: 'body', tone: 'default' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export type TextVariants = VariantProps<typeof textStyles>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Slot } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { type TextVariants, textStyles } from './text.styles'
|
|
5
|
+
|
|
6
|
+
export interface TextProps extends ComponentProps<'p'>, TextVariants {
|
|
7
|
+
/** Render the child element (e.g. `<span>`, `<label>`) with text styles instead of a `<p>`. */
|
|
8
|
+
asChild?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Text(inProps: TextProps) {
|
|
12
|
+
const [props, slots] = useComponentConfig('Text', inProps)
|
|
13
|
+
const { size, tone, weight, mono, truncate, asChild, className, ...rest } = props
|
|
14
|
+
const s = textStyles({ size, tone, weight, mono, truncate })
|
|
15
|
+
const Comp = asChild ? Slot.Root : 'p'
|
|
16
|
+
return <Comp data-slot="text" className={s.base({ class: [slots.base, className] })} {...rest} />
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Textarea, textareaStyles } from './textarea'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { tv } from '../../utils/tv'
|
|
4
|
+
import { useFieldControl } from '../field/field-context'
|
|
5
|
+
import { fieldControl } from '../input/input.styles'
|
|
6
|
+
|
|
7
|
+
export const textareaStyles = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
base: [
|
|
10
|
+
...fieldControl,
|
|
11
|
+
'field-sizing-content min-h-20 resize-y rounded-[min(var(--radius-md),var(--radius-lg))] px-3 py-2 text-small',
|
|
12
|
+
'outline-none placeholder:text-faint-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20',
|
|
13
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export function Textarea(inProps: ComponentProps<'textarea'>) {
|
|
19
|
+
const [{ className, ...rest }, slots] = useComponentConfig('Textarea', useFieldControl(inProps))
|
|
20
|
+
return (
|
|
21
|
+
<textarea
|
|
22
|
+
data-slot="textarea"
|
|
23
|
+
className={textareaStyles().base({ class: [slots.base, className] })}
|
|
24
|
+
{...rest}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tooltip, type TooltipProps, tooltipStyles } from './tooltip'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { tv } from '../../utils/tv'
|
|
5
|
+
|
|
6
|
+
export const tooltipStyles = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
content:
|
|
9
|
+
'z-50 max-w-xs animate-pop-in rounded-sm bg-secondary px-2 py-1 text-caption text-secondary-foreground shadow-md',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export interface TooltipProps
|
|
14
|
+
extends Omit<ComponentProps<typeof TooltipPrimitive.Content>, 'content'> {
|
|
15
|
+
content: ReactNode
|
|
16
|
+
children: ReactNode
|
|
17
|
+
/** Skip the tooltip entirely, e.g. when the label is already visible. */
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
delayDuration?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A short hint on hover or focus. The child must be focusable (a button or link). */
|
|
23
|
+
export function Tooltip(inProps: TooltipProps) {
|
|
24
|
+
const [props, slots] = useComponentConfig('Tooltip', inProps)
|
|
25
|
+
const { content, children, disabled, delayDuration, className, sideOffset = 6, ...rest } = props
|
|
26
|
+
if (disabled) return children
|
|
27
|
+
return (
|
|
28
|
+
<TooltipPrimitive.Root {...(delayDuration !== undefined && { delayDuration })}>
|
|
29
|
+
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
|
30
|
+
<TooltipPrimitive.Portal>
|
|
31
|
+
<TooltipPrimitive.Content
|
|
32
|
+
data-slot="tooltip"
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={tooltipStyles().content({ class: [slots.content, className] })}
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
{content}
|
|
38
|
+
</TooltipPrimitive.Content>
|
|
39
|
+
</TooltipPrimitive.Portal>
|
|
40
|
+
</TooltipPrimitive.Root>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentName, GlyphComponentRegistry } from '@glyphui/tokens'
|
|
2
|
+
import { useContext } from 'react'
|
|
3
|
+
import { GlyphContext } from '../provider/glyph-context'
|
|
4
|
+
|
|
5
|
+
type SlotsOf<K extends ComponentName> = GlyphComponentRegistry[K] extends {
|
|
6
|
+
slots: infer S extends string
|
|
7
|
+
}
|
|
8
|
+
? S
|
|
9
|
+
: never
|
|
10
|
+
|
|
11
|
+
export type SlotClasses<K extends ComponentName> = Partial<Record<SlotsOf<K>, string>>
|
|
12
|
+
|
|
13
|
+
interface LooseOverride {
|
|
14
|
+
defaultVariants?: Record<string, unknown>
|
|
15
|
+
className?: Record<string, string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Applies the theme's per-component settings (`theme.components[name]`):
|
|
20
|
+
* fills unset variant props from `defaultVariants` and returns the extra slot classes.
|
|
21
|
+
* Every styled component calls this first.
|
|
22
|
+
*/
|
|
23
|
+
export function useComponentConfig<K extends ComponentName, P extends object>(
|
|
24
|
+
name: K,
|
|
25
|
+
props: P,
|
|
26
|
+
): [props: P, slotClasses: SlotClasses<K>] {
|
|
27
|
+
const theme = useContext(GlyphContext)?.theme
|
|
28
|
+
const override = theme?.components[name] as LooseOverride | undefined
|
|
29
|
+
if (!override) return [props, {}]
|
|
30
|
+
|
|
31
|
+
let merged = props
|
|
32
|
+
const defaults = override.defaultVariants
|
|
33
|
+
if (defaults) {
|
|
34
|
+
merged = { ...props }
|
|
35
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
36
|
+
if ((merged as Record<string, unknown>)[key] === undefined) {
|
|
37
|
+
;(merged as Record<string, unknown>)[key] = value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [merged, (override.className ?? {}) as SlotClasses<K>]
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface Options<T> {
|
|
4
|
+
value?: T | undefined
|
|
5
|
+
defaultValue: T
|
|
6
|
+
onChange?: ((value: T) => void) | undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** State that can be controlled by a parent (`value` + `onChange`) or left uncontrolled. */
|
|
10
|
+
export function useControllableState<T>({ value, defaultValue, onChange }: Options<T>) {
|
|
11
|
+
const [internal, setInternal] = useState(defaultValue)
|
|
12
|
+
const isControlled = value !== undefined
|
|
13
|
+
const current = isControlled ? value : internal
|
|
14
|
+
const onChangeRef = useRef(onChange)
|
|
15
|
+
onChangeRef.current = onChange
|
|
16
|
+
|
|
17
|
+
const setValue = useCallback(
|
|
18
|
+
(next: T | ((prev: T) => T)) => {
|
|
19
|
+
const resolved = typeof next === 'function' ? (next as (prev: T) => T)(current) : next
|
|
20
|
+
if (!isControlled) setInternal(resolved)
|
|
21
|
+
onChangeRef.current?.(resolved)
|
|
22
|
+
},
|
|
23
|
+
[current, isControlled],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return [current, setValue] as const
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Subscribes to a CSS media query. Returns `fallback` during SSR. */
|
|
4
|
+
export function useMediaQuery(query: string, fallback = false): boolean {
|
|
5
|
+
return useSyncExternalStore(
|
|
6
|
+
(onChange) => {
|
|
7
|
+
const list = window.matchMedia(query)
|
|
8
|
+
list.addEventListener('change', onChange)
|
|
9
|
+
return () => list.removeEventListener('change', onChange)
|
|
10
|
+
},
|
|
11
|
+
() => window.matchMedia(query).matches,
|
|
12
|
+
() => fallback,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/** True once the window has scrolled past `threshold` pixels. */
|
|
4
|
+
export function useScrolled(threshold = 8, enabled = true): boolean {
|
|
5
|
+
const [scrolled, setScrolled] = useState(false)
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!enabled) return
|
|
8
|
+
const update = () => setScrolled(window.scrollY > threshold)
|
|
9
|
+
update()
|
|
10
|
+
window.addEventListener('scroll', update, { passive: true })
|
|
11
|
+
return () => window.removeEventListener('scroll', update)
|
|
12
|
+
}, [threshold, enabled])
|
|
13
|
+
return scrolled
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import './register'
|
|
2
|
+
|
|
3
|
+
// Theme engine (re-exported so apps only import from @glyphui/ui).
|
|
4
|
+
export {
|
|
5
|
+
type ColorMode,
|
|
6
|
+
type ColorToken,
|
|
7
|
+
type ComponentOverrides,
|
|
8
|
+
defineTheme,
|
|
9
|
+
type RadiusToken,
|
|
10
|
+
type ShadowToken,
|
|
11
|
+
type TextToken,
|
|
12
|
+
type Theme,
|
|
13
|
+
type ThemeInput,
|
|
14
|
+
type Tone,
|
|
15
|
+
themeToCss,
|
|
16
|
+
tokenVar,
|
|
17
|
+
} from '@glyphui/tokens'
|
|
18
|
+
// Components
|
|
19
|
+
export * from './components/avatar'
|
|
20
|
+
export * from './components/badge'
|
|
21
|
+
export * from './components/button'
|
|
22
|
+
export * from './components/card'
|
|
23
|
+
export * from './components/checkbox'
|
|
24
|
+
export * from './components/container'
|
|
25
|
+
export * from './components/eyebrow'
|
|
26
|
+
export * from './components/field'
|
|
27
|
+
export * from './components/heading'
|
|
28
|
+
export * from './components/input'
|
|
29
|
+
export * from './components/kbd'
|
|
30
|
+
export * from './components/label'
|
|
31
|
+
export * from './components/section'
|
|
32
|
+
export * from './components/segmented-control'
|
|
33
|
+
export * from './components/select'
|
|
34
|
+
export * from './components/separator'
|
|
35
|
+
export * from './components/skeleton'
|
|
36
|
+
export * from './components/spinner'
|
|
37
|
+
export * from './components/stack'
|
|
38
|
+
export * from './components/switch'
|
|
39
|
+
export * from './components/table'
|
|
40
|
+
export * from './components/tabs'
|
|
41
|
+
export * from './components/text'
|
|
42
|
+
export * from './components/textarea'
|
|
43
|
+
export * from './components/tooltip'
|
|
44
|
+
export * from './hooks'
|
|
45
|
+
// Patterns
|
|
46
|
+
export * from './patterns/app-shell'
|
|
47
|
+
export * from './patterns/async'
|
|
48
|
+
// Providers and hooks
|
|
49
|
+
export * from './provider'
|
|
50
|
+
export { brutalist, editorial, glyph, type PresetName, playful, presets } from './themes'
|
|
51
|
+
export { type ClassValue, cn } from './utils/cn'
|
|
52
|
+
export { tv, type VariantProps } from './utils/tv'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Minimal internal icon set, so components don't force an icon library on consumers. */
|
|
4
|
+
type IconProps = SVGProps<SVGSVGElement>
|
|
5
|
+
|
|
6
|
+
function Icon({ children, ...props }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
width="16"
|
|
11
|
+
height="16"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
strokeWidth="2"
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
aria-hidden="true"
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</svg>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const CheckIcon = (p: IconProps) => (
|
|
27
|
+
<Icon {...p}>
|
|
28
|
+
<path d="M20 6 9 17l-5-5" />
|
|
29
|
+
</Icon>
|
|
30
|
+
)
|
|
31
|
+
export const MinusIcon = (p: IconProps) => (
|
|
32
|
+
<Icon {...p}>
|
|
33
|
+
<path d="M5 12h14" />
|
|
34
|
+
</Icon>
|
|
35
|
+
)
|
|
36
|
+
export const ChevronDownIcon = (p: IconProps) => (
|
|
37
|
+
<Icon {...p}>
|
|
38
|
+
<path d="m6 9 6 6 6-6" />
|
|
39
|
+
</Icon>
|
|
40
|
+
)
|
|
41
|
+
export const ChevronLeftIcon = (p: IconProps) => (
|
|
42
|
+
<Icon {...p}>
|
|
43
|
+
<path d="m15 18-6-6 6-6" />
|
|
44
|
+
</Icon>
|
|
45
|
+
)
|
|
46
|
+
export const ChevronRightIcon = (p: IconProps) => (
|
|
47
|
+
<Icon {...p}>
|
|
48
|
+
<path d="m9 18 6-6-6-6" />
|
|
49
|
+
</Icon>
|
|
50
|
+
)
|
|
51
|
+
export const ArrowUpIcon = (p: IconProps) => (
|
|
52
|
+
<Icon {...p}>
|
|
53
|
+
<path d="m5 12 7-7 7 7M12 19V5" />
|
|
54
|
+
</Icon>
|
|
55
|
+
)
|
|
56
|
+
export const ArrowDownIcon = (p: IconProps) => (
|
|
57
|
+
<Icon {...p}>
|
|
58
|
+
<path d="M12 5v14M19 12l-7 7-7-7" />
|
|
59
|
+
</Icon>
|
|
60
|
+
)
|
|
61
|
+
export const ChevronsUpDownIcon = (p: IconProps) => (
|
|
62
|
+
<Icon {...p}>
|
|
63
|
+
<path d="m7 15 5 5 5-5M7 9l5-5 5 5" />
|
|
64
|
+
</Icon>
|
|
65
|
+
)
|
|
66
|
+
export const XIcon = (p: IconProps) => (
|
|
67
|
+
<Icon {...p}>
|
|
68
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
69
|
+
</Icon>
|
|
70
|
+
)
|
|
71
|
+
export const MenuIcon = (p: IconProps) => (
|
|
72
|
+
<Icon {...p}>
|
|
73
|
+
<path d="M4 6h16M4 12h16M4 18h16" />
|
|
74
|
+
</Icon>
|
|
75
|
+
)
|
|
76
|
+
export const PanelLeftIcon = (p: IconProps) => (
|
|
77
|
+
<Icon {...p}>
|
|
78
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
79
|
+
<path d="M9 3v18" />
|
|
80
|
+
</Icon>
|
|
81
|
+
)
|
|
82
|
+
export const SearchIcon = (p: IconProps) => (
|
|
83
|
+
<Icon {...p}>
|
|
84
|
+
<circle cx="11" cy="11" r="7" />
|
|
85
|
+
<path d="m20 20-3.5-3.5" />
|
|
86
|
+
</Icon>
|
|
87
|
+
)
|
|
88
|
+
export const InboxIcon = (p: IconProps) => (
|
|
89
|
+
<Icon {...p}>
|
|
90
|
+
<path d="M22 12h-6l-2 3h-4l-2-3H2" />
|
|
91
|
+
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
|
92
|
+
</Icon>
|
|
93
|
+
)
|
|
94
|
+
export const AlertIcon = (p: IconProps) => (
|
|
95
|
+
<Icon {...p}>
|
|
96
|
+
<circle cx="12" cy="12" r="10" />
|
|
97
|
+
<path d="M12 8v4M12 16h.01" />
|
|
98
|
+
</Icon>
|
|
99
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface AppShellContextValue {
|
|
4
|
+
/** Desktop sidebar is collapsed to an icon rail. */
|
|
5
|
+
collapsed: boolean
|
|
6
|
+
setCollapsed: (collapsed: boolean) => void
|
|
7
|
+
/** Mobile navigation drawer is open. */
|
|
8
|
+
mobileOpen: boolean
|
|
9
|
+
setMobileOpen: (open: boolean) => void
|
|
10
|
+
/** True for the sidebar copy rendered inside the mobile drawer. */
|
|
11
|
+
inDrawer: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AppShellContext = createContext<AppShellContextValue | null>(null)
|
|
15
|
+
|
|
16
|
+
/** Sidebar state. Works outside an AppShell too (always expanded). */
|
|
17
|
+
export function useAppShell(): AppShellContextValue {
|
|
18
|
+
return (
|
|
19
|
+
useContext(AppShellContext) ?? {
|
|
20
|
+
collapsed: false,
|
|
21
|
+
setCollapsed: () => {},
|
|
22
|
+
mobileOpen: false,
|
|
23
|
+
setMobileOpen: () => {},
|
|
24
|
+
inDrawer: false,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { focusRing, transition } from '../../utils/styles'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
|
|
4
|
+
export const appShellStyles = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
root: 'flex w-full bg-background',
|
|
7
|
+
sidebar: 'hidden shrink-0 md:flex',
|
|
8
|
+
main: 'flex min-w-0 flex-1 flex-col',
|
|
9
|
+
mobileBar: [
|
|
10
|
+
'flex h-14 shrink-0 items-center gap-2 border-b-border px-3 md:hidden',
|
|
11
|
+
'border-b-(length:--glyph-border-width)',
|
|
12
|
+
],
|
|
13
|
+
overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
|
|
14
|
+
drawer:
|
|
15
|
+
'fixed inset-y-0 left-0 z-50 flex w-72 max-w-[85vw] animate-slide-in-left shadow-lg outline-none [&>[data-slot=sidebar]]:w-full',
|
|
16
|
+
},
|
|
17
|
+
variants: {
|
|
18
|
+
layout: {
|
|
19
|
+
page: {
|
|
20
|
+
root: 'min-h-dvh',
|
|
21
|
+
sidebar: 'sticky top-0 h-dvh',
|
|
22
|
+
mobileBar: 'sticky top-0 z-30 bg-background/80 backdrop-blur-xl',
|
|
23
|
+
},
|
|
24
|
+
embedded: {
|
|
25
|
+
root: 'h-full min-h-0 overflow-hidden',
|
|
26
|
+
sidebar: 'h-full',
|
|
27
|
+
main: 'overflow-y-auto',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: { layout: 'page' },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export type AppShellVariants = VariantProps<typeof appShellStyles>
|
|
35
|
+
|
|
36
|
+
export const sidebarStyles = tv({
|
|
37
|
+
slots: {
|
|
38
|
+
root: [
|
|
39
|
+
'group/sidebar flex h-full w-60 flex-col overflow-hidden border-r-border bg-subtle',
|
|
40
|
+
'border-r-(length:--glyph-border-width)',
|
|
41
|
+
'transition-[width] duration-(--glyph-duration-base) ease-standard data-[collapsed=true]:w-15',
|
|
42
|
+
],
|
|
43
|
+
header: 'flex h-14 shrink-0 items-center gap-2 px-3',
|
|
44
|
+
content: 'flex flex-1 flex-col gap-5 overflow-y-auto overflow-x-hidden px-3 py-3',
|
|
45
|
+
section: 'flex flex-col gap-0.5',
|
|
46
|
+
sectionLabel:
|
|
47
|
+
'mb-1 truncate px-2 font-mono text-caption uppercase tracking-[0.1em] text-faint-foreground group-data-[collapsed=true]/sidebar:sr-only',
|
|
48
|
+
item: [
|
|
49
|
+
'relative flex h-8 w-full shrink-0 cursor-pointer items-center gap-2.5 rounded-sm px-2 text-small font-medium text-muted-foreground',
|
|
50
|
+
transition,
|
|
51
|
+
focusRing,
|
|
52
|
+
'hover:bg-muted hover:text-foreground',
|
|
53
|
+
'aria-[current=page]:bg-muted aria-[current=page]:text-foreground',
|
|
54
|
+
'before:absolute before:inset-y-1.5 before:-left-3 before:w-0.5 before:rounded-r-pill before:bg-transparent aria-[current=page]:before:bg-primary',
|
|
55
|
+
'group-data-[collapsed=true]/sidebar:justify-center group-data-[collapsed=true]/sidebar:px-0',
|
|
56
|
+
],
|
|
57
|
+
itemIcon: 'flex size-4 shrink-0 items-center justify-center [&>svg]:size-4',
|
|
58
|
+
itemLabel: 'min-w-0 flex-1 truncate text-left group-data-[collapsed=true]/sidebar:hidden',
|
|
59
|
+
itemBadge:
|
|
60
|
+
'ml-auto font-mono text-caption text-faint-foreground tabular-nums group-data-[collapsed=true]/sidebar:hidden',
|
|
61
|
+
footer: [
|
|
62
|
+
'mt-auto flex shrink-0 items-center gap-2 border-t-border px-3 py-3 group-data-[collapsed=true]/sidebar:justify-center',
|
|
63
|
+
'border-t-(length:--glyph-border-width)',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const navbarStyles = tv({
|
|
69
|
+
slots: {
|
|
70
|
+
root: [
|
|
71
|
+
'top-0 z-40 w-full border-b-transparent',
|
|
72
|
+
'border-b-(length:--glyph-border-width)',
|
|
73
|
+
'transition-[background-color,border-color,backdrop-filter] duration-(--glyph-duration-base) ease-standard',
|
|
74
|
+
],
|
|
75
|
+
inner: 'flex h-16 items-center gap-6',
|
|
76
|
+
brand: 'flex shrink-0 items-center gap-2 font-display text-h4 text-foreground',
|
|
77
|
+
links: 'hidden items-center gap-1 md:flex',
|
|
78
|
+
link: [
|
|
79
|
+
'rounded-sm px-3 py-1.5 text-small font-medium text-muted-foreground',
|
|
80
|
+
transition,
|
|
81
|
+
focusRing,
|
|
82
|
+
'hover:text-foreground aria-[current=page]:text-foreground',
|
|
83
|
+
],
|
|
84
|
+
actions: 'ml-auto hidden items-center gap-2 md:flex',
|
|
85
|
+
menuButton: 'ml-auto md:hidden',
|
|
86
|
+
overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
|
|
87
|
+
panel: [
|
|
88
|
+
'fixed inset-x-0 top-0 z-50 max-h-dvh overflow-y-auto border-b-border bg-background px-4 pb-6 shadow-lg outline-none animate-slide-in-top',
|
|
89
|
+
'border-b-(length:--glyph-border-width)',
|
|
90
|
+
],
|
|
91
|
+
panelHeader: 'flex h-16 items-center justify-between',
|
|
92
|
+
panelLinks: 'flex flex-col gap-1',
|
|
93
|
+
panelLink: [
|
|
94
|
+
'rounded-md px-3 py-2.5 text-body font-medium text-muted-foreground',
|
|
95
|
+
focusRing,
|
|
96
|
+
'hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground',
|
|
97
|
+
],
|
|
98
|
+
panelActions: [
|
|
99
|
+
'mt-4 flex flex-col gap-2 border-t-border pt-4',
|
|
100
|
+
'border-t-(length:--glyph-border-width)',
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
variants: {
|
|
104
|
+
sticky: { true: { root: 'sticky' }, false: { root: 'relative' } },
|
|
105
|
+
blur: {
|
|
106
|
+
true: {
|
|
107
|
+
root: 'data-[scrolled=true]:border-b-border data-[scrolled=true]:bg-background/72 data-[scrolled=true]:backdrop-blur-xl data-[scrolled=true]:backdrop-saturate-150',
|
|
108
|
+
},
|
|
109
|
+
false: { root: 'border-b-border bg-background' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
defaultVariants: { sticky: true, blur: true },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export const pageHeaderStyles = tv({
|
|
116
|
+
slots: {
|
|
117
|
+
root: [
|
|
118
|
+
'flex flex-col gap-4 border-b-border pb-5 sm:flex-row sm:items-end sm:justify-between',
|
|
119
|
+
'border-b-(length:--glyph-border-width)',
|
|
120
|
+
],
|
|
121
|
+
text: 'grid min-w-0 gap-1',
|
|
122
|
+
eyebrow: 'font-mono text-caption uppercase tracking-[0.12em] text-faint-foreground',
|
|
123
|
+
title: 'font-display text-h3 text-foreground',
|
|
124
|
+
description: 'max-w-2xl text-small text-muted-foreground',
|
|
125
|
+
actions: 'flex shrink-0 flex-wrap items-center gap-2',
|
|
126
|
+
},
|
|
127
|
+
})
|