@iloveagents/foundry-web-primitives 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/AGENTS.md ADDED
@@ -0,0 +1,45 @@
1
+ Stateless UI primitives for the LastSpace agent UI. Published as `@iloveagents/foundry-web-primitives`.
2
+
3
+ # Architecture
4
+
5
+ ```
6
+ src/
7
+ index.ts ← barrel export (all public API)
8
+ utils.ts ← cn() — clsx + tailwind-merge
9
+ button.tsx ← Button + buttonVariants
10
+ checkbox.tsx ← Checkbox (Radix UI)
11
+ field.tsx ← Field, FieldSet (form-row composition)
12
+ input.tsx ← Input + inputClassName
13
+ textarea.tsx ← Textarea + textareaClassName
14
+ ```
15
+
16
+ # Import boundary
17
+
18
+ - MUST NOT import from `@lastspace/*` (zero workspace dependencies — pure leaf package). **Enforced** by the `OPEN_CORE_GRAPH` rule in `packages/web-ui/src/__tests__/open-core-guards.test.ts`.
19
+ - MUST NOT use `@/` path aliases (relative imports only). Convention; not currently guarded for this package — guard coverage tracked under #92's follow-ups.
20
+ - MUST NOT import from any `apps/`, `features/`, or `pages/`. Convention; same status as above.
21
+
22
+ # What goes here
23
+
24
+ Stateless, generic UI building blocks. Specifically:
25
+
26
+ - shadcn-style primitives wrapping Radix UI roots
27
+ - Layout/composition helpers (`Field` rows for forms)
28
+ - Pure styling utilities (`cn`, variant helpers)
29
+
30
+ # What does NOT go here
31
+
32
+ - Any component that touches a Zustand store, agent runtime, or theme runtime
33
+ - Any component that renders into a portal AND needs theming (Dialog, Popover, DropdownMenu — those stay in `@iloveagents/foundry-web-ui` until the theme-runtime coupling is refactored)
34
+ - Any component with backend / API knowledge
35
+
36
+ # Theming
37
+
38
+ Components use Tailwind utility classes referencing CSS variables (shadcn convention):
39
+ - `bg-background`, `text-foreground`, `border-border`, `bg-primary`, `text-primary-foreground`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `text-accent-foreground`, `border-input`, `ring-ring`, `bg-destructive`, `text-destructive-foreground`, `bg-secondary`, `text-secondary-foreground`
40
+
41
+ Consumers must define these tokens in their app's CSS (see `apps/web/src/index.css` for the canonical definitions).
42
+
43
+ # Bundle-size budget
44
+
45
+ `@iloveagents/foundry-web-primitives` ≤30 KB gz — **target, not yet enforced**. The `size-limit` infrastructure scaffolds in a follow-up PR on #92. Until then, manually check the build output. Adding heavier components (Dialog, Popover, table primitives) is fine if they stay zero-coupled, but watch the budget.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iLoveAgents, a brand of Leitwolf GmbH
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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@iloveagents/foundry-web-primitives",
3
+ "version": "0.1.0",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "type": "module",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "peerDependencies": {
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "lucide-react": ">=0.400.0"
17
+ },
18
+ "dependencies": {
19
+ "@radix-ui/react-checkbox": "^1.3.3",
20
+ "@radix-ui/react-slot": "^1.2.0",
21
+ "class-variance-authority": "^0.7.0",
22
+ "clsx": "^2.1.0",
23
+ "tailwind-merge": "^3.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^19.2.2",
27
+ "jsdom": "^28.1.0",
28
+ "typescript": "~5.9.3",
29
+ "vitest": "^4.1.4"
30
+ },
31
+ "scripts": {
32
+ "test:unit": "vitest run --passWithNoTests"
33
+ }
34
+ }
package/src/button.tsx ADDED
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "./utils.ts";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
16
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
17
+ ghost: "hover:bg-accent hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "h-9 px-4 py-2",
22
+ sm: "h-8 rounded-md px-3 text-xs",
23
+ lg: "h-10 rounded-md px-8",
24
+ icon: "h-9 w-9",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ size: "default",
30
+ },
31
+ },
32
+ );
33
+
34
+ export interface ButtonProps
35
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : "button";
42
+ return (
43
+ <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
+ );
45
+ },
46
+ );
47
+ Button.displayName = "Button";
48
+
49
+ export { Button, buttonVariants };
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+
5
+ import { cn } from "./utils.ts";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer size-4 shrink-0 rounded border border-border bg-background shadow-sm outline-none transition",
15
+ "focus-visible:ring-2 focus-visible:ring-primary/15 disabled:cursor-not-allowed disabled:opacity-50",
16
+ "data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
22
+ <Check className="size-3.5" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ));
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
27
+
28
+ export { Checkbox };
package/src/field.tsx ADDED
@@ -0,0 +1,46 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+
3
+ import { cn } from "./utils.ts";
4
+
5
+ export function Field({
6
+ label,
7
+ hint,
8
+ error,
9
+ htmlFor,
10
+ action,
11
+ className,
12
+ children,
13
+ }: {
14
+ label?: ReactNode;
15
+ hint?: ReactNode;
16
+ error?: ReactNode;
17
+ htmlFor?: string;
18
+ action?: ReactNode;
19
+ className?: string;
20
+ children: ReactNode;
21
+ }) {
22
+ return (
23
+ <label className={cn("block space-y-1.5", className)} htmlFor={htmlFor}>
24
+ {label || action ? (
25
+ <span className="flex items-center justify-between gap-3">
26
+ {label ? (
27
+ <span className="text-xs font-medium text-muted-foreground">{label}</span>
28
+ ) : (
29
+ <span />
30
+ )}
31
+ {action}
32
+ </span>
33
+ ) : null}
34
+ {children}
35
+ {hint ? <span className="block text-xs text-muted-foreground">{hint}</span> : null}
36
+ {error ? <span className="block text-xs text-destructive">{error}</span> : null}
37
+ </label>
38
+ );
39
+ }
40
+
41
+ export function FieldSet({
42
+ className,
43
+ ...props
44
+ }: ComponentPropsWithoutRef<"div">) {
45
+ return <div className={cn("grid gap-4", className)} {...props} />;
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { cn } from "./utils.ts";
2
+ export { Button, buttonVariants } from "./button.tsx";
3
+ export type { ButtonProps } from "./button.tsx";
4
+ export { Checkbox } from "./checkbox.tsx";
5
+ export { Input, inputClassName } from "./input.tsx";
6
+ export { Textarea, textareaClassName } from "./textarea.tsx";
7
+ export { Field, FieldSet } from "./field.tsx";
package/src/input.tsx ADDED
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils.ts";
4
+
5
+ const inputClassName =
6
+ "flex h-10 w-full rounded-xl border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/15 disabled:cursor-not-allowed disabled:opacity-50";
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
9
+ ({ className, type = "text", ...props }, ref) => {
10
+ return <input ref={ref} type={type} className={cn(inputClassName, className)} {...props} />;
11
+ },
12
+ );
13
+ Input.displayName = "Input";
14
+
15
+ export { Input, inputClassName };
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils.ts";
4
+
5
+ const textareaClassName =
6
+ "flex min-h-24 w-full rounded-xl border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/15 disabled:cursor-not-allowed disabled:opacity-50";
7
+
8
+ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
9
+ ({ className, ...props }, ref) => {
10
+ return <textarea ref={ref} className={cn(textareaClassName, className)} {...props} />;
11
+ },
12
+ );
13
+ Textarea.displayName = "Textarea";
14
+
15
+ export { Textarea, textareaClassName };
package/src/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }