@tiniestcloud/create 0.0.0-stage → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/index.mjs +117 -0
- package/package.json +33 -4
- package/sdk.d.ts +290 -0
- package/skills/tiny-design/SKILL.md +333 -0
- package/skills/tiny-design/app.md +72 -0
- package/skills/tiny-design/dashboard.md +54 -0
- package/skills/tiny-design/form.md +48 -0
- package/skills/tiny-design/presentation.md +127 -0
- package/skills/tiny-design/website.md +53 -0
- package/templates/app/AGENTS.md +41 -0
- package/templates/app/CLAUDE.md +1 -0
- package/templates/app/README.md +10 -0
- package/templates/app/components.json +25 -0
- package/templates/app/gitignore +24 -0
- package/templates/app/index.html +27 -0
- package/templates/app/package.json +32 -0
- package/templates/app/src/App.tsx +39 -0
- package/templates/app/src/components/theme-toggle.tsx +57 -0
- package/templates/app/src/components/ui/button.tsx +66 -0
- package/templates/app/src/components/ui/tooltip.tsx +54 -0
- package/templates/app/src/index.css +130 -0
- package/templates/app/src/lib/theme.ts +39 -0
- package/templates/app/src/lib/tiny.tsx +59 -0
- package/templates/app/src/lib/utils.ts +1 -0
- package/templates/app/src/main.tsx +20 -0
- package/templates/app/src/pages/home.tsx +10 -0
- package/templates/app/tsconfig.app.json +29 -0
- package/templates/app/tsconfig.json +12 -0
- package/templates/app/tsconfig.node.json +24 -0
- package/templates/app/vite.config.ts +14 -0
- package/templates/site/AGENTS.md +23 -0
- package/templates/site/CLAUDE.md +1 -0
- package/templates/site/README.md +10 -0
- package/templates/site/astro.config.mjs +11 -0
- package/templates/site/gitignore +24 -0
- package/templates/site/package.json +22 -0
- package/templates/site/src/layouts/Layout.astro +30 -0
- package/templates/site/src/pages/about.astro +10 -0
- package/templates/site/src/pages/index.astro +10 -0
- package/templates/site/src/styles/global.css +1 -0
- package/templates/site/tsconfig.json +5 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Designing a website — pages people read
|
|
2
|
+
|
|
3
|
+
> If the person has design standards of their own — a design system, components, a brand guide, an app or site to match — follow those, and use this playbook only for what they leave open. If they gave none, follow it all.
|
|
4
|
+
|
|
5
|
+
A landing page, a portfolio, a small business, an event, docs or a blog: people arrive from a
|
|
6
|
+
link, decide in seconds whether it is for them, read, and maybe act once. Build it on the site
|
|
7
|
+
starter (Astro), so every page arrives as finished HTML that search engines and link previews
|
|
8
|
+
can read.
|
|
9
|
+
|
|
10
|
+
## Shape by purpose
|
|
11
|
+
|
|
12
|
+
- **Landing page:** a hero that says what it is and for whom → proof (logos, numbers, a quote)
|
|
13
|
+
→ how it works in three steps → what you get → questions → one clear final call to action.
|
|
14
|
+
- **Small business:** what, where and when first — hours, location, the menu or services, how
|
|
15
|
+
to book or call — then the story and the photos.
|
|
16
|
+
- **Portfolio:** the work first, big; a short line about the person; each project with the
|
|
17
|
+
problem, the result and the images.
|
|
18
|
+
- **Event:** name, date, place and the register button above the fold; schedule, speakers,
|
|
19
|
+
getting there, questions.
|
|
20
|
+
- **Docs or blog:** a reading layout — a narrow column (about 65 characters), a table of
|
|
21
|
+
contents, clear headings, code and images that fit the column.
|
|
22
|
+
|
|
23
|
+
## The hero
|
|
24
|
+
|
|
25
|
+
Open with the most characteristic thing in the subject's world: the product in use, the dish,
|
|
26
|
+
the building, the work itself — not a gradient with three statistics. A headline under about
|
|
27
|
+
ten words that says what it is and for whom; one primary action and at most one secondary.
|
|
28
|
+
|
|
29
|
+
## Craft
|
|
30
|
+
|
|
31
|
+
- Rhythm: generous vertical space between sections (80px or more on desktop), content width
|
|
32
|
+
capped (around 1100–1200px), prose narrower. Sections use that width — features side by
|
|
33
|
+
side, work or photos in a grid — rather than one narrow column all the way down. Vary section treatments sparingly — a band of
|
|
34
|
+
colour, a full-bleed image — so the page has a beat without looking like a template.
|
|
35
|
+
- Type can be more expressive than in an app: a display face for headlines, a calm text face
|
|
36
|
+
at 17–19px with a line height around 1.6; `text-wrap: balance` on headings.
|
|
37
|
+
- Images: real photographs or screenshots with one consistent treatment; width and height set,
|
|
38
|
+
lazy below the fold, compressed (WebP or AVIF), alt text that says what is shown.
|
|
39
|
+
- Motion: one orchestrated entrance for the hero, at most a gentle reveal as sections scroll
|
|
40
|
+
in. Never hijack scrolling.
|
|
41
|
+
- Navigation: a header that stays at the top as you scroll (it may shrink), a real menu on
|
|
42
|
+
a phone, a footer with contact and the essentials.
|
|
43
|
+
- Every page has its own title and description, one `h1`, headings in order, and an image for
|
|
44
|
+
link previews.
|
|
45
|
+
- One form (a sign-up, a booking request) follows the form playbook; the site adds the SDK tag
|
|
46
|
+
only for it.
|
|
47
|
+
|
|
48
|
+
## Checks
|
|
49
|
+
|
|
50
|
+
- In five seconds a stranger can say what this is, who it is for and what to do next.
|
|
51
|
+
- It reads well on a phone first; tap targets are big enough and nothing scrolls sideways.
|
|
52
|
+
- The page loads fast: self-hosted fonts, sized images, little JavaScript.
|
|
53
|
+
- Then the general checklist, "Before you call it done".
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# This app runs on Tiniest Cloud
|
|
2
|
+
|
|
3
|
+
React, TypeScript, Vite, Tailwind CSS and shadcn/ui, deployed to Tiniest Cloud as a static build.
|
|
4
|
+
|
|
5
|
+
- Sign-in, data, files and AI come from the global `tiny` (the `<script src="/sdk.js">` tag in
|
|
6
|
+
index.html; types in src/tiny.d.ts; React hooks in src/lib/tiny.tsx). Do not use localStorage,
|
|
7
|
+
write a login flow, or add a server of your own. The Tiniest Cloud guide has the whole SDK.
|
|
8
|
+
- Server logic — anything the person using the app must not be able to change, like a price or a
|
|
9
|
+
booking — is a backend function: `public/_functions/<name>.ts` exporting
|
|
10
|
+
`default async (input, tiny: Tiny.Server) => value`, called from the page with
|
|
11
|
+
`await tiny.call("<name>", input)`. The build copies it into `dist/_functions/`; it runs on
|
|
12
|
+
Tiniest Cloud and is never served.
|
|
13
|
+
- `npm run dev` shows a notice instead of the app: `tiny` only exists on Tiniest Cloud. To see the
|
|
14
|
+
app working, run `npm run build` and deploy this folder with `dist` built inside (no
|
|
15
|
+
node_modules, no .env files): `dist` is served, and the rest is kept as the project's source.
|
|
16
|
+
- Where files go, so every project reads the same way in Tiniest Cloud's Code tab:
|
|
17
|
+
- `src/pages/` — one file per screen (`home.tsx`, `orders.tsx`). App.tsx frames them; with
|
|
18
|
+
more than one, it routes to each with React Router, whose browser routes work as-is (the
|
|
19
|
+
platform answers every path with index.html). Keep Vite's default `base`.
|
|
20
|
+
- `src/components/` — pieces the screens share; `src/components/ui/` for shadcn components.
|
|
21
|
+
- `src/hooks/` — the app's own `useSomething` hooks. The SDK's hooks stay in src/lib/tiny.tsx.
|
|
22
|
+
- `src/lib/` — plain code: helpers, formatting, types (`types.ts`), calculations.
|
|
23
|
+
- `public/` — files served as they are (images, icons), plus `_functions/`, `tiny.rules.json`
|
|
24
|
+
and `tiny.brand.json`.
|
|
25
|
+
- A file past about 300 lines is doing too much: move its parts into components and hooks.
|
|
26
|
+
- Components: `npx shadcn@latest add <name>` adds one to src/components/ui. They are a starting
|
|
27
|
+
point: restyle them to fit this app (radius, size, colour, spacing) in src/components/ui.
|
|
28
|
+
- Design: before the first screen, read the tiny-design skill — `.agents/skills/tiny-design/`
|
|
29
|
+
(the same files are in `.claude/skills/` for Claude Code): SKILL.md, then app.md. Where that
|
|
30
|
+
folder is missing, `tiny_guide` with topic `app` returns the same. Write what you decide in
|
|
31
|
+
DESIGN.md.
|
|
32
|
+
- Look: give the app its own palette in src/index.css (the `:root` and `.dark` blocks) that suits
|
|
33
|
+
what it is and who uses it — never ship the default grey. Lay each screen out for what it
|
|
34
|
+
holds (a sidebar, a grid of cards, a table, a list beside its detail); the frame in App.tsx and
|
|
35
|
+
the first screen in src/pages/home.tsx are placeholders, and a narrow centred column suits only
|
|
36
|
+
one focused task. Light, dark and "match
|
|
37
|
+
device" are chosen with `ThemeToggle` (src/components/theme-toggle.tsx); keep both themes
|
|
38
|
+
looking right.
|
|
39
|
+
- Their own components or brand: if the person points at a folder, a style guide or another app,
|
|
40
|
+
that is the design — reuse their components and bring their colours, fonts and logo into this
|
|
41
|
+
project (copied in, never linked to a path on their computer).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# App
|
|
2
|
+
|
|
3
|
+
Built with React, TypeScript, Vite, Tailwind CSS and shadcn/ui, and hosted on Tiniest Cloud.
|
|
4
|
+
|
|
5
|
+
npm install
|
|
6
|
+
npm run build # the site is in dist/
|
|
7
|
+
|
|
8
|
+
Deploy this folder, with `dist` built inside, to Tiniest Cloud: `dist` is served, and the rest is
|
|
9
|
+
kept as the project's source. `npm run dev` only shows a notice: sign-in and data
|
|
10
|
+
come from the platform, so the app runs once it is deployed.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-nova",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/index.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "@/components",
|
|
17
|
+
"utils": "@/lib/utils",
|
|
18
|
+
"ui": "@/components/ui",
|
|
19
|
+
"lib": "@/lib",
|
|
20
|
+
"hooks": "@/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>App</title>
|
|
7
|
+
<!-- The saved theme (src/lib/theme.ts), applied before anything draws so a dark page never flashes white. -->
|
|
8
|
+
<script>
|
|
9
|
+
(function () {
|
|
10
|
+
var saved = document.cookie.match(/(?:^|; )theme=(light|dark)/);
|
|
11
|
+
var dark = saved ? saved[1] === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
12
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
13
|
+
document.documentElement.style.colorScheme = dark ? "dark" : "light";
|
|
14
|
+
})();
|
|
15
|
+
</script>
|
|
16
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
17
|
+
<link rel="apple-touch-icon" href="/_pwa/apple-touch-icon.png" />
|
|
18
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
19
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
20
|
+
<!-- Tiniest Cloud's SDK: the global `tiny` (sign-in, data, AI, files). Served by the platform; vite-ignore leaves it as written. -->
|
|
21
|
+
<script src="/sdk.js" vite-ignore></script>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div id="root"></div>
|
|
25
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"typecheck": "tsc -b"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
13
|
+
"@tailwindcss/vite": "^4",
|
|
14
|
+
"class-variance-authority": "^0.7.1",
|
|
15
|
+
"cn": "^0.4.0",
|
|
16
|
+
"lucide-react": "^1.48.0",
|
|
17
|
+
"radix-ui": "^1.6.7",
|
|
18
|
+
"react": "^19.2.8",
|
|
19
|
+
"react-dom": "^19.2.8",
|
|
20
|
+
"shadcn": "^4.21.0",
|
|
21
|
+
"tailwindcss": "^4",
|
|
22
|
+
"tw-animate-css": "^1.4.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20",
|
|
26
|
+
"@types/react": "^19",
|
|
27
|
+
"@types/react-dom": "^19",
|
|
28
|
+
"@vitejs/plugin-react": "^6",
|
|
29
|
+
"typescript": "~6",
|
|
30
|
+
"vite": "^8"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button"
|
|
2
|
+
import { ThemeToggle } from "@/components/theme-toggle"
|
|
3
|
+
import { useMe } from "@/lib/tiny"
|
|
4
|
+
import Home from "@/pages/home"
|
|
5
|
+
|
|
6
|
+
export default function App() {
|
|
7
|
+
const me = useMe()
|
|
8
|
+
if (me === undefined) return null
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex min-h-svh flex-col">
|
|
12
|
+
<header className="sticky top-0 z-40 border-b bg-background">
|
|
13
|
+
<div className="mx-auto flex h-14 w-full max-w-7xl items-center justify-between gap-4 px-4 sm:px-6">
|
|
14
|
+
<h1 className="text-base font-medium">App</h1>
|
|
15
|
+
<div className="flex items-center gap-3">
|
|
16
|
+
<ThemeToggle />
|
|
17
|
+
{me ? (
|
|
18
|
+
<div className="flex min-w-0 items-center gap-3 text-sm text-muted-foreground">
|
|
19
|
+
<span className="hidden truncate sm:inline">{me.email}</span>
|
|
20
|
+
<Button variant="outline" size="sm" onClick={() => tiny.logout()}>
|
|
21
|
+
Sign out
|
|
22
|
+
</Button>
|
|
23
|
+
</div>
|
|
24
|
+
) : (
|
|
25
|
+
<Button size="sm" onClick={() => tiny.login()}>
|
|
26
|
+
Sign in
|
|
27
|
+
</Button>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
{/* A placeholder frame around the screens in src/pages. With more than one screen, add
|
|
33
|
+
React Router here: a route per page, the header shared by all of them. */}
|
|
34
|
+
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6">
|
|
35
|
+
<Home />
|
|
36
|
+
</main>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { Monitor, Moon, Sun } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
|
6
|
+
import { getTheme, setTheme, type Theme } from "@/lib/theme"
|
|
7
|
+
|
|
8
|
+
const CHOICES: Array<{ value: Theme; label: string; Icon: typeof Sun }> = [
|
|
9
|
+
{ value: "light", label: "Light", Icon: Sun },
|
|
10
|
+
{ value: "dark", label: "Dark", Icon: Moon },
|
|
11
|
+
{ value: "system", label: "Match device", Icon: Monitor },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
/** Light, dark or the device's setting, as three small buttons with styled tooltips. Restyle it to fit the app. */
|
|
15
|
+
export function ThemeToggle({ className }: { className?: string }) {
|
|
16
|
+
const [theme, setCurrent] = useState<Theme>(getTheme)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const sync = () => setCurrent(getTheme())
|
|
20
|
+
window.addEventListener("themechange", sync)
|
|
21
|
+
return () => window.removeEventListener("themechange", sync)
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<TooltipProvider delayDuration={300}>
|
|
26
|
+
<div
|
|
27
|
+
role="radiogroup"
|
|
28
|
+
aria-label="Theme"
|
|
29
|
+
className={cn("inline-flex items-center gap-0.5 rounded-full border bg-muted/50 p-0.5", className)}
|
|
30
|
+
>
|
|
31
|
+
{CHOICES.map(({ value, label, Icon }) => (
|
|
32
|
+
<Tooltip key={value}>
|
|
33
|
+
<TooltipTrigger asChild>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
role="radio"
|
|
37
|
+
aria-checked={theme === value}
|
|
38
|
+
aria-label={label}
|
|
39
|
+
onClick={() => setTheme(value)}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex size-7 items-center justify-center rounded-full text-muted-foreground transition-colors duration-200",
|
|
42
|
+
"hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
43
|
+
theme === value && "bg-background text-foreground shadow-sm"
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<Icon className="size-3.5" />
|
|
47
|
+
</button>
|
|
48
|
+
</TooltipTrigger>
|
|
49
|
+
<TooltipContent side="bottom" sideOffset={6}>
|
|
50
|
+
{label}
|
|
51
|
+
</TooltipContent>
|
|
52
|
+
</Tooltip>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
</TooltipProvider>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "cn"
|
|
4
|
+
import { Slot } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg 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/50 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",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
12
|
+
outline:
|
|
13
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
14
|
+
secondary:
|
|
15
|
+
"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",
|
|
16
|
+
ghost:
|
|
17
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
18
|
+
destructive:
|
|
19
|
+
"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",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default:
|
|
24
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
25
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
28
|
+
icon: "size-8",
|
|
29
|
+
"icon-xs":
|
|
30
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
"icon-sm":
|
|
32
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
33
|
+
"icon-lg": "size-9",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "default",
|
|
38
|
+
size: "default",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function Button({
|
|
44
|
+
className,
|
|
45
|
+
variant = "default",
|
|
46
|
+
size = "default",
|
|
47
|
+
asChild = false,
|
|
48
|
+
...props
|
|
49
|
+
}: React.ComponentProps<"button"> &
|
|
50
|
+
VariantProps<typeof buttonVariants> & {
|
|
51
|
+
asChild?: boolean
|
|
52
|
+
}) {
|
|
53
|
+
const Comp = asChild ? Slot.Root : "button"
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Comp
|
|
57
|
+
data-slot="button"
|
|
58
|
+
data-variant={variant}
|
|
59
|
+
data-size={size}
|
|
60
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "cn"
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
function TooltipProvider({
|
|
6
|
+
delayDuration = 0,
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipPrimitive.Provider
|
|
11
|
+
data-slot="tooltip-provider"
|
|
12
|
+
delayDuration={delayDuration}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tooltip({
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
21
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TooltipTrigger({
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
27
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TooltipContent({
|
|
31
|
+
className,
|
|
32
|
+
sideOffset = 0,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
36
|
+
return (
|
|
37
|
+
<TooltipPrimitive.Portal>
|
|
38
|
+
<TooltipPrimitive.Content
|
|
39
|
+
data-slot="tooltip-content"
|
|
40
|
+
sideOffset={sideOffset}
|
|
41
|
+
className={cn(
|
|
42
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
|
49
|
+
</TooltipPrimitive.Content>
|
|
50
|
+
</TooltipPrimitive.Portal>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
@import "@fontsource-variable/geist";
|
|
5
|
+
|
|
6
|
+
@custom-variant dark (&:is(.dark *));
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--font-heading: var(--font-sans);
|
|
10
|
+
--font-sans: 'Geist Variable', sans-serif;
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--color-foreground: var(--foreground);
|
|
41
|
+
--color-background: var(--background);
|
|
42
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
43
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
44
|
+
--radius-lg: var(--radius);
|
|
45
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
46
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
47
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
48
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:root {
|
|
52
|
+
--background: oklch(1 0 0);
|
|
53
|
+
--foreground: oklch(0.145 0 0);
|
|
54
|
+
--card: oklch(1 0 0);
|
|
55
|
+
--card-foreground: oklch(0.145 0 0);
|
|
56
|
+
--popover: oklch(1 0 0);
|
|
57
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
58
|
+
--primary: oklch(0.205 0 0);
|
|
59
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--secondary: oklch(0.97 0 0);
|
|
61
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
62
|
+
--muted: oklch(0.97 0 0);
|
|
63
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
64
|
+
--accent: oklch(0.97 0 0);
|
|
65
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
66
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
67
|
+
--border: oklch(0.922 0 0);
|
|
68
|
+
--input: oklch(0.922 0 0);
|
|
69
|
+
--ring: oklch(0.708 0 0);
|
|
70
|
+
--chart-1: oklch(0.87 0 0);
|
|
71
|
+
--chart-2: oklch(0.556 0 0);
|
|
72
|
+
--chart-3: oklch(0.439 0 0);
|
|
73
|
+
--chart-4: oklch(0.371 0 0);
|
|
74
|
+
--chart-5: oklch(0.269 0 0);
|
|
75
|
+
--radius: 0.625rem;
|
|
76
|
+
--sidebar: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
78
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
82
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
83
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dark {
|
|
87
|
+
--background: oklch(0.145 0 0);
|
|
88
|
+
--foreground: oklch(0.985 0 0);
|
|
89
|
+
--card: oklch(0.205 0 0);
|
|
90
|
+
--card-foreground: oklch(0.985 0 0);
|
|
91
|
+
--popover: oklch(0.205 0 0);
|
|
92
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
93
|
+
--primary: oklch(0.922 0 0);
|
|
94
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
95
|
+
--secondary: oklch(0.269 0 0);
|
|
96
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--muted: oklch(0.269 0 0);
|
|
98
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
99
|
+
--accent: oklch(0.269 0 0);
|
|
100
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
101
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
102
|
+
--border: oklch(1 0 0 / 10%);
|
|
103
|
+
--input: oklch(1 0 0 / 15%);
|
|
104
|
+
--ring: oklch(0.556 0 0);
|
|
105
|
+
--chart-1: oklch(0.87 0 0);
|
|
106
|
+
--chart-2: oklch(0.556 0 0);
|
|
107
|
+
--chart-3: oklch(0.439 0 0);
|
|
108
|
+
--chart-4: oklch(0.371 0 0);
|
|
109
|
+
--chart-5: oklch(0.269 0 0);
|
|
110
|
+
--sidebar: oklch(0.205 0 0);
|
|
111
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
113
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
115
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
117
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@layer base {
|
|
121
|
+
* {
|
|
122
|
+
@apply border-border outline-ring/50;
|
|
123
|
+
}
|
|
124
|
+
body {
|
|
125
|
+
@apply bg-background text-foreground;
|
|
126
|
+
}
|
|
127
|
+
html {
|
|
128
|
+
@apply font-sans;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light, dark, or whatever the device prefers — chosen by the person using the app and
|
|
3
|
+
* remembered on this device.
|
|
4
|
+
*
|
|
5
|
+
* Kept in a cookie rather than localStorage: it is a display preference, not data, and
|
|
6
|
+
* Tiniest Cloud flags localStorage in a deploy because that is where apps wrongly keep data.
|
|
7
|
+
* index.html reads the same cookie before anything draws, so a dark page never flashes white.
|
|
8
|
+
*/
|
|
9
|
+
export type Theme = "light" | "dark" | "system"
|
|
10
|
+
|
|
11
|
+
const device = window.matchMedia("(prefers-color-scheme: dark)")
|
|
12
|
+
|
|
13
|
+
export function getTheme(): Theme {
|
|
14
|
+
const match = document.cookie.match(/(?:^|; )theme=(light|dark)/)
|
|
15
|
+
return match ? (match[1] as Theme) : "system"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function apply(theme: Theme) {
|
|
19
|
+
const dark = theme === "dark" || (theme === "system" && device.matches)
|
|
20
|
+
document.documentElement.classList.toggle("dark", dark)
|
|
21
|
+
document.documentElement.style.colorScheme = dark ? "dark" : "light"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setTheme(theme: Theme) {
|
|
25
|
+
document.cookie =
|
|
26
|
+
theme === "system"
|
|
27
|
+
? "theme=; path=/; max-age=0; samesite=lax"
|
|
28
|
+
: `theme=${theme}; path=/; max-age=31536000; samesite=lax`
|
|
29
|
+
apply(theme)
|
|
30
|
+
window.dispatchEvent(new Event("themechange"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Apply the saved choice now, and follow the device while the choice is "system". */
|
|
34
|
+
export function startTheme() {
|
|
35
|
+
apply(getTheme())
|
|
36
|
+
device.addEventListener("change", () => {
|
|
37
|
+
if (getTheme() === "system") apply("system")
|
|
38
|
+
})
|
|
39
|
+
}
|