astralyx-ui 0.13.0 → 0.15.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 +2 -2
- package/package.json +2 -2
- package/registry/index.json +12 -1
- package/registry/items/logo.json +18 -0
- package/registry/items/tabs.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://ui.astralyx.dev)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**344 accessible React components you copy into your repo** — with a CLI and a
|
|
8
8
|
registry that work out what each one needs and bring that too.
|
|
9
9
|
|
|
10
10
|
**[Browse every component, running, at ui.astralyx.dev →](https://ui.astralyx.dev)**
|
|
@@ -20,7 +20,7 @@ npx astralyx-ui add button
|
|
|
20
20
|
|
|
21
21
|
## What you get
|
|
22
22
|
|
|
23
|
-
- **
|
|
23
|
+
- **344 components** across 36 categories — forms, data, overlays, commerce,
|
|
24
24
|
auth, AI, crypto, observability, developer tooling.
|
|
25
25
|
- **12 primitives** underneath them. Slot, Popper, FocusTrap, Dismissable and
|
|
26
26
|
friends — no headless-UI dependency.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astralyx-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "344 accessible React components you copy into your repo, with a CLI and registry that resolve what each one needs.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Astralyx",
|
|
8
8
|
"homepage": "https://ui.astralyx.dev",
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astralyx-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"homepage": "https://ui.astralyx.dev",
|
|
5
5
|
"items": [
|
|
6
6
|
{
|
|
@@ -2420,6 +2420,17 @@
|
|
|
2420
2420
|
"separator"
|
|
2421
2421
|
]
|
|
2422
2422
|
},
|
|
2423
|
+
{
|
|
2424
|
+
"name": "logo",
|
|
2425
|
+
"type": "registry:ui",
|
|
2426
|
+
"title": "Logo",
|
|
2427
|
+
"description": "A brand mark that follows the room it has: the full artwork while the rail is open, the square mark once it collapses. Falls back to the product name, and to its initials when only 52px are left.",
|
|
2428
|
+
"category": "Utility",
|
|
2429
|
+
"dependencies": [],
|
|
2430
|
+
"registryDependencies": [
|
|
2431
|
+
"lib-utils"
|
|
2432
|
+
]
|
|
2433
|
+
},
|
|
2423
2434
|
{
|
|
2424
2435
|
"name": "map-embed",
|
|
2425
2436
|
"type": "registry:ui",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "logo",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Logo",
|
|
5
|
+
"description": "A brand mark that follows the room it has: the full artwork while the rail is open, the square mark once it collapses. Falls back to the product name, and to its initials when only 52px are left.",
|
|
6
|
+
"category": "Utility",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"lib-utils"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/ui/logo.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"content": "'use client'\n\nimport { useState, type ComponentProps, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\n/**\n * A brand mark that answers to how much room it has.\n *\n * A rail shows a wordmark when it is open and a square mark when it collapses to\n * 52px, and the usual way to get that is two copies of the artwork with opposing\n * visibility classes on them. That works, and it is the thing everyone rewrites\n * per project — including this repo, where the same trick was inlined in the\n * examples' shell and again in the sidebar's own documentation, once with a\n * cropped `viewBox` to fake the square version.\n *\n * The switch is CSS by default, keyed off the sidebar's `data-state`. That is\n * deliberate: reading the sidebar's context would make every project that runs\n * `add logo` install a sidebar it may not have, and the mark belongs in headers\n * and login screens too. Outside a rail nothing matches the selector, so the\n * full artwork shows and stays shown.\n *\n * `collapsed` overrides it. Pass a boolean when something other than a sidebar\n * decides — a header that shrinks on scroll, a canvas that zooms — and only that\n * one mark renders, which is also the way to avoid fetching both files.\n *\n * The accessible name lives on the container and everything inside it is hidden\n * from assistive technology. Two copies of a logo are one logo, and a reader\n * should not hear the company's name twice for a decision made in CSS.\n */\n\ntype LogoSource = string | ReactNode\n\n/** First letters of the first and last word — \"Astralyx UI\" becomes \"AU\". */\nfunction initialsFrom(text: string) {\n const words = text.trim().split(/\\s+/).filter(Boolean)\n if (words.length === 0) return ''\n if (words.length === 1) return words[0].slice(0, 2).toUpperCase()\n return (words[0][0] + words[words.length - 1][0]).toUpperCase()\n}\n\n/**\n * One rendition: a URL, whatever node you handed over, or the text that stands\n * in for artwork that is missing or refused to load.\n */\nfunction Mark({\n source,\n text,\n className,\n}: {\n source: LogoSource | undefined\n text: string\n className?: string\n}) {\n const [failed, setFailed] = useState(false)\n\n if (typeof source === 'string' && source && !failed) {\n return (\n <img\n src={source}\n alt=\"\"\n // A broken URL falls through to the text rather than leaving the\n // browser's torn-page icon where a brand mark should be.\n onError={() => setFailed(true)}\n className={cn('h-full w-auto object-contain', className)}\n />\n )\n }\n\n if (source != null && typeof source !== 'string') {\n return <span className={cn('flex h-full items-center [&_svg]:h-full [&_svg]:w-auto', className)}>{source}</span>\n }\n\n return (\n <span className={cn('truncate text-sm font-semibold tracking-tight', className)}>{text}</span>\n )\n}\n\nfunction Logo({\n icon,\n full,\n alt,\n fallbackText = '',\n collapsed,\n className,\n ...props\n}: Omit<ComponentProps<'span'>, 'children'> & {\n /** The square mark, for when the rail is collapsed. */\n icon?: LogoSource\n /** The full artwork — mark and wordmark together. */\n full?: LogoSource\n /**\n * The accessible name for whichever rendition is showing. Pass an empty\n * string when the product name is already written beside it, which makes the\n * mark decorative rather than announcing it twice.\n */\n alt: string\n /** Drawn when there is no artwork, or when the image fails. Collapsed, it is\n * reduced to initials — a rail is 52px and a name does not fit. */\n fallbackText?: string\n /** Overrides the sidebar's own state. Renders one mark instead of both. */\n collapsed?: boolean\n}) {\n const short = initialsFrom(fallbackText)\n\n const shell = cn('flex h-6 shrink-0 items-center', className)\n const label = alt ? { role: 'img' as const, 'aria-label': alt } : { 'aria-hidden': true }\n\n if (collapsed !== undefined) {\n return (\n <span data-slot=\"logo\" data-state={collapsed ? 'collapsed' : 'expanded'} className={shell} {...label} {...props}>\n <Mark\n source={collapsed ? (icon ?? full) : (full ?? icon)}\n text={collapsed ? short : fallbackText}\n />\n </span>\n )\n }\n\n return (\n <span data-slot=\"logo\" className={shell} {...label} {...props}>\n <Mark\n source={full ?? icon}\n text={fallbackText}\n className=\"group-data-[state=collapsed]/sidebar:hidden\"\n />\n <Mark\n source={icon ?? full}\n text={short}\n className=\"hidden group-data-[state=collapsed]/sidebar:flex group-data-[state=collapsed]/sidebar:items-center\"\n />\n </span>\n )\n}\n\nexport { Logo }\nexport type { LogoSource }\n"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/registry/items/tabs.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"path": "components/ui/tabs.tsx",
|
|
19
19
|
"type": "registry:ui",
|
|
20
|
-
"content": "import type { ComponentProps } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { TabsProvider, tabIds, useTabs } from '@/components/primitives/tabs'\nimport { enterFade } from '@/lib/motion'\nimport { focusRing, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\nconst listVariants = cva('flex', {\n variants: {\n variant: {\n /** Segmented control: a filled track with the active tab lifted out. */\n solid: 'bg-muted gap-0.5 p-0.5',\n /** A rule with the active tab underlined. */\n underline: 'border-border gap-4 border-b',\n },\n orientation: {\n horizontal: 'flex-row items-center',\n vertical: 'flex-col items-stretch',\n },\n },\n compoundVariants: [\n { variant: 'underline', orientation: 'vertical', class: 'border-b-0 border-r' },\n ],\n defaultVariants: { variant: 'solid', orientation: 'horizontal' },\n})\n\nconst triggerVariants = cva(\n [\n 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap',\n 'text-sm font-medium',\n 'transition-colors duration-150 ease-out motion-reduce:transition-none',\n 'disabled:pointer-events-none disabled:opacity-50',\n focusRing,\n ].join(' '),\n {\n variants: {\n variant: {\n solid: cn(\n radius.control,\n 'px-3 py-1.5',\n 'text-muted-foreground hover:text-foreground',\n 'data-[state=active]:bg-background data-[state=active]:text-foreground',\n ),\n underline: cn(\n 'border-b-2 border-transparent px-1 py-2',\n 'text-muted-foreground hover:text-foreground',\n 'data-[state=active]:border-foreground data-[state=active]:text-foreground',\n ),\n },\n },\n defaultVariants: { variant: 'solid' },\n },\n)\n\ntype TabsProps = Omit<ComponentProps<'div'>, 'onChange'> & {\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n orientation?: 'horizontal' | 'vertical'\n activationMode?: 'automatic' | 'manual'\n}\n\nfunction Tabs({\n className,\n value,\n defaultValue,\n onValueChange,\n orientation = 'horizontal',\n activationMode = 'automatic',\n ...props\n}: TabsProps) {\n return (\n <TabsProvider\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n orientation={orientation}\n activationMode={activationMode}\n >\n <div\n data-slot=\"tabs\"\n className={cn(\n 'flex gap-3',\n orientation === 'vertical' ? 'flex-row' : 'flex-col',\n className,\n )}\n {...props}\n />\n </TabsProvider>\n )\n}\n\nfunction TabsList({\n className,\n variant,\n ...props\n}: ComponentProps<'div'> & VariantProps<typeof listVariants>) {\n const { orientation, onListKeyDown } = useTabs()\n\n return (\n <div\n role=\"tablist\"\n data-slot=\"tabs-list\"\n aria-orientation={orientation}\n onKeyDown={onListKeyDown}\n className={cn(\n listVariants({ variant, orientation }),\n variant === 'underline' ? '' : radius.control,\n 'w-fit',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n variant,\n value,\n ...props\n}: Omit<ComponentProps<'button'>, 'value'> &\n VariantProps<typeof triggerVariants> & { value: string }) {\n const { value: selected, select, baseId } = useTabs()\n const active = selected === value\n const ids = tabIds(baseId, value)\n\n return (\n <button\n type=\"button\"\n role=\"tab\"\n id={ids.trigger}\n data-slot=\"tabs-trigger\"\n data-value={value}\n data-state={active ? 'active' : 'inactive'}\n aria-selected={active}\n // Only the active panel is mounted, so only the active trigger can point\n // at one. Setting it unconditionally leaves every inactive tab with a\n // dangling reference to an element that is not in the document.\n aria-controls={active ? ids.panel : undefined}\n // Only the active tab is in the tab order; arrows move between the rest.\n tabIndex={active ? 0 : -1}\n onClick={() => select(value)}\n className={cn(triggerVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\n/**\n * A panel.\n *\n * Unmounted when it is not selected, which is usually what you want: a panel\n * that keeps a video playing or a subscription open behind another tab is a\n * bug, and a fresh mount is the cheapest way to guarantee it cannot happen.\n *\n * `keepMounted` is for the case where the content has to exist whether or not\n * anyone is looking at it — the one that prompted this was documentation, where\n * the source behind a Code tab is the page's actual content and unmounting it\n * meant it never reached the HTML a crawler reads. Hidden, not removed: the\n * panel keeps its markup, and `hidden` takes it out of the view and out of the\n * accessibility tree just as unmounting did.\n */\nfunction TabsContent({\n className,\n value,\n keepMounted = false,\n ...props\n}: ComponentProps<'div'> & { value: string; keepMounted?: boolean }) {\n const { value: selected, baseId } = useTabs()\n const active = selected === value\n if (!active && !keepMounted) return null\n\n const ids = tabIds(baseId, value)\n\n return (\n <div\n role=\"tabpanel\"\n id={ids.panel}\n data-slot=\"tabs-content\"\n aria-labelledby={ids.trigger}\n // A panel nobody can see is not a tab stop.\n tabIndex={active ? 0 : -1}\n hidden={!active}\n className={cn(active && enterFade, 'outline-none', className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger, listVariants, triggerVariants }\nexport type { TabsProps }\n"
|
|
20
|
+
"content": "import { createContext, use, type ComponentProps } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { TabsProvider, tabIds, useTabs } from '@/components/primitives/tabs'\nimport { enterFade } from '@/lib/motion'\nimport { focusRing, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\nconst listVariants = cva('flex', {\n variants: {\n variant: {\n /** Segmented control: a filled track with the active tab lifted out. */\n solid: 'bg-muted gap-0.5 p-0.5',\n /** A rule with the active tab underlined. */\n underline: 'border-border gap-4 border-b',\n /**\n * A browser's tab strip: the active tab is the panel, pulled upwards.\n *\n * The strip is the recessed ground and the tabs sit flush on its bottom\n * edge, so the active one runs straight into the content below it with\n * nothing between them.\n */\n browser: 'bg-muted gap-1 px-1.5 pt-1.5',\n },\n orientation: {\n horizontal: 'flex-row items-center',\n vertical: 'flex-col items-stretch',\n },\n },\n compoundVariants: [\n { variant: 'underline', orientation: 'vertical', class: 'border-b-0 border-r' },\n ],\n defaultVariants: { variant: 'solid', orientation: 'horizontal' },\n})\n\nconst triggerVariants = cva(\n [\n 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap',\n 'text-sm font-medium',\n 'transition-colors duration-150 ease-out motion-reduce:transition-none',\n 'disabled:pointer-events-none disabled:opacity-50',\n focusRing,\n ].join(' '),\n {\n variants: {\n variant: {\n solid: cn(\n radius.control,\n 'px-3 py-1.5',\n 'text-muted-foreground hover:text-foreground',\n 'data-[state=active]:bg-background data-[state=active]:text-foreground',\n ),\n underline: cn(\n 'border-b-2 border-transparent px-1 py-2',\n 'text-muted-foreground hover:text-foreground',\n 'data-[state=active]:border-foreground data-[state=active]:text-foreground',\n ),\n /*\n * The connected tab.\n *\n * No borders anywhere, which is the whole trick: the active tab and the\n * panel are the same fill, so where they meet there is nothing to draw\n * and nothing to line up. A bordered version has to hide one border\n * with another and comes apart at every zoom level.\n *\n * The two pseudo-elements are the outward flare at the bottom corners.\n * Each is a square of panel fill sitting just outside the tab, with a\n * quarter circle masked out of the corner that faces away from it —\n * which leaves the concave curve a browser tab has and a folder tab\n * does not.\n */\n browser: cn(\n // Tall enough to be the header's own height class. A browser's tab\n // fills its titlebar; at the default control padding it came out\n // shorter than the collapse control beside it and read as something\n // dropped into the bar rather than part of it.\n 'relative h-9 rounded-t-lg px-4',\n 'text-muted-foreground hover:text-foreground',\n 'data-[state=active]:bg-card data-[state=active]:text-foreground',\n 'data-[state=active]:before:bg-card data-[state=active]:after:bg-card',\n // The flares carry the same transition as the tab. Without it the\n // fill eases across while the two corners snap, and the shape looks\n // like it arrives in two pieces.\n 'before:transition-colors before:duration-150 before:ease-out',\n 'after:transition-colors after:duration-150 after:ease-out',\n 'motion-reduce:before:transition-none motion-reduce:after:transition-none',\n \"before:pointer-events-none before:absolute before:-left-2 before:bottom-0 before:size-2 before:content-['']\",\n \"after:pointer-events-none after:absolute after:-right-2 after:bottom-0 after:size-2 after:content-['']\",\n 'before:[mask-image:radial-gradient(circle_at_top_left,transparent_8px,black_8.5px)]',\n 'after:[mask-image:radial-gradient(circle_at_top_right,transparent_8px,black_8.5px)]',\n ),\n },\n },\n defaultVariants: { variant: 'solid' },\n },\n)\n\ntype TabsVariant = NonNullable<VariantProps<typeof listVariants>['variant']>\n\n/**\n * The look, shared down the tree.\n *\n * It lives here rather than in the primitive because the primitive is\n * behaviour: roving focus, activation mode, ids. Nothing about how a tab is\n * painted belongs in it.\n *\n * A variant set on the child still wins, so the older way of writing it —\n * `variant` on the list and on each trigger — keeps working. What the context\n * adds is a variant the *panel* can see, which `browser` needs: an active tab\n * that runs into its content has to know what the content looks like.\n */\nconst VariantContext = createContext<TabsVariant>('solid')\n\nfunction useTabsVariant(override: TabsVariant | null | undefined) {\n const inherited = use(VariantContext)\n return override ?? inherited\n}\n\ntype TabsProps = Omit<ComponentProps<'div'>, 'onChange'> & {\n /** Sets the look for the list, the triggers and the panel at once. */\n variant?: TabsVariant\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n orientation?: 'horizontal' | 'vertical'\n activationMode?: 'automatic' | 'manual'\n}\n\nfunction Tabs({\n className,\n value,\n defaultValue,\n onValueChange,\n orientation = 'horizontal',\n activationMode = 'automatic',\n variant = 'solid',\n ...props\n}: TabsProps) {\n return (\n <TabsProvider\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n orientation={orientation}\n activationMode={activationMode}\n >\n <VariantContext value={variant}>\n <div\n data-slot=\"tabs\"\n data-variant={variant}\n className={cn(\n 'flex',\n // The strip and the panel are one surface, so there is nothing to\n // space apart. Every other variant reads as two things.\n variant === 'browser' ? 'gap-0' : 'gap-3',\n orientation === 'vertical' ? 'flex-row' : 'flex-col',\n className,\n )}\n {...props}\n />\n </VariantContext>\n </TabsProvider>\n )\n}\n\nfunction TabsList({\n className,\n variant,\n ...props\n}: ComponentProps<'div'> & VariantProps<typeof listVariants>) {\n const { orientation, onListKeyDown } = useTabs()\n const look = useTabsVariant(variant)\n\n return (\n <div\n role=\"tablist\"\n data-slot=\"tabs-list\"\n aria-orientation={orientation}\n onKeyDown={onListKeyDown}\n className={cn(\n listVariants({ variant: look, orientation }),\n look === 'underline' ? '' : radius.control,\n // A browser strip spans its container and is only round on top; the\n // panel below finishes the shape.\n look === 'browser' ? 'w-full rounded-b-none' : 'w-fit',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n variant,\n value,\n ...props\n}: Omit<ComponentProps<'button'>, 'value'> &\n VariantProps<typeof triggerVariants> & { value: string }) {\n const { value: selected, select, baseId } = useTabs()\n const look = useTabsVariant(variant)\n const active = selected === value\n const ids = tabIds(baseId, value)\n\n return (\n <button\n type=\"button\"\n role=\"tab\"\n id={ids.trigger}\n data-slot=\"tabs-trigger\"\n data-value={value}\n data-state={active ? 'active' : 'inactive'}\n aria-selected={active}\n // Only the active panel is mounted, so only the active trigger can point\n // at one. Setting it unconditionally leaves every inactive tab with a\n // dangling reference to an element that is not in the document.\n aria-controls={active ? ids.panel : undefined}\n // Only the active tab is in the tab order; arrows move between the rest.\n tabIndex={active ? 0 : -1}\n onClick={() => select(value)}\n className={cn(triggerVariants({ variant: look }), className)}\n {...props}\n />\n )\n}\n\n/**\n * A panel.\n *\n * Unmounted when it is not selected, which is usually what you want: a panel\n * that keeps a video playing or a subscription open behind another tab is a\n * bug, and a fresh mount is the cheapest way to guarantee it cannot happen.\n *\n * `keepMounted` is for the case where the content has to exist whether or not\n * anyone is looking at it — the one that prompted this was documentation, where\n * the source behind a Code tab is the page's actual content and unmounting it\n * meant it never reached the HTML a crawler reads. Hidden, not removed: the\n * panel keeps its markup, and `hidden` takes it out of the view and out of the\n * accessibility tree just as unmounting did.\n */\nfunction TabsContent({\n className,\n value,\n keepMounted = false,\n variant,\n ...props\n}: ComponentProps<'div'> & {\n value: string\n keepMounted?: boolean\n variant?: TabsVariant\n}) {\n const { value: selected, baseId } = useTabs()\n const look = useTabsVariant(variant)\n const active = selected === value\n if (!active && !keepMounted) return null\n\n const ids = tabIds(baseId, value)\n\n return (\n <div\n role=\"tabpanel\"\n id={ids.panel}\n data-slot=\"tabs-content\"\n aria-labelledby={ids.trigger}\n // A panel nobody can see is not a tab stop.\n tabIndex={active ? 0 : -1}\n hidden={!active}\n className={cn(\n active && enterFade,\n 'outline-none',\n // The panel *is* the active tab, continued. Same fill, no border\n // between them, and the corners it does round are the two at the\n // bottom — the strip above rounds the other two.\n look === 'browser' && cn('bg-card p-4', radius.control, 'rounded-t-none'),\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger, listVariants, triggerVariants }\nexport type { TabsProps }\n"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|