@hitch42/applet 0.1.0-beta.2
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/dist/_internal/client.d.ts +7 -0
- package/dist/_internal/client.js +60 -0
- package/dist/_internal/server.d.ts +79 -0
- package/dist/_internal/server.js +232 -0
- package/dist/applet.d.ts +1 -0
- package/dist/applet.js +160 -0
- package/dist/auth-DaVK0k5R.js +350 -0
- package/dist/batch-Qa4B4-Yp.d.ts +30 -0
- package/dist/build-BRkmrUcj.js +74 -0
- package/dist/build-CMluoV6q.js +2 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +38 -0
- package/dist/contract-DMrpbaLJ.d.ts +23 -0
- package/dist/deploy-DKB-Tef7.js +2416 -0
- package/dist/generate-BFmIzHaR.js +484 -0
- package/dist/generate-CefnB-bp.js +2 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +136 -0
- package/dist/init-CQRsnd3i.js +240 -0
- package/dist/lazy-client-x0DCI98S.js +64 -0
- package/dist/load-config-BnxocLAQ.js +56 -0
- package/dist/logs-m00pzr_O.js +183 -0
- package/dist/manifest-BQCAxATS.d.ts +81 -0
- package/dist/manifest-DyoJ1MIL.js +174 -0
- package/dist/project-CEmDnlKa.js +346 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +517 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +104 -0
- package/dist/upload-DRbVhuDm.js +114 -0
- package/dist/upload-YbRKEqR6.d.ts +112 -0
- package/dist/variables-CP7Ce3Np.js +133 -0
- package/dist/vite.d.ts +63 -0
- package/dist/vite.js +358 -0
- package/dist/workflow-CTo2kZF1.js +51 -0
- package/dist/workflow-DurM6Fwx.d.ts +28 -0
- package/package.json +104 -0
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +137 -0
- package/scaffold/.agents/skills/hitch-applets/references/client.md +73 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +109 -0
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +171 -0
- package/scaffold/.agents/skills/hitch-applets/references/markdown.md +37 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +152 -0
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +86 -0
- package/scaffold/.agents/skills/hitch-applets/references/server.md +98 -0
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +73 -0
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/applet/components.json +25 -0
- package/scaffold/applet/src/client/components/ui/alert.tsx +73 -0
- package/scaffold/applet/src/client/components/ui/avatar.tsx +100 -0
- package/scaffold/applet/src/client/components/ui/badge.tsx +55 -0
- package/scaffold/applet/src/client/components/ui/button.tsx +90 -0
- package/scaffold/applet/src/client/components/ui/card.tsx +85 -0
- package/scaffold/applet/src/client/components/ui/checkbox.tsx +35 -0
- package/scaffold/applet/src/client/components/ui/dialog.tsx +164 -0
- package/scaffold/applet/src/client/components/ui/dropdown-menu.tsx +235 -0
- package/scaffold/applet/src/client/components/ui/input-group.tsx +144 -0
- package/scaffold/applet/src/client/components/ui/input.tsx +22 -0
- package/scaffold/applet/src/client/components/ui/label.tsx +29 -0
- package/scaffold/applet/src/client/components/ui/popover.tsx +74 -0
- package/scaffold/applet/src/client/components/ui/select.tsx +243 -0
- package/scaffold/applet/src/client/components/ui/separator.tsx +25 -0
- package/scaffold/applet/src/client/components/ui/sheet.tsx +177 -0
- package/scaffold/applet/src/client/components/ui/skeleton.tsx +13 -0
- package/scaffold/applet/src/client/components/ui/spinner.tsx +16 -0
- package/scaffold/applet/src/client/components/ui/switch.tsx +41 -0
- package/scaffold/applet/src/client/components/ui/table.tsx +114 -0
- package/scaffold/applet/src/client/components/ui/tabs.tsx +78 -0
- package/scaffold/applet/src/client/components/ui/textarea.tsx +23 -0
- package/scaffold/applet/src/client/lib/utils.ts +6 -0
- package/scaffold/applet/src/client/styles/globals.css +136 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Applet server
|
|
2
|
+
|
|
3
|
+
Use the server half for work that must not run in the browser.
|
|
4
|
+
|
|
5
|
+
## Choose the correct half
|
|
6
|
+
|
|
7
|
+
- Stay client-only for normal Hitch data queries and mutations.
|
|
8
|
+
- The client SDK uses the current user's identity and access.
|
|
9
|
+
- Use the server half for secrets.
|
|
10
|
+
- Use the server half for external services.
|
|
11
|
+
- Use the server half for imperative Hitch data access.
|
|
12
|
+
- Do not add a server proxy only for filtering or access control.
|
|
13
|
+
|
|
14
|
+
## Define the router
|
|
15
|
+
|
|
16
|
+
- Import `os` from `@orpc/server`.
|
|
17
|
+
- Build an object that contains oRPC procedures.
|
|
18
|
+
- Define each procedure with `os.handler`.
|
|
19
|
+
- Default-export `{ router }`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { os } from "@orpc/server";
|
|
23
|
+
|
|
24
|
+
const router = {
|
|
25
|
+
hello: os.handler(() => ({ hello: "World!" })),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default { router };
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Nest procedure objects when the applet needs nested paths.
|
|
32
|
+
- Do not name a top-level procedure `hitch`.
|
|
33
|
+
- The client reserves `hitch` for the Hitch data client.
|
|
34
|
+
- The generated API client type rejects a router with that reserved key.
|
|
35
|
+
|
|
36
|
+
## Call the router from the client
|
|
37
|
+
|
|
38
|
+
- Import `api` from `@hitch42/applet/client`.
|
|
39
|
+
- Use its generated procedure types.
|
|
40
|
+
- Use TanStack query options for a query.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { useQuery } from "@tanstack/react-query";
|
|
44
|
+
import { api } from "@hitch42/applet/client";
|
|
45
|
+
|
|
46
|
+
const greeting = useQuery(api.hello.queryOptions());
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Use the generated mutation options for a mutation.
|
|
50
|
+
- Keep procedure inputs and outputs inferred from the router.
|
|
51
|
+
|
|
52
|
+
## Access Hitch from the server
|
|
53
|
+
|
|
54
|
+
Import the server exports:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { env, hitch } from "@hitch42/applet/server";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Use `hitch` for imperative calls inside a procedure.
|
|
61
|
+
- A browser request makes `hitch` use that user's identity.
|
|
62
|
+
- A scheduled request makes `hitch` use the applet identity.
|
|
63
|
+
- A request without a complete user or applet identity receives HTTP 401.
|
|
64
|
+
|
|
65
|
+
## Define variables
|
|
66
|
+
|
|
67
|
+
- Add each server variable to the applet manifest.
|
|
68
|
+
- Set `secret: true` for a secret value.
|
|
69
|
+
- Set `optional: true` when the value can be absent.
|
|
70
|
+
- Use a valid environment key.
|
|
71
|
+
- Do not use a key with the reserved `HITCH_` prefix.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
variables: {
|
|
75
|
+
API_URL: {},
|
|
76
|
+
API_TOKEN: { secret: true },
|
|
77
|
+
OPTIONAL_VALUE: { optional: true },
|
|
78
|
+
},
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- Read a required value as a `string` from `env`.
|
|
82
|
+
- Read an optional value as `string | undefined`.
|
|
83
|
+
- Use only variables declared in the manifest.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const response = await fetch(env.API_URL, {
|
|
87
|
+
headers: { authorization: `Bearer ${env.API_TOKEN}` },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Understand identity
|
|
92
|
+
|
|
93
|
+
- A user request carries a user ID and session token.
|
|
94
|
+
- Server `hitch` calls then use the user's access.
|
|
95
|
+
- A scheduled run carries an applet ID and applet token.
|
|
96
|
+
- Server `hitch` calls then use the applet's manifest grants.
|
|
97
|
+
- A scheduled run has no user identity.
|
|
98
|
+
- User-only endpoints reject the applet identity with HTTP 403.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
Use a workflow for server work that runs in steps, retries a step, or waits between steps.
|
|
4
|
+
|
|
5
|
+
## Define a workflow
|
|
6
|
+
|
|
7
|
+
- Import `createWorkflow` from `@hitch42/applet/server`.
|
|
8
|
+
- Give the workflow function a `step` and a typed `payload`.
|
|
9
|
+
- Put each unit of work in `step.do`.
|
|
10
|
+
- Add the workflow to a `workflows` object.
|
|
11
|
+
- Default-export `{ router, workflows }`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { os } from "@orpc/server";
|
|
15
|
+
import { createWorkflow, hitch } from "@hitch42/applet/server";
|
|
16
|
+
|
|
17
|
+
export const workflows = {
|
|
18
|
+
importOrders: createWorkflow(async (step, payload: { since: string }) => {
|
|
19
|
+
const orders = await step.do("fetch", () => fetchOrders(payload.since));
|
|
20
|
+
await step.do(
|
|
21
|
+
"upsert",
|
|
22
|
+
{ retries: { limit: 5, delay: { seconds: 10 }, backoff: "exponential" } },
|
|
23
|
+
() => upsertOrders(orders),
|
|
24
|
+
);
|
|
25
|
+
await step.sleep("cool down", { hours: 1 });
|
|
26
|
+
return { count: orders.length };
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const router = {
|
|
31
|
+
importNow: os.handler(() => hitch.workflows.importOrders.start({ since: "2026-09-01" })),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default { router, workflows };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- The key in `workflows` is the workflow code.
|
|
38
|
+
- Export `workflows` by name so generated types can use it without a type cycle.
|
|
39
|
+
- `step.do(name, fn)` runs `fn` and stores its result. A step can run more than once: if the platform loses the response after `fn` finished, it runs the step again. Make each step safe to repeat.
|
|
40
|
+
- `step.do(name, { retries: { limit: 5, delay: { seconds: 10 }, backoff: "exponential" } }, fn)` retries `fn` after an error. `backoff` is `constant` (default), `linear`, or `exponential`.
|
|
41
|
+
- `step.sleep(name, duration)` pauses the run. Give an object such as `{ minutes: 10 }` or `{ days: 1, hours: 12 }`. The fields are `milliseconds`, `seconds`, `minutes`, `hours`, `days`, and `weeks`, and they add together. A negative field or a total above 365 days fails the run.
|
|
42
|
+
- The return value of the workflow function is the run result.
|
|
43
|
+
|
|
44
|
+
## Start a workflow
|
|
45
|
+
|
|
46
|
+
- Call `hitch.workflows.<code>.start(payload)` from a server procedure.
|
|
47
|
+
- The call returns `{ id }` at once. It does not wait for the run.
|
|
48
|
+
- Call `hitch.workflows.<code>.get(id)` to read `status`, `result`, `error`, and `sleepUntil`.
|
|
49
|
+
- The status is `in_progress`, `sleeping`, `success`, `error`, or `terminated`.
|
|
50
|
+
- Call `hitch.workflows.<code>.list({ status: "open" })` to read the open runs of that workflow. The list has no `result` field.
|
|
51
|
+
- The `status` option accepts one status or `open`. `open` means `in_progress` or `sleeping`.
|
|
52
|
+
- The `limit` option accepts 1 to 100. The default is 50. The list shows the newest runs first.
|
|
53
|
+
- A schedule procedure can start a workflow. The client reaches it through a procedure.
|
|
54
|
+
|
|
55
|
+
## Follow the rules
|
|
56
|
+
|
|
57
|
+
- A run acts as whoever started it. Started from a user's request, `hitch` uses that user's access. Started from a schedule, it acts as the applet with the manifest grants.
|
|
58
|
+
- The platform runs the workflow function again for each step. Code outside `step.do` must be deterministic. Do not read the time or random values outside a step.
|
|
59
|
+
- Use a fixed step name. A name that repeats gets a sequence number.
|
|
60
|
+
- Do not run steps in parallel. Await each step before the next.
|
|
61
|
+
- A step result must be JSON. A result above 1 MiB fails the run.
|
|
62
|
+
- A step result comes back as its JSON form on replay. A `Date` becomes a string. Return plain data.
|
|
63
|
+
- Each step has the same limits as a scheduled run: 30,000 milliseconds of CPU and 1,000 subrequests.
|
|
64
|
+
- A step that still fails after its retries throws inside the workflow function. Catch it to run a compensating step. An error you do not catch fails the run.
|
|
65
|
+
- A run keeps the release it started on. A new release does not stop or change a run that is in progress or asleep.
|
|
66
|
+
- An owner or admin can terminate an open run from the applet settings page. A terminated run has the status `terminated` and does not resume.
|
|
67
|
+
|
|
68
|
+
## Diagnose a failed run
|
|
69
|
+
|
|
70
|
+
- Call `get(id)` and read `error`.
|
|
71
|
+
- Workflow step output is in the applet logs with the type `workflow`, the run id, and the workflow code.
|
|
72
|
+
- Run `applet logs <code> --run <id>` to see one run.
|
|
73
|
+
- Include `--level log` during diagnosis.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Hitch Applet Workspace
|
|
2
|
+
|
|
3
|
+
Before any command or file edit, read and follow `.agents/skills/hitch-applets/SKILL.md`.
|
|
4
|
+
|
|
5
|
+
Use `pnpm` for package commands.
|
|
6
|
+
|
|
7
|
+
Edit only `hitch.config.ts`, each `applet.manifest.ts`, and files in an applet `src` directory.
|
|
8
|
+
The applet CLI replaces all other workspace files.
|
|
9
|
+
|
|
10
|
+
## What makes a good applet
|
|
11
|
+
|
|
12
|
+
- A good applet does one job for one group of people. Ship the whole workflow for that job, and nothing else.
|
|
13
|
+
- Small applets win. Prefer two focused applets over one applet with modes.
|
|
14
|
+
- The organization's Hitch data is the single source of truth. Read and write Hitch objects. Do not build a parallel store, cache, or copy.
|
|
15
|
+
- Do not add sample business records, hard-coded business records, or in-memory business data.
|
|
16
|
+
- Access control belongs to the platform. Each call carries the caller's identity and grants. Do not gate data in applet code — a filter in the UI is a convenience, not a boundary.
|
|
17
|
+
- Stay client-first. A server half exists for secrets, external services, and schedules. When in doubt, leave it out.
|
|
18
|
+
- Use the platform's parts — records, files, schedules, the theme — before you build custom machinery.
|
|
19
|
+
- Design for the user in front of you. Their words name the routes, fields, and buttons. When the data model and their language disagree, raise it — do not paper over it.
|
|
20
|
+
|
|
21
|
+
## Before you build
|
|
22
|
+
|
|
23
|
+
- Do not create an applet until the first complete user workflow is clear.
|
|
24
|
+
- Treat thresholds, status rules, notification timing, notification channels, and recipients as product decisions.
|
|
25
|
+
- Ask one product question when a required decision is missing. Do not write files until the user answers it.
|
|
26
|
+
- Read generated Hitch types before you select objects or attributes.
|
|
27
|
+
- If generated types are unavailable, ask for access or ask for the exact object and attribute names.
|
|
28
|
+
- Start with the smallest complete workflow. Do not add dashboards, metrics, search, filters, or settings without a request.
|
|
29
|
+
|
|
30
|
+
## Work with the user
|
|
31
|
+
|
|
32
|
+
- You will be developing applets for non-technical users. You are a Forward Deployed Engineer for Hitch itself.
|
|
33
|
+
- Your job is to understand user requirements and implement the solution to their problems.
|
|
34
|
+
- Use plain language. Avoid overly technical jargon and focus on understanding.
|
|
35
|
+
- Do not ask the user how to do something or which technical option is better. Lead with value and trade-offs instead.
|
|
36
|
+
- Ask only for decisions that affect the app's behavior. Handle implementation details without asking when a safe default exists.
|
|
37
|
+
- Ask one clear question at a time. Give a recommended choice when the user must make a technical decision.
|
|
38
|
+
- Explain an error in plain language before you show technical details.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "aria-rhea",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/client/styles/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "@/client/components",
|
|
17
|
+
"utils": "@/client/lib/utils",
|
|
18
|
+
"ui": "@/client/components/ui",
|
|
19
|
+
"lib": "@/client/lib",
|
|
20
|
+
"hooks": "@/client/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/client/lib/utils";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"group/alert relative grid w-full gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function Alert({
|
|
23
|
+
className,
|
|
24
|
+
variant,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-slot="alert"
|
|
30
|
+
role="alert"
|
|
31
|
+
className={cn(alertVariants({ variant }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="alert-title"
|
|
41
|
+
className={cn(
|
|
42
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="alert-description"
|
|
54
|
+
className={cn(
|
|
55
|
+
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
data-slot="alert-action"
|
|
67
|
+
className={cn("absolute top-2.5 right-3", className)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/client/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Avatar({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<"div"> & {
|
|
10
|
+
size?: "default" | "sm" | "lg";
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="avatar"
|
|
15
|
+
data-size={size}
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ImageState = "loading" | "loaded" | "error";
|
|
26
|
+
|
|
27
|
+
function AvatarImage({ className, ...props }: React.ComponentProps<"img">) {
|
|
28
|
+
const [state, setState] = React.useState<ImageState>(props.src ? "loading" : "error");
|
|
29
|
+
return (
|
|
30
|
+
<img
|
|
31
|
+
data-slot="avatar-image"
|
|
32
|
+
alt={props.alt || ""}
|
|
33
|
+
data-state={state}
|
|
34
|
+
onLoad={() => setState("loaded")}
|
|
35
|
+
onError={() => setState("error")}
|
|
36
|
+
className={cn(
|
|
37
|
+
"peer aspect-square size-full rounded-full object-cover data-[state=error]:hidden",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function AvatarFallback({ className, ...props }: React.ComponentProps<"div">) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-slot="avatar-fallback"
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs peer-data-[state=error]:flex peer-[*]:hidden",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
data-slot="avatar-badge"
|
|
62
|
+
className={cn(
|
|
63
|
+
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
|
64
|
+
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
|
65
|
+
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
|
66
|
+
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="avatar-group"
|
|
78
|
+
className={cn(
|
|
79
|
+
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
data-slot="avatar-group-count"
|
|
91
|
+
className={cn(
|
|
92
|
+
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/client/lib/utils";
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-2xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
11
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
14
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
15
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
16
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
variant: "default",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function Badge({
|
|
26
|
+
className,
|
|
27
|
+
variant = "default",
|
|
28
|
+
render,
|
|
29
|
+
...props
|
|
30
|
+
}: React.ComponentProps<"span"> &
|
|
31
|
+
VariantProps<typeof badgeVariants> & {
|
|
32
|
+
render?: (props: React.HTMLAttributes<HTMLElement>) => React.ReactNode;
|
|
33
|
+
}) {
|
|
34
|
+
if (render) {
|
|
35
|
+
const renderProps = {
|
|
36
|
+
"data-slot": "badge",
|
|
37
|
+
"data-variant": variant,
|
|
38
|
+
className: cn(badgeVariants({ variant }), className),
|
|
39
|
+
...props,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return render(renderProps);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
data-slot="badge"
|
|
48
|
+
data-variant={variant}
|
|
49
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import {
|
|
6
|
+
Button as ButtonPrimitive,
|
|
7
|
+
Link as LinkPrimitive,
|
|
8
|
+
type ButtonProps as ButtonPrimitiveProps,
|
|
9
|
+
type LinkProps as LinkPrimitiveProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
|
|
12
|
+
import { cn } from "@/client/lib/utils";
|
|
13
|
+
|
|
14
|
+
const buttonVariants = cva(
|
|
15
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-2xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
20
|
+
outline:
|
|
21
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30",
|
|
22
|
+
secondary:
|
|
23
|
+
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
24
|
+
ghost:
|
|
25
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
26
|
+
destructive:
|
|
27
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
28
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
default:
|
|
32
|
+
"h-8 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
|
33
|
+
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
|
34
|
+
sm: "h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
35
|
+
lg: "h-9 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
36
|
+
icon: "size-8",
|
|
37
|
+
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
38
|
+
"icon-sm": "size-7",
|
|
39
|
+
"icon-lg": "size-9",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
variant: "default",
|
|
44
|
+
size: "default",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function Button({
|
|
50
|
+
className,
|
|
51
|
+
variant = "default",
|
|
52
|
+
size = "default",
|
|
53
|
+
...props
|
|
54
|
+
}: Omit<ButtonPrimitiveProps, "className"> &
|
|
55
|
+
React.RefAttributes<HTMLButtonElement> &
|
|
56
|
+
VariantProps<typeof buttonVariants> & {
|
|
57
|
+
className?: string;
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<ButtonPrimitive
|
|
61
|
+
data-slot="button"
|
|
62
|
+
data-variant={variant}
|
|
63
|
+
data-size={size}
|
|
64
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function LinkButton({
|
|
71
|
+
className,
|
|
72
|
+
variant = "default",
|
|
73
|
+
size = "default",
|
|
74
|
+
...props
|
|
75
|
+
}: Omit<LinkPrimitiveProps, "className"> &
|
|
76
|
+
VariantProps<typeof buttonVariants> & {
|
|
77
|
+
className?: string;
|
|
78
|
+
}) {
|
|
79
|
+
return (
|
|
80
|
+
<LinkPrimitive
|
|
81
|
+
data-slot="button"
|
|
82
|
+
data-variant={variant}
|
|
83
|
+
data-size={size}
|
|
84
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { Button, LinkButton, buttonVariants };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/client/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
14
|
+
className={cn(
|
|
15
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-[min(var(--radius-4xl),24px)] bg-card py-(--card-spacing) text-sm text-card-foreground shadow-sm ring-1 ring-foreground/5 [--card-spacing:--spacing(5)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] dark:ring-foreground/10 *:[img:first-child]:rounded-t-[min(var(--radius-4xl),24px)] *:[img:last-child]:rounded-b-[min(var(--radius-4xl),24px)]",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-[min(var(--radius-4xl),24px)] px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
className={cn("font-heading text-base font-medium", className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="card-description"
|
|
50
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
data-slot="card-action"
|
|
60
|
+
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
67
|
+
return (
|
|
68
|
+
<div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} />
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="card-footer"
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex items-center rounded-b-[min(var(--radius-4xl),24px)] px-(--card-spacing) [.border-t]:pt-(--card-spacing)",
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Checkbox as CheckboxPrimitive,
|
|
3
|
+
composeRenderProps,
|
|
4
|
+
type CheckboxProps,
|
|
5
|
+
} from "react-aria-components";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/client/lib/utils";
|
|
8
|
+
import { CheckIcon } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
function Checkbox({ className, children, ...props }: CheckboxProps) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[5px] border border-transparent bg-input/90 transition-shadow outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-transparent after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-focus-visible:border-ring data-focus-visible:ring-3 data-focus-visible:ring-ring/30 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-invalid:data-selected:border-primary dark:data-selected:bg-primary",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{composeRenderProps(children, (children, { isSelected, isIndeterminate }) => (
|
|
21
|
+
<>
|
|
22
|
+
<span
|
|
23
|
+
data-slot="checkbox-indicator"
|
|
24
|
+
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
|
25
|
+
>
|
|
26
|
+
{(isSelected || isIndeterminate) && <CheckIcon />}
|
|
27
|
+
</span>
|
|
28
|
+
{children}
|
|
29
|
+
</>
|
|
30
|
+
))}
|
|
31
|
+
</CheckboxPrimitive>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { Checkbox };
|