@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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/components/chip.d.ts +16 -0
  4. package/dist/components/chip.d.ts.map +1 -0
  5. package/dist/components/chip.js +49 -0
  6. package/dist/components/chip.js.map +1 -0
  7. package/dist/components/index.d.ts +8 -0
  8. package/dist/components/index.js +8 -0
  9. package/dist/components/language-switcher.d.ts +22 -0
  10. package/dist/components/language-switcher.d.ts.map +1 -0
  11. package/dist/components/language-switcher.js +42 -0
  12. package/dist/components/language-switcher.js.map +1 -0
  13. package/dist/components/marquee.d.ts +23 -0
  14. package/dist/components/marquee.d.ts.map +1 -0
  15. package/dist/components/marquee.js +49 -0
  16. package/dist/components/marquee.js.map +1 -0
  17. package/dist/components/message-bubble.d.ts +28 -0
  18. package/dist/components/message-bubble.d.ts.map +1 -0
  19. package/dist/components/message-bubble.js +81 -0
  20. package/dist/components/message-bubble.js.map +1 -0
  21. package/dist/components/stat-tile.d.ts +18 -0
  22. package/dist/components/stat-tile.d.ts.map +1 -0
  23. package/dist/components/stat-tile.js +54 -0
  24. package/dist/components/stat-tile.js.map +1 -0
  25. package/dist/components/streaming-dots.d.ts +19 -0
  26. package/dist/components/streaming-dots.d.ts.map +1 -0
  27. package/dist/components/streaming-dots.js +29 -0
  28. package/dist/components/streaming-dots.js.map +1 -0
  29. package/dist/components/theme-toggle.d.ts +13 -0
  30. package/dist/components/theme-toggle.d.ts.map +1 -0
  31. package/dist/components/theme-toggle.js +54 -0
  32. package/dist/components/theme-toggle.js.map +1 -0
  33. package/dist/elements/avatar.d.ts +14 -0
  34. package/dist/elements/avatar.d.ts.map +1 -0
  35. package/dist/elements/avatar.js +52 -0
  36. package/dist/elements/avatar.js.map +1 -0
  37. package/dist/elements/badge.d.ts +12 -0
  38. package/dist/elements/badge.d.ts.map +1 -0
  39. package/dist/elements/badge.js +29 -0
  40. package/dist/elements/badge.js.map +1 -0
  41. package/dist/elements/button.d.ts +13 -0
  42. package/dist/elements/button.d.ts.map +1 -0
  43. package/dist/elements/button.js +49 -0
  44. package/dist/elements/button.js.map +1 -0
  45. package/dist/elements/card.d.ts +12 -0
  46. package/dist/elements/card.d.ts.map +1 -0
  47. package/dist/elements/card.js +56 -0
  48. package/dist/elements/card.js.map +1 -0
  49. package/dist/elements/checkbox.d.ts +7 -0
  50. package/dist/elements/checkbox.d.ts.map +1 -0
  51. package/dist/elements/checkbox.js +22 -0
  52. package/dist/elements/checkbox.js.map +1 -0
  53. package/dist/elements/dialog.d.ts +20 -0
  54. package/dist/elements/dialog.d.ts.map +1 -0
  55. package/dist/elements/dialog.js +95 -0
  56. package/dist/elements/dialog.js.map +1 -0
  57. package/dist/elements/dropdown-menu.d.ts +28 -0
  58. package/dist/elements/dropdown-menu.d.ts.map +1 -0
  59. package/dist/elements/dropdown-menu.js +124 -0
  60. package/dist/elements/dropdown-menu.js.map +1 -0
  61. package/dist/elements/index.d.ts +17 -0
  62. package/dist/elements/index.js +17 -0
  63. package/dist/elements/input.d.ts +6 -0
  64. package/dist/elements/input.d.ts.map +1 -0
  65. package/dist/elements/input.js +15 -0
  66. package/dist/elements/input.js.map +1 -0
  67. package/dist/elements/label.d.ts +7 -0
  68. package/dist/elements/label.d.ts.map +1 -0
  69. package/dist/elements/label.js +16 -0
  70. package/dist/elements/label.js.map +1 -0
  71. package/dist/elements/popover.d.ts +13 -0
  72. package/dist/elements/popover.d.ts.map +1 -0
  73. package/dist/elements/popover.js +57 -0
  74. package/dist/elements/popover.js.map +1 -0
  75. package/dist/elements/select.d.ts +18 -0
  76. package/dist/elements/select.d.ts.map +1 -0
  77. package/dist/elements/select.js +99 -0
  78. package/dist/elements/select.js.map +1 -0
  79. package/dist/elements/separator.d.ts +7 -0
  80. package/dist/elements/separator.d.ts.map +1 -0
  81. package/dist/elements/separator.js +18 -0
  82. package/dist/elements/separator.js.map +1 -0
  83. package/dist/elements/skeleton.d.ts +5 -0
  84. package/dist/elements/skeleton.d.ts.map +1 -0
  85. package/dist/elements/skeleton.js +14 -0
  86. package/dist/elements/skeleton.js.map +1 -0
  87. package/dist/elements/sonner.d.ts +17 -0
  88. package/dist/elements/sonner.d.ts.map +1 -0
  89. package/dist/elements/sonner.js +40 -0
  90. package/dist/elements/sonner.js.map +1 -0
  91. package/dist/elements/switch.d.ts +9 -0
  92. package/dist/elements/switch.d.ts.map +1 -0
  93. package/dist/elements/switch.js +21 -0
  94. package/dist/elements/switch.js.map +1 -0
  95. package/dist/elements/textarea.d.ts +6 -0
  96. package/dist/elements/textarea.d.ts.map +1 -0
  97. package/dist/elements/textarea.js +14 -0
  98. package/dist/elements/textarea.js.map +1 -0
  99. package/dist/index.d.ts +27 -0
  100. package/dist/index.js +25 -0
  101. package/dist/lib/cn.d.ts +6 -0
  102. package/dist/lib/cn.d.ts.map +1 -0
  103. package/dist/lib/cn.js +10 -0
  104. package/dist/lib/cn.js.map +1 -0
  105. package/dist/styles.css +18 -0
  106. package/dist/tokens/01-ramps.css +142 -0
  107. package/dist/tokens/02-scales.css +28 -0
  108. package/dist/tokens/03-bridge.css +75 -0
  109. package/dist/tokens/index.css +7 -0
  110. package/dist/tokens/palettes/base.css +96 -0
  111. package/dist/tokens/palettes/cyberpunk.css +83 -0
  112. package/dist/tokens/palettes/gold.css +77 -0
  113. package/dist/tokens/palettes/minimal.css +82 -0
  114. 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"}