@lfrprazeres/ui 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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/components/chip.d.ts +16 -0
- package/dist/components/chip.d.ts.map +1 -0
- package/dist/components/chip.js +49 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +8 -0
- package/dist/components/language-switcher.d.ts +22 -0
- package/dist/components/language-switcher.d.ts.map +1 -0
- package/dist/components/language-switcher.js +42 -0
- package/dist/components/language-switcher.js.map +1 -0
- package/dist/components/marquee.d.ts +23 -0
- package/dist/components/marquee.d.ts.map +1 -0
- package/dist/components/marquee.js +49 -0
- package/dist/components/marquee.js.map +1 -0
- package/dist/components/message-bubble.d.ts +28 -0
- package/dist/components/message-bubble.d.ts.map +1 -0
- package/dist/components/message-bubble.js +81 -0
- package/dist/components/message-bubble.js.map +1 -0
- package/dist/components/stat-tile.d.ts +18 -0
- package/dist/components/stat-tile.d.ts.map +1 -0
- package/dist/components/stat-tile.js +54 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/streaming-dots.d.ts +19 -0
- package/dist/components/streaming-dots.d.ts.map +1 -0
- package/dist/components/streaming-dots.js +29 -0
- package/dist/components/streaming-dots.js.map +1 -0
- package/dist/components/theme-toggle.d.ts +13 -0
- package/dist/components/theme-toggle.d.ts.map +1 -0
- package/dist/components/theme-toggle.js +54 -0
- package/dist/components/theme-toggle.js.map +1 -0
- package/dist/elements/avatar.d.ts +14 -0
- package/dist/elements/avatar.d.ts.map +1 -0
- package/dist/elements/avatar.js +52 -0
- package/dist/elements/avatar.js.map +1 -0
- package/dist/elements/badge.d.ts +12 -0
- package/dist/elements/badge.d.ts.map +1 -0
- package/dist/elements/badge.js +29 -0
- package/dist/elements/badge.js.map +1 -0
- package/dist/elements/button.d.ts +13 -0
- package/dist/elements/button.d.ts.map +1 -0
- package/dist/elements/button.js +49 -0
- package/dist/elements/button.js.map +1 -0
- package/dist/elements/card.d.ts +12 -0
- package/dist/elements/card.d.ts.map +1 -0
- package/dist/elements/card.js +56 -0
- package/dist/elements/card.js.map +1 -0
- package/dist/elements/checkbox.d.ts +7 -0
- package/dist/elements/checkbox.d.ts.map +1 -0
- package/dist/elements/checkbox.js +22 -0
- package/dist/elements/checkbox.js.map +1 -0
- package/dist/elements/dialog.d.ts +20 -0
- package/dist/elements/dialog.d.ts.map +1 -0
- package/dist/elements/dialog.js +95 -0
- package/dist/elements/dialog.js.map +1 -0
- package/dist/elements/dropdown-menu.d.ts +28 -0
- package/dist/elements/dropdown-menu.d.ts.map +1 -0
- package/dist/elements/dropdown-menu.js +124 -0
- package/dist/elements/dropdown-menu.js.map +1 -0
- package/dist/elements/index.d.ts +17 -0
- package/dist/elements/index.js +17 -0
- package/dist/elements/input.d.ts +6 -0
- package/dist/elements/input.d.ts.map +1 -0
- package/dist/elements/input.js +15 -0
- package/dist/elements/input.js.map +1 -0
- package/dist/elements/label.d.ts +7 -0
- package/dist/elements/label.d.ts.map +1 -0
- package/dist/elements/label.js +16 -0
- package/dist/elements/label.js.map +1 -0
- package/dist/elements/popover.d.ts +13 -0
- package/dist/elements/popover.d.ts.map +1 -0
- package/dist/elements/popover.js +57 -0
- package/dist/elements/popover.js.map +1 -0
- package/dist/elements/select.d.ts +18 -0
- package/dist/elements/select.d.ts.map +1 -0
- package/dist/elements/select.js +99 -0
- package/dist/elements/select.js.map +1 -0
- package/dist/elements/separator.d.ts +7 -0
- package/dist/elements/separator.d.ts.map +1 -0
- package/dist/elements/separator.js +18 -0
- package/dist/elements/separator.js.map +1 -0
- package/dist/elements/skeleton.d.ts +5 -0
- package/dist/elements/skeleton.d.ts.map +1 -0
- package/dist/elements/skeleton.js +14 -0
- package/dist/elements/skeleton.js.map +1 -0
- package/dist/elements/sonner.d.ts +17 -0
- package/dist/elements/sonner.d.ts.map +1 -0
- package/dist/elements/sonner.js +40 -0
- package/dist/elements/sonner.js.map +1 -0
- package/dist/elements/switch.d.ts +9 -0
- package/dist/elements/switch.d.ts.map +1 -0
- package/dist/elements/switch.js +21 -0
- package/dist/elements/switch.js.map +1 -0
- package/dist/elements/textarea.d.ts +6 -0
- package/dist/elements/textarea.d.ts.map +1 -0
- package/dist/elements/textarea.js +14 -0
- package/dist/elements/textarea.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +25 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +10 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/styles.css +18 -0
- package/dist/tokens/01-ramps.css +142 -0
- package/dist/tokens/02-scales.css +28 -0
- package/dist/tokens/03-bridge.css +75 -0
- package/dist/tokens/index.css +7 -0
- package/dist/tokens/palettes/base.css +96 -0
- package/dist/tokens/palettes/cyberpunk.css +83 -0
- package/dist/tokens/palettes/gold.css +77 -0
- package/dist/tokens/palettes/minimal.css +82 -0
- package/package.json +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luiz Fernando Prazeres
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @lfrprazeres/ui
|
|
2
|
+
|
|
3
|
+
A React design system: shadcn elements, composed components, and a three-tier
|
|
4
|
+
token contract with swappable palettes.
|
|
5
|
+
|
|
6
|
+
Built for React 19 and Tailwind CSS v4. ESM only.
|
|
7
|
+
|
|
8
|
+
> **Built for my own projects.** This exists so the things I build share one
|
|
9
|
+
> component layer instead of each rebuilding buttons and a token system. It is
|
|
10
|
+
> not a community project and has no roadmap beyond what I need next.
|
|
11
|
+
>
|
|
12
|
+
> That said, it is MIT licensed and genuinely usable. Install it, fork it, copy
|
|
13
|
+
> the bits you like. Just know that the API moves when my projects need it to,
|
|
14
|
+
> which is why it stays on `0.x`. Pin an exact version if that matters to you.
|
|
15
|
+
|
|
16
|
+
**[Component docs and live playground](https://lfrprazeres.github.io/ui)** ·
|
|
17
|
+
[Changelog](./CHANGELOG.md)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @lfrprazeres/ui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`react`, `react-dom` and `tailwindcss` are peer dependencies.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Import the stylesheet once, at your CSS entry point:
|
|
30
|
+
|
|
31
|
+
```css
|
|
32
|
+
@import "tailwindcss";
|
|
33
|
+
@import "@lfrprazeres/ui/styles.css";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That is the whole setup. You do **not** need a `@source` line pointing into
|
|
37
|
+
`node_modules`: the shipped stylesheet scans its own `dist` for class names.
|
|
38
|
+
|
|
39
|
+
On Next.js, add the package to `optimizePackageImports`. Importing from the root
|
|
40
|
+
barrel is ergonomic but pulls the whole module graph into the dev server's
|
|
41
|
+
module map, which slows cold start. This fixes it, and does not change the
|
|
42
|
+
production bundle either way:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// next.config.ts
|
|
46
|
+
export default {
|
|
47
|
+
experimental: { optimizePackageImports: ["@lfrprazeres/ui"] },
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Use
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { Button, Card, StatTile } from "@lfrprazeres/ui";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or from a tier directly:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Button } from "@lfrprazeres/ui/elements";
|
|
61
|
+
import { StatTile } from "@lfrprazeres/ui/components";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both resolve to the same bytes. `Button` measures 9.59 kB either way.
|
|
65
|
+
|
|
66
|
+
## What ships
|
|
67
|
+
|
|
68
|
+
**Elements** (16): Avatar, Badge, Button, Card, Checkbox, Dialog, DropdownMenu,
|
|
69
|
+
Input, Label, Popover, Select, Separator, Skeleton, Switch, Textarea, Toaster.
|
|
70
|
+
|
|
71
|
+
**Components** (7): Chip, LanguageSwitcher, Marquee, MessageBubble, StatTile,
|
|
72
|
+
StreamingDots, ThemeToggle.
|
|
73
|
+
|
|
74
|
+
## Tiers
|
|
75
|
+
|
|
76
|
+
| Tier | What it is | Ships |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| **elements** | Single-purpose. Composes no other exported part, carries no data shape. Taken from the shadcn registry as-is. | yes |
|
|
79
|
+
| **components** | Composed from two or more elements. Owns interaction, still domain-agnostic. | yes |
|
|
80
|
+
| **features** | Owns a typed data prop or a layout region, and knows your domain. | **no, build these in your app** |
|
|
81
|
+
|
|
82
|
+
The library never ships features. If two projects need the same one, extract the
|
|
83
|
+
reusable mechanism as a component and keep a thin feature on top of it in each.
|
|
84
|
+
|
|
85
|
+
## Palettes
|
|
86
|
+
|
|
87
|
+
A palette is a preset that redefines the semantic tier. Components never know
|
|
88
|
+
which one is active, because they only read semantic names.
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<html class="palette-gold dark">
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Four ship: `base` (no class, lives at `:root`), `palette-gold`,
|
|
95
|
+
`palette-cyberpunk`, `palette-minimal`. Add `dark` alongside for the dark
|
|
96
|
+
variant of any of them.
|
|
97
|
+
|
|
98
|
+
A palette may redefine every semantic colour role, plus `--radius`, `--spacing`
|
|
99
|
+
and the three font stacks. Because Tailwind emits utilities as
|
|
100
|
+
`calc(var(--spacing) * n)` and `var(--radius)`, redefining those rescales the
|
|
101
|
+
whole system at runtime with no rebuild.
|
|
102
|
+
|
|
103
|
+
To write your own, redefine the semantic tier under a class. That is the entire
|
|
104
|
+
theming API.
|
|
105
|
+
|
|
106
|
+
## Reduced motion
|
|
107
|
+
|
|
108
|
+
Handled at the token layer, not per component. Under
|
|
109
|
+
`prefers-reduced-motion: reduce` the `--motion-duration-*` tokens collapse to
|
|
110
|
+
zero, so anything reading them inherits the behaviour without opting in. The
|
|
111
|
+
animation is not disabled, because disabling it hides the outcome: the user
|
|
112
|
+
lands on the final state instantly instead.
|
|
113
|
+
|
|
114
|
+
Two deliberate exceptions, both loops rather than transitions. A loop has no
|
|
115
|
+
final state, so collapsing its duration to zero would be an infinite speed:
|
|
116
|
+
|
|
117
|
+
- `Marquee` suppresses its animation and becomes a scrollable region.
|
|
118
|
+
- `StreamingDots` swaps the bounce for an opacity pulse, because removing it
|
|
119
|
+
outright would delete the only "something is happening" cue a sighted user
|
|
120
|
+
has.
|
|
121
|
+
|
|
122
|
+
The rule: **collapse durations for transitions, suppress movement for loops.**
|
|
123
|
+
|
|
124
|
+
## One deviation from upstream shadcn
|
|
125
|
+
|
|
126
|
+
`Toaster` takes `theme` as a prop. Upstream reads it with `useTheme()` from
|
|
127
|
+
`next-themes`, which would put a Next-only dependency inside a
|
|
128
|
+
framework-agnostic package. If you re-scaffold that file with the shadcn CLI,
|
|
129
|
+
the import comes back and needs re-inverting.
|
|
130
|
+
|
|
131
|
+
## Scripts
|
|
132
|
+
|
|
133
|
+
| Command | What it does |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `pnpm storybook` | Component docs on :6006 |
|
|
136
|
+
| `pnpm test` | Every story as a browser test, axe violations fail |
|
|
137
|
+
| `pnpm build` | Builds the package with tsdown |
|
|
138
|
+
| `pnpm check:rsc` | Fails if a `"use client"` directive was lost in the build |
|
|
139
|
+
| `pnpm check:package` | publint plus are-the-types-wrong |
|
|
140
|
+
| `pnpm check:size` | size-limit budgets |
|
|
141
|
+
|
|
142
|
+
## Notes on the build
|
|
143
|
+
|
|
144
|
+
Built unbundled, preserving module structure. That is load-bearing rather than a
|
|
145
|
+
preference: bundling hoists or strips the `"use client"` directives thirteen
|
|
146
|
+
modules depend on, and React Server Components then break with no error at all.
|
|
147
|
+
`pnpm check:rsc` compares source against the build and fails if any went
|
|
148
|
+
missing.
|
|
149
|
+
|
|
150
|
+
`verify/` holds a minimal Next App Router consumer that installs the packed
|
|
151
|
+
tarball and proves the contract from outside. See `verify/README.md`.
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
Mostly for my own future reference, see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
156
|
+
Every change that ships needs a changeset, and CI enforces it.
|
|
157
|
+
|
|
158
|
+
## Licence
|
|
159
|
+
|
|
160
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VariantProps } from "class-variance-authority";
|
|
2
|
+
import { ComponentProps, ReactNode } from "react";
|
|
3
|
+
//#region src/components/chip.d.ts
|
|
4
|
+
declare const chipVariants: (props?: ({
|
|
5
|
+
size?: "default" | "sm" | null | undefined;
|
|
6
|
+
variant?: "default" | "accent" | "muted" | "outline" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
type ChipProps = ComponentProps<"span"> & VariantProps<typeof chipVariants> & {
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Renders as an anchor when set, keeping the same visual treatment. */
|
|
11
|
+
href?: string;
|
|
12
|
+
};
|
|
13
|
+
declare function Chip({ className, variant, size, icon, href, children, ...props }: ChipProps): import("react").JSX.Element;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { Chip, ChipProps, chipVariants };
|
|
16
|
+
//# sourceMappingURL=chip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chip.d.ts","names":[],"sources":["../../src/components/chip.tsx"],"mappings":";;;cAIM,eAAY;;;6CAoBjB;KAEW,YAAY,yBACtB,oBAAoB;EAClB,OAAO;;EAEP;;iBAGY,OACd,WACA,SACA,MACA,MACA,MACA,aACG,SACF,4BAAS,IAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/chip.tsx
|
|
5
|
+
const chipVariants = cva("inline-flex max-w-full items-center gap-1.5 rounded-full border font-medium text-xs transition-colors [&_svg]:size-3 [&_svg]:shrink-0", {
|
|
6
|
+
defaultVariants: {
|
|
7
|
+
size: "default",
|
|
8
|
+
variant: "default"
|
|
9
|
+
},
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
default: "px-2.5 py-1",
|
|
13
|
+
sm: "px-2 py-0.5 text-[11px]"
|
|
14
|
+
},
|
|
15
|
+
variant: {
|
|
16
|
+
accent: "border-transparent bg-accent text-accent-foreground",
|
|
17
|
+
default: "border-border bg-card text-card-foreground",
|
|
18
|
+
muted: "border-transparent bg-muted text-muted-foreground",
|
|
19
|
+
outline: "border-border bg-transparent text-foreground"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
function Chip({ className, variant, size, icon, href, children, ...props }) {
|
|
24
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [icon, /* @__PURE__ */ jsx("span", {
|
|
25
|
+
className: "truncate",
|
|
26
|
+
children
|
|
27
|
+
})] });
|
|
28
|
+
if (href) return /* @__PURE__ */ jsx("a", {
|
|
29
|
+
className: cn(chipVariants({
|
|
30
|
+
size,
|
|
31
|
+
variant
|
|
32
|
+
}), "hover:bg-accent hover:text-accent-foreground", className),
|
|
33
|
+
href,
|
|
34
|
+
...props,
|
|
35
|
+
children: content
|
|
36
|
+
});
|
|
37
|
+
return /* @__PURE__ */ jsx("span", {
|
|
38
|
+
className: cn(chipVariants({
|
|
39
|
+
size,
|
|
40
|
+
variant
|
|
41
|
+
}), className),
|
|
42
|
+
...props,
|
|
43
|
+
children: content
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Chip, chipVariants };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=chip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chip.js","names":[],"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nconst chipVariants = cva(\n \"inline-flex max-w-full items-center gap-1.5 rounded-full border font-medium text-xs transition-colors [&_svg]:size-3 [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"px-2.5 py-1\",\n sm: \"px-2 py-0.5 text-[11px]\",\n },\n variant: {\n accent: \"border-transparent bg-accent text-accent-foreground\",\n default: \"border-border bg-card text-card-foreground\",\n muted: \"border-transparent bg-muted text-muted-foreground\",\n outline: \"border-border bg-transparent text-foreground\",\n },\n },\n }\n);\n\nexport type ChipProps = ComponentProps<\"span\"> &\n VariantProps<typeof chipVariants> & {\n icon?: ReactNode;\n /** Renders as an anchor when set, keeping the same visual treatment. */\n href?: string;\n };\n\nexport function Chip({\n className,\n variant,\n size,\n icon,\n href,\n children,\n ...props\n}: ChipProps) {\n const content = (\n <>\n {icon}\n <span className=\"truncate\">{children}</span>\n </>\n );\n\n if (href) {\n return (\n <a\n className={cn(\n chipVariants({ size, variant }),\n \"hover:bg-accent hover:text-accent-foreground\",\n className\n )}\n href={href}\n {...(props as ComponentProps<\"a\">)}\n >\n {content}\n </a>\n );\n }\n\n return (\n <span className={cn(chipVariants({ size, variant }), className)} {...props}>\n {content}\n </span>\n );\n}\n\nexport { chipVariants };\n"],"mappings":";;;;AAIA,MAAM,eAAe,IACnB,yIACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SAAS;GACT,IAAI;EACN;EACA,SAAS;GACP,QAAQ;GACR,SAAS;GACT,OAAO;GACP,SAAS;EACX;CACF;AACF,CACF;AASA,SAAgB,KAAK,EACnB,WACA,SACA,MACA,MACA,MACA,UACA,GAAG,SACS;CACZ,MAAM,UACJ,qBAAA,UAAA,EAAA,UAAA,CACG,MACD,oBAAC,QAAD;EAAM,WAAU;EAAY;CAAe,CAAA,CAC3C,EAAA,CAAA;CAGJ,IAAI,MACF,OACE,oBAAC,KAAD;EACE,WAAW,GACT,aAAa;GAAE;GAAM;EAAQ,CAAC,GAC9B,gDACA,SACF;EACM;EACN,GAAK;EAEJ,UAAA;CACA,CAAA;CAIP,OACE,oBAAC,QAAD;EAAM,WAAW,GAAG,aAAa;GAAE;GAAM;EAAQ,CAAC,GAAG,SAAS;EAAG,GAAI;EAClE,UAAA;CACG,CAAA;AAEV"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Chip, ChipProps, chipVariants } from "./chip.js";
|
|
2
|
+
import { LanguageOption, LanguageSwitcher, LanguageSwitcherProps } from "./language-switcher.js";
|
|
3
|
+
import { Marquee, MarqueeProps } from "./marquee.js";
|
|
4
|
+
import { MessageBubble, MessageBubbleProps, MessageRole } from "./message-bubble.js";
|
|
5
|
+
import { StatDirection, StatTile, StatTileProps } from "./stat-tile.js";
|
|
6
|
+
import { StreamingDots, StreamingDotsProps } from "./streaming-dots.js";
|
|
7
|
+
import { ThemeMode, ThemeToggle, ThemeToggleProps } from "./theme-toggle.js";
|
|
8
|
+
export { Chip, ChipProps, LanguageOption, LanguageSwitcher, LanguageSwitcherProps, Marquee, MarqueeProps, MessageBubble, MessageBubbleProps, MessageRole, StatDirection, StatTile, StatTileProps, StreamingDots, StreamingDotsProps, ThemeMode, ThemeToggle, ThemeToggleProps, chipVariants };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Chip, chipVariants } from "./chip.js";
|
|
2
|
+
import { LanguageSwitcher } from "./language-switcher.js";
|
|
3
|
+
import { Marquee } from "./marquee.js";
|
|
4
|
+
import { MessageBubble } from "./message-bubble.js";
|
|
5
|
+
import { StatTile } from "./stat-tile.js";
|
|
6
|
+
import { StreamingDots } from "./streaming-dots.js";
|
|
7
|
+
import { ThemeToggle } from "./theme-toggle.js";
|
|
8
|
+
export { Chip, LanguageSwitcher, Marquee, MessageBubble, StatTile, StreamingDots, ThemeToggle, chipVariants };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/language-switcher.d.ts
|
|
3
|
+
interface LanguageOption {
|
|
4
|
+
/** Optional leading adornment, such as a flag emoji. */
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Locale code, shown on the trigger. */
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
interface LanguageSwitcherProps {
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Accessible name for the trigger. */
|
|
13
|
+
label?: string;
|
|
14
|
+
onValueChange: (value: string) => void;
|
|
15
|
+
options: LanguageOption[];
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
/** Controlled. Wires to next-intl, i18next or anything else from the outside. */
|
|
19
|
+
declare function LanguageSwitcher({ options, value, onValueChange, label, className }: LanguageSwitcherProps): import("react").JSX.Element;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { LanguageOption, LanguageSwitcher, LanguageSwitcherProps };
|
|
22
|
+
//# sourceMappingURL=language-switcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-switcher.d.ts","names":[],"sources":["../../src/components/language-switcher.tsx"],"mappings":";;UAciB;;EAEf,OAAO;EACP;;EAEA;;UAGe;EACf;;EAEA;EACA,gBAAgB;EAChB,SAAS;EACT;;;iBAIc,mBACd,SACA,OACA,eACA,OACA,aACC,wCAAqB,IAAA"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { Button } from "../elements/button.js";
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "../elements/dropdown-menu.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { GlobeIcon } from "lucide-react";
|
|
7
|
+
//#region src/components/language-switcher.tsx
|
|
8
|
+
/** Controlled. Wires to next-intl, i18next or anything else from the outside. */
|
|
9
|
+
function LanguageSwitcher({ options, value, onValueChange, label = "Language", className }) {
|
|
10
|
+
const active = options.find((option) => option.value === value);
|
|
11
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
12
|
+
asChild: true,
|
|
13
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
14
|
+
"aria-label": label,
|
|
15
|
+
className: cn("gap-1.5", className),
|
|
16
|
+
size: "sm",
|
|
17
|
+
variant: "ghost",
|
|
18
|
+
children: [/* @__PURE__ */ jsx(GlobeIcon, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: "font-medium uppercase tracking-wide",
|
|
20
|
+
children: active?.value ?? value
|
|
21
|
+
})]
|
|
22
|
+
})
|
|
23
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
24
|
+
align: "end",
|
|
25
|
+
className: "w-44",
|
|
26
|
+
children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
|
|
27
|
+
onValueChange,
|
|
28
|
+
value,
|
|
29
|
+
children: options.map((option) => /* @__PURE__ */ jsxs(DropdownMenuRadioItem, {
|
|
30
|
+
value: option.value,
|
|
31
|
+
children: [option.icon ? /* @__PURE__ */ jsx("span", {
|
|
32
|
+
"aria-hidden": "true",
|
|
33
|
+
children: option.icon
|
|
34
|
+
}) : null, option.label]
|
|
35
|
+
}, option.value))
|
|
36
|
+
})
|
|
37
|
+
})] });
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { LanguageSwitcher };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=language-switcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-switcher.js","names":[],"sources":["../../src/components/language-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { GlobeIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"@/elements/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n} from \"@/elements/dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface LanguageOption {\n /** Optional leading adornment, such as a flag emoji. */\n icon?: ReactNode;\n label: string;\n /** Locale code, shown on the trigger. */\n value: string;\n}\n\nexport interface LanguageSwitcherProps {\n className?: string;\n /** Accessible name for the trigger. */\n label?: string;\n onValueChange: (value: string) => void;\n options: LanguageOption[];\n value: string;\n}\n\n/** Controlled. Wires to next-intl, i18next or anything else from the outside. */\nexport function LanguageSwitcher({\n options,\n value,\n onValueChange,\n label = \"Language\",\n className,\n}: LanguageSwitcherProps) {\n const active = options.find((option) => option.value === value);\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n aria-label={label}\n className={cn(\"gap-1.5\", className)}\n size=\"sm\"\n variant=\"ghost\"\n >\n <GlobeIcon aria-hidden=\"true\" />\n <span className=\"font-medium uppercase tracking-wide\">\n {active?.value ?? value}\n </span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-44\">\n <DropdownMenuRadioGroup onValueChange={onValueChange} value={value}>\n {options.map((option) => (\n <DropdownMenuRadioItem key={option.value} value={option.value}>\n {option.icon ? (\n <span aria-hidden=\"true\">{option.icon}</span>\n ) : null}\n {option.label}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":";;;;;;;;AAgCA,SAAgB,iBAAiB,EAC/B,SACA,OACA,eACA,QAAQ,YACR,aACwB;CACxB,MAAM,SAAS,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK;CAE9D,OACE,qBAAC,cAAD,EAAA,UAAA,CACE,oBAAC,qBAAD;EAAqB,SAAA;EACnB,UAAA,qBAAC,QAAD;GACE,cAAY;GACZ,WAAW,GAAG,WAAW,SAAS;GAClC,MAAK;GACL,SAAQ;GAJV,UAAA,CAME,oBAAC,WAAD,EAAW,eAAY,OAAQ,CAAA,GAC/B,oBAAC,QAAD;IAAM,WAAU;IACb,UAAA,QAAQ,SAAS;GACd,CAAA,CACA;;CACW,CAAA,GACrB,oBAAC,qBAAD;EAAqB,OAAM;EAAM,WAAU;EACzC,UAAA,oBAAC,wBAAD;GAAuC;GAAsB;GAC1D,UAAA,QAAQ,KAAK,WACZ,qBAAC,uBAAD;IAA0C,OAAO,OAAO;IAAxD,UAAA,CACG,OAAO,OACN,oBAAC,QAAD;KAAM,eAAY;KAAQ,UAAA,OAAO;IAAW,CAAA,IAC1C,MACH,OAAO,KACa;GALK,GAAA,OAAO,KAKZ,CACxB;EACqB,CAAA;CACL,CAAA,CACT,EAAA,CAAA;AAElB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/marquee.d.ts
|
|
3
|
+
interface MarqueeProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Seconds for one full pass. Larger is slower. */
|
|
7
|
+
duration?: number;
|
|
8
|
+
/** Fade the leading and trailing edges into the background. */
|
|
9
|
+
fade?: boolean;
|
|
10
|
+
/** Scroll direction. */
|
|
11
|
+
reverse?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A continuously scrolling strip.
|
|
15
|
+
*
|
|
16
|
+
* Under reduced motion this suppresses the animation and becomes a scrollable
|
|
17
|
+
* region, rather than using the duration tokens. A loop has no final state to
|
|
18
|
+
* land on, so collapsing its duration to zero would be an infinite speed.
|
|
19
|
+
*/
|
|
20
|
+
declare function Marquee({ children, duration, reverse, fade, className }: MarqueeProps): import("react").JSX.Element;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { Marquee, MarqueeProps };
|
|
23
|
+
//# sourceMappingURL=marquee.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marquee.d.ts","names":[],"sources":["../../src/components/marquee.tsx"],"mappings":";;UAMiB;EACf,UAAU;EACV;;EAEA;;EAEA;;EAEA;;;;;;;;;iBAac,UACd,UACA,UACA,SACA,MACA,aACC,+BAAY,IAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
5
|
+
//#region src/components/marquee.tsx
|
|
6
|
+
const FADE_MASK = "[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]";
|
|
7
|
+
/**
|
|
8
|
+
* A continuously scrolling strip.
|
|
9
|
+
*
|
|
10
|
+
* Under reduced motion this suppresses the animation and becomes a scrollable
|
|
11
|
+
* region, rather than using the duration tokens. A loop has no final state to
|
|
12
|
+
* land on, so collapsing its duration to zero would be an infinite speed.
|
|
13
|
+
*/
|
|
14
|
+
function Marquee({ children, duration = 30, reverse = false, fade = false, className }) {
|
|
15
|
+
const reduced = useReducedMotion();
|
|
16
|
+
const track = /* @__PURE__ */ jsx("div", {
|
|
17
|
+
"aria-hidden": void 0,
|
|
18
|
+
className: "flex shrink-0 items-center gap-6 pr-6",
|
|
19
|
+
children
|
|
20
|
+
});
|
|
21
|
+
if (reduced) return /* @__PURE__ */ jsx("div", {
|
|
22
|
+
className: cn("overflow-x-auto", className),
|
|
23
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
24
|
+
className: "flex w-max items-center",
|
|
25
|
+
children: track
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
return /* @__PURE__ */ jsx("div", {
|
|
29
|
+
className: cn("overflow-hidden", fade && FADE_MASK, className),
|
|
30
|
+
children: /* @__PURE__ */ jsxs(motion.div, {
|
|
31
|
+
animate: { x: reverse ? ["-50%", "0%"] : ["0%", "-50%"] },
|
|
32
|
+
className: "flex w-max items-center",
|
|
33
|
+
transition: {
|
|
34
|
+
duration,
|
|
35
|
+
ease: "linear",
|
|
36
|
+
repeat: Number.POSITIVE_INFINITY
|
|
37
|
+
},
|
|
38
|
+
children: [track, /* @__PURE__ */ jsx("div", {
|
|
39
|
+
"aria-hidden": "true",
|
|
40
|
+
className: "flex shrink-0 items-center gap-6 pr-6",
|
|
41
|
+
children
|
|
42
|
+
})]
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Marquee };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=marquee.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marquee.js","names":[],"sources":["../../src/components/marquee.tsx"],"sourcesContent":["\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface MarqueeProps {\n children: ReactNode;\n className?: string;\n /** Seconds for one full pass. Larger is slower. */\n duration?: number;\n /** Fade the leading and trailing edges into the background. */\n fade?: boolean;\n /** Scroll direction. */\n reverse?: boolean;\n}\n\nconst FADE_MASK =\n \"[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]\";\n\n/**\n * A continuously scrolling strip.\n *\n * Under reduced motion this suppresses the animation and becomes a scrollable\n * region, rather than using the duration tokens. A loop has no final state to\n * land on, so collapsing its duration to zero would be an infinite speed.\n */\nexport function Marquee({\n children,\n duration = 30,\n reverse = false,\n fade = false,\n className,\n}: MarqueeProps) {\n const reduced = useReducedMotion();\n\n const track = (\n <div\n aria-hidden={undefined}\n className=\"flex shrink-0 items-center gap-6 pr-6\"\n >\n {children}\n </div>\n );\n\n if (reduced) {\n return (\n <div className={cn(\"overflow-x-auto\", className)}>\n <div className=\"flex w-max items-center\">{track}</div>\n </div>\n );\n }\n\n return (\n <div className={cn(\"overflow-hidden\", fade && FADE_MASK, className)}>\n <motion.div\n animate={{ x: reverse ? [\"-50%\", \"0%\"] : [\"0%\", \"-50%\"] }}\n className=\"flex w-max items-center\"\n transition={{\n duration,\n ease: \"linear\",\n repeat: Number.POSITIVE_INFINITY,\n }}\n >\n {track}\n <div\n aria-hidden=\"true\"\n className=\"flex shrink-0 items-center gap-6 pr-6\"\n >\n {children}\n </div>\n </motion.div>\n </div>\n );\n}\n"],"mappings":";;;;;AAiBA,MAAM,YACJ;;;;;;;;AASF,SAAgB,QAAQ,EACtB,UACA,WAAW,IACX,UAAU,OACV,OAAO,OACP,aACe;CACf,MAAM,UAAU,iBAAiB;CAEjC,MAAM,QACJ,oBAAC,OAAD;EACE,eAAa,KAAA;EACb,WAAU;EAET;CACE,CAAA;CAGP,IAAI,SACF,OACE,oBAAC,OAAD;EAAK,WAAW,GAAG,mBAAmB,SAAS;EAC7C,UAAA,oBAAC,OAAD;GAAK,WAAU;GAA2B,UAAA;EAAW,CAAA;CAClD,CAAA;CAIT,OACE,oBAAC,OAAD;EAAK,WAAW,GAAG,mBAAmB,QAAQ,WAAW,SAAS;EAChE,UAAA,qBAAC,OAAO,KAAR;GACE,SAAS,EAAE,GAAG,UAAU,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,MAAM,EAAE;GACxD,WAAU;GACV,YAAY;IACV;IACA,MAAM;IACN,QAAQ,OAAO;GACjB;GAPF,UAAA,CASG,OACD,oBAAC,OAAD;IACE,eAAY;IACZ,WAAU;IAET;GACE,CAAA,CACK;;CACT,CAAA;AAET"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/message-bubble.d.ts
|
|
3
|
+
type MessageRole = "user" | "assistant" | "tool";
|
|
4
|
+
interface MessageBubbleProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
/** Rendered below the body, typically a row of Chips. */
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
/** Who sent it. Maps directly from an AI SDK message role. */
|
|
10
|
+
from: MessageRole;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A chat message.
|
|
14
|
+
*
|
|
15
|
+
* Animates in on mount, and out when removed, but the exit only runs if the
|
|
16
|
+
* consumer wraps the list in `AnimatePresence` and gives each bubble a stable
|
|
17
|
+
* `key`. Without that the element unmounts immediately and there is nothing
|
|
18
|
+
* left to animate.
|
|
19
|
+
*
|
|
20
|
+
* Under reduced motion the movement is dropped and only the fade remains,
|
|
21
|
+
* rather than removing the animation outright. The preference targets
|
|
22
|
+
* vestibular triggers, which means translation and scale, so a cross-fade stays
|
|
23
|
+
* within it while still marking that a new message arrived.
|
|
24
|
+
*/
|
|
25
|
+
declare function MessageBubble({ from, children, footer, className }: MessageBubbleProps): import("react").JSX.Element;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { MessageBubble, MessageBubbleProps, MessageRole };
|
|
28
|
+
//# sourceMappingURL=message-bubble.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-bubble.d.ts","names":[],"sources":["../../src/components/message-bubble.tsx"],"mappings":";;KAMY;UAuBK;EACf,UAAU;EACV;;EAEA,SAAS;;EAET,MAAM;;;;;;;;;;;;;;;iBAgBQ,gBACd,MACA,UACA,QACA,aACC,qCAAkB,IAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
5
|
+
//#region src/components/message-bubble.tsx
|
|
6
|
+
const ROLE = {
|
|
7
|
+
assistant: {
|
|
8
|
+
align: "justify-start",
|
|
9
|
+
offset: -8,
|
|
10
|
+
surface: "border bg-card text-card-foreground"
|
|
11
|
+
},
|
|
12
|
+
tool: {
|
|
13
|
+
align: "justify-start",
|
|
14
|
+
offset: -8,
|
|
15
|
+
surface: "border border-dashed bg-muted text-muted-foreground"
|
|
16
|
+
},
|
|
17
|
+
user: {
|
|
18
|
+
align: "justify-end",
|
|
19
|
+
offset: 8,
|
|
20
|
+
surface: "bg-primary text-primary-foreground"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A chat message.
|
|
25
|
+
*
|
|
26
|
+
* Animates in on mount, and out when removed, but the exit only runs if the
|
|
27
|
+
* consumer wraps the list in `AnimatePresence` and gives each bubble a stable
|
|
28
|
+
* `key`. Without that the element unmounts immediately and there is nothing
|
|
29
|
+
* left to animate.
|
|
30
|
+
*
|
|
31
|
+
* Under reduced motion the movement is dropped and only the fade remains,
|
|
32
|
+
* rather than removing the animation outright. The preference targets
|
|
33
|
+
* vestibular triggers, which means translation and scale, so a cross-fade stays
|
|
34
|
+
* within it while still marking that a new message arrived.
|
|
35
|
+
*/
|
|
36
|
+
function MessageBubble({ from, children, footer, className }) {
|
|
37
|
+
const reduced = useReducedMotion();
|
|
38
|
+
const { align, offset, surface } = ROLE[from];
|
|
39
|
+
return /* @__PURE__ */ jsx(motion.div, {
|
|
40
|
+
animate: {
|
|
41
|
+
opacity: 1,
|
|
42
|
+
scale: 1,
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0
|
|
45
|
+
},
|
|
46
|
+
className: cn("flex w-full", align, className),
|
|
47
|
+
exit: reduced ? { opacity: 0 } : {
|
|
48
|
+
opacity: 0,
|
|
49
|
+
scale: .97,
|
|
50
|
+
y: -4
|
|
51
|
+
},
|
|
52
|
+
initial: reduced ? { opacity: 0 } : {
|
|
53
|
+
opacity: 0,
|
|
54
|
+
scale: .98,
|
|
55
|
+
x: offset,
|
|
56
|
+
y: 6
|
|
57
|
+
},
|
|
58
|
+
transition: reduced ? {
|
|
59
|
+
duration: .15,
|
|
60
|
+
ease: "linear"
|
|
61
|
+
} : {
|
|
62
|
+
damping: 26,
|
|
63
|
+
stiffness: 320,
|
|
64
|
+
type: "spring"
|
|
65
|
+
},
|
|
66
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
67
|
+
className: cn("max-w-[80%] rounded-lg px-4 py-3 text-sm leading-relaxed", surface),
|
|
68
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
69
|
+
className: "whitespace-pre-wrap",
|
|
70
|
+
children
|
|
71
|
+
}), footer ? /* @__PURE__ */ jsx("div", {
|
|
72
|
+
className: "mt-2 flex flex-wrap gap-1",
|
|
73
|
+
children: footer
|
|
74
|
+
}) : null]
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { MessageBubble };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=message-bubble.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-bubble.js","names":[],"sources":["../../src/components/message-bubble.tsx"],"sourcesContent":["\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport type MessageRole = \"user\" | \"assistant\" | \"tool\";\n\nconst ROLE = {\n assistant: {\n align: \"justify-start\",\n offset: -8,\n surface: \"border bg-card text-card-foreground\",\n },\n tool: {\n align: \"justify-start\",\n offset: -8,\n surface: \"border border-dashed bg-muted text-muted-foreground\",\n },\n user: {\n align: \"justify-end\",\n offset: 8,\n surface: \"bg-primary text-primary-foreground\",\n },\n} satisfies Record<\n MessageRole,\n { align: string; offset: number; surface: string }\n>;\n\nexport interface MessageBubbleProps {\n children: ReactNode;\n className?: string;\n /** Rendered below the body, typically a row of Chips. */\n footer?: ReactNode;\n /** Who sent it. Maps directly from an AI SDK message role. */\n from: MessageRole;\n}\n\n/**\n * A chat message.\n *\n * Animates in on mount, and out when removed, but the exit only runs if the\n * consumer wraps the list in `AnimatePresence` and gives each bubble a stable\n * `key`. Without that the element unmounts immediately and there is nothing\n * left to animate.\n *\n * Under reduced motion the movement is dropped and only the fade remains,\n * rather than removing the animation outright. The preference targets\n * vestibular triggers, which means translation and scale, so a cross-fade stays\n * within it while still marking that a new message arrived.\n */\nexport function MessageBubble({\n from,\n children,\n footer,\n className,\n}: MessageBubbleProps) {\n const reduced = useReducedMotion();\n const { align, offset, surface } = ROLE[from];\n\n return (\n <motion.div\n animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n className={cn(\"flex w-full\", align, className)}\n exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.97, y: -4 }}\n initial={\n reduced ? { opacity: 0 } : { opacity: 0, scale: 0.98, x: offset, y: 6 }\n }\n transition={\n reduced\n ? { duration: 0.15, ease: \"linear\" }\n : { damping: 26, stiffness: 320, type: \"spring\" }\n }\n >\n <div\n className={cn(\n \"max-w-[80%] rounded-lg px-4 py-3 text-sm leading-relaxed\",\n surface\n )}\n >\n <div className=\"whitespace-pre-wrap\">{children}</div>\n {footer ? (\n <div className=\"mt-2 flex flex-wrap gap-1\">{footer}</div>\n ) : null}\n </div>\n </motion.div>\n );\n}\n"],"mappings":";;;;;AAQA,MAAM,OAAO;CACX,WAAW;EACT,OAAO;EACP,QAAQ;EACR,SAAS;CACX;CACA,MAAM;EACJ,OAAO;EACP,QAAQ;EACR,SAAS;CACX;CACA,MAAM;EACJ,OAAO;EACP,QAAQ;EACR,SAAS;CACX;AACF;;;;;;;;;;;;;;AA2BA,SAAgB,cAAc,EAC5B,MACA,UACA,QACA,aACqB;CACrB,MAAM,UAAU,iBAAiB;CACjC,MAAM,EAAE,OAAO,QAAQ,YAAY,KAAK;CAExC,OACE,oBAAC,OAAO,KAAR;EACE,SAAS;GAAE,SAAS;GAAG,OAAO;GAAG,GAAG;GAAG,GAAG;EAAE;EAC5C,WAAW,GAAG,eAAe,OAAO,SAAS;EAC7C,MAAM,UAAU,EAAE,SAAS,EAAE,IAAI;GAAE,SAAS;GAAG,OAAO;GAAM,GAAG;EAAG;EAClE,SACE,UAAU,EAAE,SAAS,EAAE,IAAI;GAAE,SAAS;GAAG,OAAO;GAAM,GAAG;GAAQ,GAAG;EAAE;EAExE,YACE,UACI;GAAE,UAAU;GAAM,MAAM;EAAS,IACjC;GAAE,SAAS;GAAI,WAAW;GAAK,MAAM;EAAS;EAGpD,UAAA,qBAAC,OAAD;GACE,WAAW,GACT,4DACA,OACF;GAJF,UAAA,CAME,oBAAC,OAAD;IAAK,WAAU;IAAuB;GAAc,CAAA,GACnD,SACC,oBAAC,OAAD;IAAK,WAAU;IAA6B,UAAA;GAAY,CAAA,IACtD,IACD;;CACK,CAAA;AAEhB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/stat-tile.d.ts
|
|
3
|
+
type StatDirection = "up" | "down" | "flat";
|
|
4
|
+
interface StatTileProps {
|
|
5
|
+
caption?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
/** Optional change indicator. Colour comes from the positive/negative tokens. */
|
|
8
|
+
delta?: {
|
|
9
|
+
direction: StatDirection;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
label: string;
|
|
13
|
+
value: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
declare function StatTile({ label, value, delta, caption, className }: StatTileProps): import("react").JSX.Element;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { StatDirection, StatTile, StatTileProps };
|
|
18
|
+
//# sourceMappingURL=stat-tile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat-tile.d.ts","names":[],"sources":["../../src/components/stat-tile.tsx"],"mappings":";;KAKY;UAWK;EACf;EACA;;EAEA;IAAU,WAAW;IAAe;;EACpC;EACA,OAAO;;iBAGO,WACd,OACA,OACA,OACA,SACA,aACC,gCAAa,IAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Card, CardContent } from "../elements/card.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon } from "lucide-react";
|
|
5
|
+
//#region src/components/stat-tile.tsx
|
|
6
|
+
const DIRECTION = {
|
|
7
|
+
down: {
|
|
8
|
+
className: "text-negative",
|
|
9
|
+
icon: ArrowDownIcon
|
|
10
|
+
},
|
|
11
|
+
flat: {
|
|
12
|
+
className: "text-muted-foreground",
|
|
13
|
+
icon: ArrowRightIcon
|
|
14
|
+
},
|
|
15
|
+
up: {
|
|
16
|
+
className: "text-positive",
|
|
17
|
+
icon: ArrowUpIcon
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function StatTile({ label, value, delta, caption, className }) {
|
|
21
|
+
const direction = delta ? DIRECTION[delta.direction] : null;
|
|
22
|
+
const DeltaIcon = direction?.icon;
|
|
23
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
24
|
+
className: cn("gap-0 py-4", className),
|
|
25
|
+
children: /* @__PURE__ */ jsxs(CardContent, {
|
|
26
|
+
className: "px-4",
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx("div", {
|
|
29
|
+
className: "font-medium text-muted-foreground text-xs uppercase tracking-wide",
|
|
30
|
+
children: label
|
|
31
|
+
}),
|
|
32
|
+
/* @__PURE__ */ jsx("div", {
|
|
33
|
+
className: "mt-1 font-mono font-semibold text-2xl text-foreground tabular-nums",
|
|
34
|
+
children: value
|
|
35
|
+
}),
|
|
36
|
+
delta && direction && DeltaIcon ? /* @__PURE__ */ jsxs("div", {
|
|
37
|
+
className: cn("mt-1 flex items-center gap-1 font-medium text-xs tabular-nums", direction.className),
|
|
38
|
+
children: [/* @__PURE__ */ jsx(DeltaIcon, {
|
|
39
|
+
"aria-hidden": "true",
|
|
40
|
+
className: "size-3"
|
|
41
|
+
}), delta.label]
|
|
42
|
+
}) : null,
|
|
43
|
+
caption ? /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "mt-1 text-muted-foreground text-xs",
|
|
45
|
+
children: caption
|
|
46
|
+
}) : null
|
|
47
|
+
]
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { StatTile };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=stat-tile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat-tile.js","names":[],"sources":["../../src/components/stat-tile.tsx"],"sourcesContent":["import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Card, CardContent } from \"@/elements/card\";\nimport { cn } from \"@/lib/cn\";\n\nexport type StatDirection = \"up\" | \"down\" | \"flat\";\n\nconst DIRECTION = {\n down: { className: \"text-negative\", icon: ArrowDownIcon },\n flat: { className: \"text-muted-foreground\", icon: ArrowRightIcon },\n up: { className: \"text-positive\", icon: ArrowUpIcon },\n} satisfies Record<\n StatDirection,\n { className: string; icon: typeof ArrowUpIcon }\n>;\n\nexport interface StatTileProps {\n caption?: string;\n className?: string;\n /** Optional change indicator. Colour comes from the positive/negative tokens. */\n delta?: { direction: StatDirection; label: string };\n label: string;\n value: ReactNode;\n}\n\nexport function StatTile({\n label,\n value,\n delta,\n caption,\n className,\n}: StatTileProps) {\n const direction = delta ? DIRECTION[delta.direction] : null;\n const DeltaIcon = direction?.icon;\n\n return (\n <Card className={cn(\"gap-0 py-4\", className)}>\n <CardContent className=\"px-4\">\n <div className=\"font-medium text-muted-foreground text-xs uppercase tracking-wide\">\n {label}\n </div>\n <div className=\"mt-1 font-mono font-semibold text-2xl text-foreground tabular-nums\">\n {value}\n </div>\n {delta && direction && DeltaIcon ? (\n <div\n className={cn(\n \"mt-1 flex items-center gap-1 font-medium text-xs tabular-nums\",\n direction.className\n )}\n >\n <DeltaIcon aria-hidden=\"true\" className=\"size-3\" />\n {delta.label}\n </div>\n ) : null}\n {caption ? (\n <div className=\"mt-1 text-muted-foreground text-xs\">{caption}</div>\n ) : null}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;AAOA,MAAM,YAAY;CAChB,MAAM;EAAE,WAAW;EAAiB,MAAM;CAAc;CACxD,MAAM;EAAE,WAAW;EAAyB,MAAM;CAAe;CACjE,IAAI;EAAE,WAAW;EAAiB,MAAM;CAAY;AACtD;AAcA,SAAgB,SAAS,EACvB,OACA,OACA,OACA,SACA,aACgB;CAChB,MAAM,YAAY,QAAQ,UAAU,MAAM,aAAa;CACvD,MAAM,YAAY,WAAW;CAE7B,OACE,oBAAC,MAAD;EAAM,WAAW,GAAG,cAAc,SAAS;EACzC,UAAA,qBAAC,aAAD;GAAa,WAAU;GAAvB,UAAA;IACE,oBAAC,OAAD;KAAK,WAAU;KACZ,UAAA;IACE,CAAA;IACL,oBAAC,OAAD;KAAK,WAAU;KACZ,UAAA;IACE,CAAA;IACJ,SAAS,aAAa,YACrB,qBAAC,OAAD;KACE,WAAW,GACT,iEACA,UAAU,SACZ;KAJF,UAAA,CAME,oBAAC,WAAD;MAAW,eAAY;MAAO,WAAU;KAAU,CAAA,GACjD,MAAM,KACJ;IACH,CAAA,IAAA;IACH,UACC,oBAAC,OAAD;KAAK,WAAU;KAAsC,UAAA;IAAa,CAAA,IAChE;GACO;;CACT,CAAA;AAEV"}
|