create-tilcayo-app 0.0.1 → 0.0.3

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.
@@ -0,0 +1,83 @@
1
+ {
2
+ ".env.example": "# Public frontend URL. Vite proxies /api to the local backend during development.\nVITE_API_URL=/api\n",
3
+ "README.md": "# Tilcayo React auth starter\r\n\r\nReact, TypeScript, Vite, React Router, Redux Toolkit, and Tilcayo styles/auth.\r\nUse Node.js 22.12+ or 24+. Dependencies and the production build are prepared by\r\nthe creator unless `--no-install` was supplied.\r\n\r\n```sh\r\nnpm run dev\r\n```\r\n\r\nOpen `/register` to create an account or `/login` to sign in. An authenticated\r\nuser is sent to `/dashboard`, or back to the protected URL they requested.\r\nThe header provides logout. Forms include pending states, native validation,\r\npassword confirmation on registration, and server error feedback.\r\n\r\n## Connect the backend\r\n\r\nThis is a frontend application. A Tilcayo backend with authentication and MongoDB\r\nmust be running for registration and login to work. From the framework checkout's\r\n`examples` directory you can create a separate backend:\r\n\r\n```sh\r\nnode ../packages/create-tilcayo-app/dist/bin.js my-api --type api --auth --yes\r\ncd my-api\r\n```\r\n\r\nConfigure its private `.env` (`MONGODB_URI`), start MongoDB, and run `npm run dev`.\r\nKeep generated auth secrets on the backend only. It listens on port 9149.\r\n\r\nThe frontend's Vite development proxy sends `/api` to `http://127.0.0.1:9149`.\r\nEdit `vite.config.ts` if your backend listens elsewhere. `src/app/auth.ts` uses\r\n`VITE_API_URL` or `/api`; `.env.example` contains only this public setting.\r\nWith a different frontend API origin, configure backend CORS appropriately.\r\n\r\nThe backend must expose:\r\n\r\n| Method | Path |\r\n| --- | --- |\r\n| POST | `/api/auth/register` |\r\n| POST | `/api/auth/login` |\r\n| POST | `/api/auth/logout` |\r\n| POST | `/api/auth/refresh` |\r\n| GET | `/api/auth/me` |\r\n\r\n## Routes and frontend middleware\r\n\r\n- `src/App.tsx`: router and authentication bootstrap.\r\n- `src/routes.tsx`: public, guest-only, and authenticated route objects.\r\n- `src/layouts/app-layout.tsx`: shared page layout.\r\n- `src/components/navigation.tsx`: responsive navigation and logout.\r\n- `src/middleware/auth.tsx`: `RequireAuth` and `GuestOnly` navigation guards.\r\n- `src/pages/login.page.tsx`, `register.page.tsx`: authentication pages.\r\n- `src/components/auth-form.tsx`: shared editable form.\r\n- `src/pages/dashboard.page.tsx`: protected account page.\r\n- `src/app/auth.ts`: configured auth instance, hook, and session bootstrap.\r\n- `src/app/api.ts`: inject resource endpoints into this shared authenticated API.\r\n- `src/app/store.ts`, `hooks.ts`: Redux store and application-specific typed hooks.\r\n\r\nAdd protected routes beneath the `RequireAuth` group in `src/routes.tsx`. These guards\r\ncontrol navigation; backend authentication middleware must still authorize data\r\nrequests. The frontend never contains database credentials or JWT signing secrets.\r\n\r\nTokens are kept in memory, not browser storage. Refresh handles access-token\r\nexpiry within a running session, but reloading the page signs you out. Persistent\r\nsessions require a separate, explicitly designed storage/cookie strategy; no\r\n\"remember me\" behavior is provided. Login/register screens render without a\r\nbackend; submitting them requires the API.\r\n\r\n## Build and deploy\r\n\r\n```sh\r\nnpm run build\r\nnpm run lint\r\nnpm run preview\r\n```\r\n\r\nDeploy `dist/` with client routes falling back to `index.html`. Vite's proxy runs\r\nonly during development: production must route `/api` to your backend or use a\r\npublic `VITE_API_URL` when building. Local generation links the framework's styles\r\nand React packages, so keep the checkout available and rebuild packages after edits.\r\n\r\n## Toasts\r\n\r\nLogin and registration show one success toast for five seconds, with a dismiss button.\r\nMessages use `successMessage`, then the backend response message, then a default.\r\nFor example, `<AuthForm mode=\"login\" successMessage=\"Welcome back!\" />`.\r\nForm errors stay inline. The shared layout keeps the toast visible across navigation.\r\n\r\nElsewhere, import `toast` from `src/lib/toast` and call\r\n`toast.success(\"Profile saved\")`, `toast.error(\"Unable to save\")`, or `toast.dismiss()`.\r\nUse `toast.warning(\"Check your details\")` or `toast.info(\"Update available\")` for other notices.\r\nSuccess is green, errors red, warnings amber, and information blue.\r\nA new toast replaces the previous one. No extra package is required.\r\n\r\nCustomize the shared `Toaster` in `src/layouts/app-layout.tsx`:\r\n\r\n```tsx\r\n<Toaster duration={4000} position=\"top-right\" compact className=\"my-toast\" />\r\n```\r\n\r\n`duration` is in milliseconds (default 5000; 0 disables automatic dismissal).\r\n`position` accepts `top-left`, `top-right`, `bottom-left`, or `bottom-right` (default).\r\n`compact` reduces spacing and font size while keeping the close button easy to tap.\r\n`className` applies to the outer notification container. Style it in `src/App.css`:\r\n\r\n```css\r\n.my-toast {\r\n --toast-background: #fff;\r\n --toast-radius: 8px;\r\n --toast-padding: 8px 12px;\r\n}\r\n```\r\n\r\nThese CSS variables override the defaults, including compact padding.\r\n\r\n## Forgot password\r\n\r\nChoose **Forgot password?** on `/login`, or open `/forgot-password`. Enter your\r\nemail, verify the six-digit code, then choose and confirm a new password. Reset\r\ncredentials stay in memory; refreshing the page starts recovery again.\r\n\r\nOn the backend, configure `sendPasswordResetCode`. Generated APIs and the\r\nbasic-api example support SMTP through backend-only `MAIL_USER` and\r\n`MAIL_PASS` settings. Gmail is the default; change `MAIL_HOST` and `MAIL_PORT`\r\nfor another provider. Never put either value in a `VITE_` variable.\r\nThe backend must expose `/api/auth/forgot-password`, `/api/auth/verify-reset-code`,\r\nand `/api/auth/reset-password` (all POST). Without mail configuration, the form\r\nshows that password recovery is unavailable.\r\n",
4
+ "src/app/api.ts": "import { auth } from './auth'\n\n// Inject all application endpoints into this API to share auth and refresh handling.\nexport const tilcayoApi = auth.api\n",
5
+ "src/app/auth.ts": "import { createTilcayoAuth } from '@tilcayo/react/auth'\n\nexport const auth = createTilcayoAuth({ baseUrl: import.meta.env.VITE_API_URL || '/api' })\nexport const { useAuth, AuthBootstrap } = auth\n",
6
+ "src/app/store.ts": "import { createTilcayoStore } from '@tilcayo/react/redux'\nimport { auth } from './auth'\n\nexport const store = createTilcayoStore({\n api: auth.api,\n reducers: { auth: auth.authReducer },\n devTools: false,\n})\nexport type RootState = ReturnType<typeof store.getState>\nexport type AppDispatch = typeof store.dispatch\n",
7
+ "src/App.css": ".starter-site {\r\n --tl-text: #0a0a0b;\r\n --tl-text-muted: #555e6c;\r\n --tl-primary: #090a0b;\r\n --tl-primary-hover: #282a2d;\r\n --tl-primary-foreground: #fff;\r\n --tl-border: #e0e3e8;\r\n --tl-border-strong: #d5d9df;\r\n --tl-radius-md: 12px;\r\n --tl-surface: #fff;\r\n min-height: 100dvh;\r\n position: relative;\r\n isolation: isolate;\r\n overflow: clip;\r\n background: #fff;\r\n color: var(--tl-text);\r\n font-family: Arial, Helvetica, sans-serif;\r\n}\r\n.starter-site::before {\r\n content: ''; position: absolute; z-index: -1; pointer-events: none;\r\n width: 105rem; height: 100rem; right: -62rem; top: 16rem;\r\n border-radius: 50%; background: linear-gradient(150deg, #f5f6f8 0%, #fff 60%);\r\n}\r\n.starter-header, .starter-main { width: calc(100% - 112px); max-width: 1600px; margin-inline: auto; }\r\n.starter-header { position: relative; z-index: 2; border-bottom: 1px solid #f1f2f4; }\r\n.starter-nav { width: 100%; max-width: none; min-height: 104px; display: flex; align-items: center; gap: 2rem; padding: 0; }\r\n.starter-brand { display: inline-flex; align-items: center; gap: .75rem; color: var(--tl-text); text-decoration: none; font-size: 1.8rem; font-weight: 800; letter-spacing: -.075em; flex-shrink: 0; }\r\n.starter-brand-mark { width: 37px; height: 37px; }\r\n.starter-nav-panel { display: flex; align-items: center; flex: 1; gap: 2rem; }\r\n.starter-nav-links { display: flex; justify-content: center; align-items: center; flex: 1; gap: 2.5rem; }\r\n.starter-nav-links a { position: relative; padding: .8rem 0; color: var(--tl-text); text-decoration: none; font-size: .95rem; font-weight: 500; white-space: nowrap; }\r\n.starter-nav-links a::after { content: ''; position: absolute; bottom: .25rem; left: 0; width: 100%; height: 2px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .15s; }\r\n.starter-nav-links a.active { font-weight: 700; }\r\n.starter-nav-links a.active::after, .starter-nav-links a:hover::after { transform: scaleX(1); }\r\n.starter-nav-actions { display: flex; align-items: center; justify-content: flex-end; gap: 1.25rem; }\r\n.starter-nav-actions a { color: var(--tl-text); text-decoration: none; font-size: .95rem; }\r\n.starter-get-started, .starter-nav-actions .tl-btn { display: inline-flex; align-items: center; justify-content: center; padding: .85rem 1.6rem; min-height: 48px; border: 1px solid #090a0b; border-radius: 11px; background: #090a0b; color: #fff !important; font-weight: 600; white-space: nowrap; }\r\n.starter-get-started:hover { background: #282a2d; }\r\n.starter-nav-notice { margin: 0; font-size: .8rem; color: var(--tl-text-muted); max-width: 15rem; }\r\n.starter-menu-toggle { display: none; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 1px solid var(--tl-border); border-radius: 10px; color: var(--tl-text); background: #fff; cursor: pointer; }\r\n.starter-main { padding: 0 0 72px; }\r\n.starter-hero { display: grid; grid-template-columns: 1fr 1.1fr; align-items: center; gap: clamp(2rem, 5vw, 6rem); margin-top: 56px; margin-bottom: 48px; }\r\n.starter-hero-copy { padding-block: 2rem; position: relative; z-index: 1; }\r\n.starter-eyebrow { margin: 0 0 22px; font-size: .78rem; font-weight: 700; line-height: 1.6; letter-spacing: .15em; text-transform: uppercase; }\r\n.starter-hero h1, .starter-page-intro h1 { margin: 0; color: #050506; font-weight: 800; letter-spacing: -.055em; line-height: 1.04; }\r\n.starter-hero-home h1 { font-size: clamp(3.2rem, 5.2vw, 5.6rem); }\r\n.starter-lead { margin: 22px 0 0; color: var(--tl-text-muted); font-size: clamp(1.05rem, 1.6vw, 1.5rem); line-height: 1.55; }\r\n.starter-hero-home .starter-lead { color: var(--tl-text); font-size: clamp(1.2rem, 1.7vw, 1.65rem); }\r\n.starter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; margin-top: 34px; }\r\n.starter-site .tl-btn { font-family: inherit; font-weight: 600; box-shadow: none; }\r\n.starter-actions .tl-btn { min-height: 50px; gap: 14px; padding: .85rem 1.5rem; border-radius: 12px; font-size: .95rem; }\r\n.starter-site .tl-btn-primary { background: #090a0b; border-color: #090a0b; color: #fff; }\r\n.starter-site .tl-btn-primary:hover { background: #282a2d; }\r\n.starter-site .tl-btn-outline { background: #fff; border-color: #d9dde4; color: var(--tl-text); }\r\n.starter-site .tl-btn-ghost { background: #f4f5f6; border-color: #f4f5f6; color: #353b45; }\r\n.starter-hero-media { height: clamp(340px, 32vw, 510px); overflow: hidden; background: #edf0f4; border-radius: 150px 20px 0 38px; }\r\n.starter-hero-media img { width: 100%; height: 100%; display: block; object-fit: cover; object-position: 62% center; }\r\n.starter-hero-about { grid-template-columns: 1fr 1.12fr; margin-top: 70px; margin-bottom: 58px; gap: 4rem; }\r\n.starter-hero-about h1 { max-width: 38rem; font-size: clamp(2.4rem, 3.7vw, 4rem); line-height: 1.1; }\r\n.starter-hero-about .starter-lead { max-width: 37rem; font-size: clamp(1rem, 1.45vw, 1.35rem); }\r\n.starter-hero-about .starter-hero-media { height: clamp(330px, 29vw, 450px); border-radius: 150px 20px 24px 38px; }\r\n.starter-example-grid { display: grid; gap: 24px; }\r\n.starter-example-grid-home { grid-template-columns: repeat(2, minmax(0, 1fr)); }\r\n.starter-example { display: flex; flex-direction: column; align-items: stretch; gap: 28px; min-width: 0; padding: 26px; border: 1px solid var(--tl-border); border-radius: 14px; background: #fff; box-shadow: 0 5px 14px #0f172a03; }\r\n.starter-example-heading { display: flex; align-items: flex-start; gap: 20px; }\r\n.starter-example-heading h2 { margin: 2px 0 6px; font-size: clamp(1.25rem, 1.5vw, 1.5rem); font-weight: 700; line-height: 1.2; letter-spacing: -.035em; }\r\n.starter-example-heading p { margin: 0; color: var(--tl-text-muted); font-size: clamp(.9rem, 1.1vw, 1.1rem); line-height: 1.5; }\r\n.starter-feature-icon { display: grid; place-items: center; flex: 0 0 54px; width: 54px; height: 54px; background: #f3f4f5; color: #08090b; border-radius: 12px; }\r\n.starter-feature-icon svg { width: 30px; height: 30px; }\r\n.starter-example .tl-input { margin-top: auto; min-height: 48px; background: #fff; padding-inline: 15px; border-color: #d5dae2; border-radius: 8px; font-size: .95rem; }\r\n.starter-site :is(.tl-input,.tl-textarea)::placeholder { color: #697384; opacity: 1; }\r\n.starter-example-badge { display: inline-flex; padding: 8px 19px; border-radius: 7px; background: #101112; color: #fff; font-size: .83rem; box-shadow: 0 2px 4px #0000000d; }\r\n.starter-page-intro { padding-top: clamp(56px, 7vw, 100px); margin-bottom: 58px; }\r\n.starter-page-intro .starter-eyebrow { color: #4d5663; margin-bottom: 24px; }\r\n.starter-page-intro h1 { font-size: clamp(2.6rem, 4.2vw, 4.5rem); }\r\n.starter-page-intro .starter-lead { max-width: 46rem; margin-top: 22px; }\r\n.starter-example-grid-elements { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }\r\n.starter-example-grid-elements .starter-example { min-height: 260px; position: relative; padding: 28px; gap: 35px; }\r\n.starter-card-preview { display: block; width: 100%; height: 100px; object-fit: cover; object-position: 50% 55%; filter: grayscale(1); border-radius: 10px; }\r\n.starter-button-examples { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: auto; }\r\n.starter-button-examples .tl-btn { min-height: 48px; padding: .65rem 1rem; border-radius: 10px; font-size: .95rem; }\r\n.starter-demo-status { font-size: .8rem; color: var(--tl-text-muted); position: absolute; bottom: 6px; left: 28px; }\r\n.starter-more-elements { margin-top: 54px; border-top: 1px solid #e7e9ed; }\r\n.starter-more-elements > summary { width: fit-content; padding: 1.25rem 0; font-size: .9rem; font-weight: 600; cursor: pointer; color: #4d5663; }\r\n.starter-more-content { display: grid; gap: 24px; margin-top: 12px; }\r\n.starter-feature-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; padding-top: 32px; border-top: 1px solid var(--tl-border); }\r\n.starter-feature-strip section { display: flex; gap: 22px; padding: 0 32px; border-left: 1px solid var(--tl-border); }\r\n.starter-feature-strip section:first-child { padding-left: 0; border: 0; }\r\n.starter-feature-strip h2 { margin: 0 0 6px; font-size: 1.2rem; font-weight: 700; letter-spacing: -.035em; }\r\n.starter-feature-strip p { margin: 0; color: var(--tl-text-muted); font-size: .95rem; }\r\n.starter-skip { position: absolute; top: -5rem; left: 1rem; padding: 1rem; background: #fff; color: #111; z-index: 10; }\r\n.starter-skip:focus { top: 1rem; }\r\n@media (max-width: 1100px) {\r\n .starter-nav-links { gap: 1.6rem; }\r\n .starter-nav-panel { gap: 1.5rem; }\r\n .starter-example-grid-elements .starter-example { padding: 22px; }\r\n .starter-example-heading { gap: 14px; }\r\n .starter-example-heading p { font-size: .85rem; }\r\n .starter-button-examples { gap: 7px; }\r\n .starter-button-examples .tl-btn { padding-inline: .75rem; }\r\n .starter-feature-strip section { padding-inline: 20px; gap: 14px; }\r\n}\r\n@media (max-width: 850px) {\r\n .starter-header,.starter-main { width: calc(100% - 48px); }\r\n .starter-nav { min-height: 84px; gap: 1rem; }\r\n .starter-brand { font-size: 1.55rem; }\r\n .starter-brand-mark { width: 30px; height: 30px; }\r\n .starter-nav-links { gap: 1.25rem; }\r\n .starter-get-started { padding-inline: 1rem; }\r\n .starter-nav-actions { gap: .75rem; }\r\n .starter-hero { gap: 2rem; }\r\n .starter-example-grid-elements { grid-template-columns: 1fr; }\r\n .starter-example-grid-elements .starter-example { min-height: 0; }\r\n .starter-card-preview { height: 140px; }\r\n .starter-feature-strip { gap: 26px; grid-template-columns: 1fr; }\r\n .starter-feature-strip section { padding: 0; border: 0; }\r\n}\r\n@media (max-width: 767px) {\r\n .starter-nav { flex-wrap: wrap; justify-content: space-between; padding-block: 14px; }\r\n .starter-menu-toggle { display: inline-flex; }\r\n .starter-nav-panel { display: none; flex: 0 0 100%; flex-direction: column; align-items: stretch; gap: 1rem; padding-block: .75rem; }\r\n .starter-nav-panel.is-open { display: flex; }\r\n .starter-nav-links { flex-direction: column; align-items: stretch; gap: .25rem; }\r\n .starter-nav-links a { padding: .65rem 0; }\r\n .starter-nav-links a::after { width: 32px; }\r\n .starter-nav-actions { justify-content: flex-start; flex-wrap: wrap; }\r\n .starter-nav-notice { max-width: none; }\r\n .starter-hero,.starter-hero-about { grid-template-columns: 1fr; gap: 1rem; margin-top: 28px; margin-bottom: 30px; }\r\n .starter-hero-copy { padding-block: 1rem; }\r\n .starter-hero-home h1 { font-size: clamp(3rem, 11vw, 4.5rem); }\r\n .starter-hero-about h1 { font-size: clamp(2.4rem, 8vw, 3.8rem); }\r\n .starter-eyebrow { font-size: .7rem; margin-bottom: 18px; }\r\n .starter-hero-media,.starter-hero-about .starter-hero-media { height: 300px; border-radius: 85px 14px 18px 24px; }\r\n .starter-actions { gap: 12px; margin-top: 24px; }\r\n .starter-actions .tl-btn { padding-inline: 1.1rem; font-size: .85rem; }\r\n .starter-example-grid-home { grid-template-columns: 1fr; }\r\n .starter-example { padding: 22px; gap: 24px; }\r\n .starter-page-intro { padding-top: 42px; margin-bottom: 34px; }\r\n .starter-page-intro .starter-lead { font-size: 1.1rem; }\r\n .starter-desktop-break { display: none; }\r\n .starter-main { padding-bottom: 44px; }\r\n}\r\n\r\n\r\n.auth-panel {\r\n width: 100%;\r\n max-width: 28rem;\r\n margin-inline: auto;\r\n padding: clamp(1.25rem, 4vw, 2rem);\r\n gap: 1.5rem;\r\n box-shadow: 0 16px 48px rgb(15 23 42 / 0.06);\r\n}\r\n.auth-heading { text-align: center; }\r\n.auth-mark {\r\n display: grid;\r\n place-items: center;\r\n width: 3.5rem;\r\n height: 3.5rem;\r\n margin: 0 auto 1.25rem;\r\n border-radius: 50%;\r\n color: var(--tl-primary);\r\n background: color-mix(in srgb, var(--tl-primary) 12%, var(--tl-surface));\r\n}\r\n.auth-heading h1 { font-size: 1.75rem; letter-spacing: -0.035em; }\r\n.auth-heading p { margin-top: 0.5rem; font-size: 0.9375rem; }\r\n.auth-fields { border: 0; padding: 0; margin: 0; min-width: 0; gap: 1rem; }\r\n.auth-panel .tl-label { font-size: 0.875rem; }\r\n.auth-panel .tl-input { min-height: 3rem; background: var(--tl-surface); }\r\n.auth-password { position: relative; }\r\n.auth-password .tl-input { padding-right: 3rem; }\r\n.auth-password-toggle {\r\n position: absolute;\r\n inset-block: 0;\r\n right: 0;\r\n display: grid;\r\n place-items: center;\r\n width: 3rem;\r\n padding: 0;\r\n border: 0;\r\n border-radius: var(--tl-radius-md);\r\n background: transparent;\r\n color: var(--tl-text-muted);\r\n cursor: pointer;\r\n}\r\n.auth-password-toggle:hover { color: var(--tl-text); }\r\n.auth-password-toggle:focus-visible { outline: 2px solid var(--tl-focus); outline-offset: -4px; }\r\n.auth-password-toggle:disabled { cursor: wait; opacity: 0.5; }\r\n.auth-submit { width: 100%; min-height: 3rem; margin-top: 0.25rem; }\r\n.auth-footer { text-align: center; font-size: 0.875rem; }\r\n.auth-footer a { font-weight: 600; text-decoration: none; }\r\n.auth-footer a:hover { text-decoration: underline; }\r\n\r\n.starter-toaster {\r\n position: fixed;\r\n z-index: 100;\r\n width: min(24rem, calc(100% - 2rem));\r\n pointer-events: none;\r\n}\r\n.starter-toaster[data-position^=\"top\"] { top: 1rem; }\r\n.starter-toaster[data-position^=\"bottom\"] { bottom: 1rem; }\r\n.starter-toaster[data-position$=\"left\"] { left: 1rem; }\r\n.starter-toaster[data-position$=\"right\"] { right: 1rem; }\r\n.starter-toaster[data-compact] { --toast-default-padding: 0.25rem 0.5rem 0.25rem 0.75rem; }\r\n.starter-toaster[data-compact] .starter-toast { gap: 0.5rem; font-size: 0.875rem; }\r\n.starter-toast {\r\n display: flex;\r\n align-items: center;\r\n gap: 1rem;\r\n padding: var(--toast-padding, var(--toast-default-padding, 0.75rem 0.75rem 0.75rem 1rem));\r\n border: 1px solid var(--tl-border);\r\n border-left: 3px solid currentColor;\r\n border-radius: var(--toast-radius, var(--tl-radius-md));\r\n background: var(--toast-background, var(--tl-surface));\r\n box-shadow: var(--tl-shadow-md);\r\n pointer-events: auto;\r\n}\r\n.starter-toast-success { color: var(--tl-success); }\r\n.starter-toast-warning { color: var(--tl-warning); }\r\n.starter-toast-info { color: var(--tl-info); }\r\n.starter-toast-error { color: var(--tl-danger); }\r\n.starter-toast p { flex: 1; margin: 0; overflow-wrap: anywhere; }\r\n.starter-toast button {\r\n display: grid;\r\n place-items: center;\r\n flex-shrink: 0;\r\n width: 2.75rem;\r\n height: 2.75rem;\r\n padding: 0;\r\n border: 0;\r\n border-radius: var(--tl-radius-sm);\r\n background: transparent;\r\n color: inherit;\r\n cursor: pointer;\r\n}\r\n.starter-toast button:hover { background: var(--tl-secondary-hover); }\r\n.starter-toast button:focus-visible { outline: 2px solid var(--tl-focus); }\r\n\r\n.starter-account-link { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: #101112; color: #fff !important; font-weight: 700; font-size: .8rem !important; }\r\n\r\n/* Shared controls use modest corners; hero image silhouettes stay expressive. */\r\n.starter-site { --tl-radius-sm: 4px; --tl-radius-md: 6px; --tl-radius-lg: 8px; }\r\n.starter-site :is(.tl-btn, .tl-input, .tl-select, .tl-textarea, .starter-get-started) { border-radius: 6px; }\r\n.starter-site :is(.starter-example, .tl-card, .starter-card-preview) { border-radius: 8px; }\r\n\r\n/* Compact public header with a solid scroll surface. */\r\n.starter-header { position: sticky; top: 0; z-index: 40; width: 100%; max-width: none; margin: 0; background: rgb(255 255 255 / .96); border-bottom: 1px solid #eff0f3; backdrop-filter: blur(12px); transition: box-shadow .18s, background-color .18s; }\r\n.starter-header .starter-nav { width: calc(100% - 112px); max-width: 1600px; min-height: 76px; margin-inline: auto; }\r\n.starter-header .starter-brand { font-size: 1.65rem; }\r\n.starter-header .starter-brand-mark { width: 32px; height: 32px; }\r\n.starter-header .starter-nav-links { gap: 2rem; }\r\n.starter-header .starter-nav-links a { font-size: .9rem; }\r\n.starter-header .starter-get-started, .starter-header .starter-nav-actions .tl-btn { min-height: 42px; padding: .65rem 1.25rem; font-size: .9rem; }\r\n.starter-header.is-scrolled { background: #fff; border-bottom-color: #e3e6eb; box-shadow: 0 4px 18px rgb(15 23 42 / .07); }\r\n.starter-toolkit-hero { grid-template-columns: .87fr 1.13fr; gap: clamp(2rem, 4vw, 4.5rem); margin-top: 84px; margin-bottom: 76px; min-height: 440px; }\r\n.starter-toolkit-hero h1 { font-size: clamp(2.8rem, 4vw, 4.2rem); line-height: 1.07; letter-spacing: -.055em; }\r\n.starter-toolkit-hero .starter-hero-copy { padding-block: 1rem; }\r\n.starter-toolkit-hero .starter-eyebrow { font-size: .8rem; letter-spacing: .16em; margin-bottom: 30px; }\r\n.starter-toolkit-hero .starter-lead { font-size: clamp(1.05rem, 1.45vw, 1.35rem); max-width: 38rem; line-height: 1.55; margin-top: 27px; }\r\n.starter-toolkit-hero .starter-actions { margin-top: 36px; }\r\n.starter-toolkit-hero .starter-actions .tl-btn { min-height: 52px; padding-inline: 1.65rem; }\r\n.starter-toolkit-features { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }\r\n.starter-toolkit-feature { display: flex; align-items: flex-start; gap: 21px; border: 1px solid #e4e7ec; border-radius: 8px; padding: 24px; background: rgb(255 255 255 / .9); }\r\n.starter-toolkit-feature h2 { margin: 0 0 8px; font-size: 1.3rem; font-weight: 700; letter-spacing: -.035em; }\r\n.starter-toolkit-feature p { margin: 0; font-size: .97rem; color: var(--tl-text-muted); line-height: 1.5; }\r\n.starter-toolkit-feature .starter-feature-icon { flex-basis: 58px; width: 58px; height: 58px; }\r\n.starter-code-editor { min-width: 0; overflow: hidden; border: 1px solid #343638; border-radius: 12px; background: #17191b; box-shadow: 0 16px 50px rgb(15 23 42 / .09); color: #e5e7eb; font-family: Consolas, 'Courier New', monospace; }\r\n.code-titlebar { height: 44px; display: flex; align-items: center; gap: 8px; padding-inline: 18px; background: linear-gradient(#26282a,#202224); border-bottom: 1px solid #2c2e30; }\r\n.code-titlebar i { width: 11px; height: 11px; border-radius: 50%; background: #75787a; }\r\n.code-titlebar > span { font-family: Arial, sans-serif; color: #989a9c; font-size: .7rem; margin-left: auto; letter-spacing: .03em; }\r\n.code-workspace { display: grid; grid-template-columns: 45px 160px minmax(0, 1fr); min-height: 352px; }\r\n.code-activity { display: flex; flex-direction: column; align-items: center; gap: 28px; padding-top: 23px; background: #202224; border-right: 1px solid #292b2d; }\r\n.code-activity svg { width: 21px; height: 21px; fill: none; stroke: #a6abad; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }\r\n.code-activity svg:first-child { stroke: #f2f3f4; }\r\n.code-explorer { padding: 18px 7px; font-size: .8rem; font-family: Arial, sans-serif; background: #191b1d; border-right: 1px solid #2b2d30; }\r\n.code-explorer strong { display: block; margin: 0 5px 15px; font-size: .85rem; font-weight: 500; }\r\n.code-folder,.code-file { min-height: 29px; display: flex; align-items: center; gap: 7px; white-space: nowrap; color: #c7cbce; }\r\n.code-indent { padding-left: 12px; }\r\n.code-indent-2 { padding-left: 24px; }\r\n.code-file small { font-size: .65rem; color: #b9c5cb; }\r\n.code-file.selected { background: #303438; border-radius: 4px; color: #fff; }\r\n.code-chevron { display: inline-block; width: 6px; height: 6px; border-right: 1px solid currentColor; border-bottom: 1px solid currentColor; transform: rotate(45deg); }\r\n.code-main { min-width: 0; }\r\n.code-tabs { display: flex; background: #202224; border-bottom: 1px solid #2c2e30; }\r\n.code-tabs button { display: inline-flex; align-items: center; gap: 9px; min-height: 38px; padding: 0 14px; border: 0; border-right: 1px solid #2b2d30; background: transparent; color: #a8adb1; font: 12px Arial, sans-serif; white-space: nowrap; cursor: pointer; }\r\n.code-tabs button[aria-selected=\"true\"] { color: #f1f3f5; background: #17191b; box-shadow: inset 0 1px #79878f; }\r\n.code-tabs button small { color: #a4bdcb; font-size: 10px; }\r\n.code-tabs button > span:last-child { margin-left: 7px; color: #c5c9cd; font-size: 15px; }\r\n.code-content { overflow-x: auto; padding: 18px 10px 24px 12px; }\r\n.code-content pre { margin: 0; min-height: 270px; font: clamp(12px, 1vw, 15px)/1.65 Consolas, 'Courier New', monospace; tab-size: 2; }\r\n.code-content code { font: inherit; white-space: pre; }\r\n.code-number { display: inline-block; width: 21px; margin-right: 18px; text-align: right; color: #757d82; user-select: none; }\r\n.code-keyword { color: #c995dc; }\r\n.code-string { color: #e5ac72; }\r\n.code-type { color: #8acbdc; }\r\n.code-function { color: #d9d2aa; }\r\n@media (max-width: 1100px) {\r\n .starter-toolkit-hero { gap: 28px; grid-template-columns: .9fr 1.1fr; }\r\n .code-workspace { grid-template-columns: 38px 125px minmax(0,1fr); }\r\n .code-explorer { font-size: .7rem; padding-inline: 4px; }\r\n .code-explorer strong { font-size: .75rem; }\r\n .code-indent-2 { padding-left: 13px; }\r\n .code-tabs button { font-size: 11px; padding-inline: 10px; }\r\n .starter-toolkit-feature { padding: 20px; gap: 14px; }\r\n .starter-toolkit-feature h2 { font-size: 1.1rem; }\r\n .starter-toolkit-feature p { font-size: .875rem; }\r\n}\r\n@media (max-width: 850px) {\r\n .starter-header .starter-nav { width: calc(100% - 48px); }\r\n .starter-toolkit-hero { grid-template-columns: 1fr; margin-top: 40px; margin-bottom: 36px; gap: 30px; }\r\n .starter-toolkit-hero h1 { max-width: 40rem; font-size: clamp(2.7rem, 6.3vw, 4rem); }\r\n .starter-toolkit-features { grid-template-columns: 1fr; gap: 16px; }\r\n .code-workspace { grid-template-columns: 42px 150px minmax(0,1fr); }\r\n .code-content pre { font-size: 12px; }\r\n}\r\n@media (max-width: 767px) {\r\n .starter-header .starter-nav { min-height: 68px; padding-block: 10px; }\r\n .starter-header .starter-brand { font-size: 1.5rem; }\r\n .starter-header .starter-brand-mark { width: 28px; height: 28px; }\r\n .starter-header .starter-nav-links { gap: .25rem; }\r\n .starter-toolkit-hero h1 { font-size: clamp(2.55rem, 9.5vw, 3.8rem); }\r\n .starter-toolkit-hero .starter-eyebrow { font-size: .65rem; margin-bottom: 22px; }\r\n .starter-toolkit-hero .starter-lead { font-size: 1.08rem; margin-top: 20px; }\r\n .starter-toolkit-hero .starter-actions { margin-top: 26px; }\r\n .starter-toolkit-hero .starter-actions .tl-btn { min-height: 46px; padding-inline: 1.2rem; }\r\n .code-workspace { grid-template-columns: 30px minmax(0,1fr); min-height: 318px; }\r\n .code-explorer { display: none; }\r\n .code-activity { gap: 25px; }\r\n .code-activity svg { width: 17px; height: 17px; }\r\n .code-tabs button { padding-inline: 9px; min-height: 36px; }\r\n .code-content { padding: 14px 8px; }\r\n .code-content pre { font-size: 10.5px; min-height: 250px; }\r\n .code-number { width: 15px; margin-right: 11px; }\r\n .code-titlebar { height: 35px; padding-inline: 12px; }\r\n}\r\n\r\n/* Flat authentication layout, shared by login, registration and recovery. */\r\n.starter-site:has(.auth-scene) > .starter-header { display: none; }\r\n.starter-site:has(.auth-scene)::before { display: none; }\r\n.starter-site:has(.auth-scene) > .starter-main { width: 100%; max-width: none; padding: 0; }\r\n.auth-page { min-height: 100dvh; background: #fff; color: #111318; font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Arial, sans-serif; }\r\n.auth-scene { display: grid; grid-template-columns: 1fr 1.05fr; width: 100%; min-height: 100dvh; position: relative; border: 0; border-radius: 0; background: #fff; box-shadow: none; overflow: hidden; }\r\n.auth-scene-content { min-width: 0; padding: 42px clamp(28px,5.4vw,90px) 38px; }\r\n.auth-topbar { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: clamp(44px,6vh,66px); }\r\n.auth-brand { font-size: 1.9rem; letter-spacing: -.05em; margin: 0; flex-shrink: 0; }\r\n.auth-brand .starter-brand-mark { width: 36px; height: 36px; }\r\n.auth-scene-form { width: 100%; max-width: 540px; }\r\n.auth-scene .auth-panel { width: 100%; max-width: none; margin: 0; padding: 0; gap: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }\r\n.auth-scene .auth-heading { text-align: left; }\r\n.auth-scene .auth-heading h1 { margin: 0; font-size: clamp(2rem,3.1vw,3.2rem); font-weight: 800; line-height: 1.12; letter-spacing: -.04em; }\r\n.auth-scene .auth-heading p { margin: 12px 0 0; color: #4b5563; font-size: 1.1rem; font-weight: 500; line-height: 1.5; }\r\n.auth-scene .auth-panel form { margin-top: 32px; gap: 16px; }\r\n.auth-scene .auth-fields { gap: 16px; }\r\n.auth-scene .tl-form-group { gap: 0; }\r\n/* Keep real labels for screen readers while matching the reference's icon fields. */\r\n.auth-scene .tl-form-group > .tl-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }\r\n.auth-scene .tl-input { min-height: 52px; padding: 13px 16px; font-size: 1rem; font-weight: 500; color: #171b22; font-family: inherit; border: 1px solid #d9dde5; border-radius: 8px; background: #fafbfc; box-shadow: none; }\r\n.starter-site .auth-scene .tl-input::placeholder { color: #4b5563; font-weight: 500; opacity: 1; }\r\n.auth-input-wrap { position: relative; }\r\n.auth-scene .auth-input-wrap .tl-input { padding-left: 50px; }\r\n.auth-scene .auth-password .tl-input { padding-right: 48px; }\r\n.auth-input-icon { position: absolute; left: 17px; top: 50%; transform: translateY(-50%); pointer-events: none; color: #14171b; }\r\n.auth-scene .auth-password-toggle { width: 46px; color: #525b68; }\r\n.auth-scene .auth-submit { display: flex; justify-content: center; align-items: center; gap: 16px; min-height: 52px; margin-top: 6px; border-radius: 8px; font-size: 1.05rem; font-weight: 700; box-shadow: none; }\r\n.auth-submit > span { font-size: 1.5rem; font-weight: 400; line-height: 1; }\r\n.auth-form-options { display: flex; justify-content: flex-end; align-items: center; min-height: 28px; font-size: .9rem; }\r\n.auth-form-options a { color: #101112; font-weight: 650; text-decoration: underline; text-underline-offset: 3px; }\r\n.auth-alternate { margin-top: 26px; }\r\n.auth-alternate-label { display: flex; align-items: center; gap: 20px; margin: 0 0 18px; color: #4b5563; font-size: .95rem; font-weight: 500; text-align: center; white-space: nowrap; }\r\n.auth-alternate-label::before, .auth-alternate-label::after { content: ''; flex: 1; border-top: 1px solid #dfe3eb; }\r\n.auth-scene .auth-alternate-link { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 50px; width: 100%; border: 1px solid #bfc6d1; border-radius: 8px; background: #fff; color: #101112; font-size: 1rem; font-weight: 700; text-decoration: none; box-shadow: none; }\r\n.auth-scene .auth-alternate-link:hover { background: #f6f7f9; border-color: #929baa; }\r\n.auth-scene a:focus-visible { outline: 2px solid #171b22; outline-offset: 4px; }\r\n.auth-scene-art { min-width: 0; margin: 0; position: relative; overflow: hidden; background: linear-gradient(125deg,#f7f8fa 5%,#e9ebf0 100%); }\r\n.auth-scene-art > svg { position: absolute; }\r\n.auth-scene-art img { position: absolute; width: 100%; height: 110%; bottom: 0; object-fit: cover; object-position: 5% center; }\r\n.auth-scene-register .auth-fields { gap: 14px; }\r\n.auth-reset-form .tl-text-muted { font-size: .85rem; line-height: 1.6; margin-top: 10px; }\r\n.auth-scene-register .auth-scene-form, .auth-scene-form:has(.auth-reset-form) { max-width: 440px; }\r\n.auth-scene-register .auth-heading h1, .auth-reset-form .auth-heading h1 { font-size: clamp(1.8rem,2.5vw,2.6rem); }\r\n.auth-scene-register .auth-heading p, .auth-reset-form .auth-heading p { font-size: 1rem; }\r\n.auth-scene-register .auth-panel form, .auth-reset-form form { margin-top: 26px; }\r\n.auth-scene-register .tl-input, .auth-reset-form .tl-input { min-height: 46px; padding-block: 10px; }\r\n.auth-scene-register .auth-submit, .auth-scene-register .auth-alternate-link, .auth-reset-form .auth-submit, .auth-reset-form .auth-alternate-link { min-height: 46px; }\r\n.auth-scene-register .auth-alternate, .auth-reset-form .auth-alternate { margin-top: 22px; }\r\n@media (max-width: 1200px) {\r\n .auth-scene { grid-template-columns: 1.1fr .9fr; }\r\n .auth-scene-content { padding-inline: 36px; }\r\n .auth-topbar { gap: 14px; flex-wrap: wrap; margin-bottom: 40px; }\r\n .auth-brand { font-size: 1.7rem; }\r\n}\r\n@media (min-width: 768px) and (max-height: 820px) {\r\n .auth-scene-content { padding-block: 28px; }\r\n .auth-topbar { margin-bottom: 34px; }\r\n .auth-scene .auth-heading h1 { font-size: 2.5rem; }\r\n .auth-scene .auth-panel form { margin-top: 24px; }\r\n .auth-scene .tl-input { min-height: 46px; padding-block: 10px; }\r\n .auth-scene .auth-submit, .auth-scene .auth-alternate-link { min-height: 46px; }\r\n .auth-alternate { margin-top: 22px; }\r\n}\r\n@media (max-width: 767px) {\r\n .auth-scene { grid-template-columns: minmax(0,1fr); }\r\n .auth-scene-content { padding: 28px 24px 36px; }\r\n .auth-topbar { margin-bottom: 40px; gap: 18px; }\r\n .auth-brand { font-size: 1.65rem; }\r\n .auth-brand .starter-brand-mark { width: 32px; height: 32px; }\r\n .auth-scene .auth-heading h1 { font-size: clamp(1.8rem,7vw,2.2rem); }\r\n .auth-scene .auth-heading p { font-size: .95rem; }\r\n .auth-scene .auth-panel form { margin-top: 26px; }\r\n .auth-scene .tl-input { min-height: 48px; font-size: .95rem; padding-block: 11px; }\r\n .auth-scene .auth-submit, .auth-scene .auth-alternate-link { min-height: 48px; }\r\n .auth-alternate-label { font-size: .875rem; gap: 14px; }\r\n .auth-scene-art { display: none; }\r\n}\r\n\r\n/* About uses the same diagonal silhouette as the signup image. */\r\n.starter-hero-about .starter-about-art { position: relative; border-radius: 0; }\r\n.starter-about-art > svg { position: absolute; }\r\n.starter-about-art img { object-position: 5% center; }\r\n\r\n.auth-footer-row .auth-back-link { display: inline-flex; align-items: center; justify-content: center; gap: 9px; min-height: 40px; padding: 8px 16px; border: 1px solid #dfe3e8; border-radius: 6px; background: #f7f8fa; text-decoration: none; }\r\n.auth-footer-row .auth-back-link:hover { background: #eef0f3; border-color: #c7cdd5; }\r\n\r\n/* Account navigation and personal dashboard. */\r\n.account-icon { flex: 0 0 21px; width: 21px; height: 21px; }\r\n.starter-account { position: relative; flex-shrink: 0; }\r\n.starter-account-toggle { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 6px; border: 0; background: transparent; color: #171b22; cursor: pointer; border-radius: 6px; }\r\n.starter-account-avatar { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: #101112; color: #fff; font-size: .85rem; font-weight: 700; }\r\n.starter-account-toggle:focus-visible, .starter-account-popover :is(a,button):focus-visible { outline: 2px solid #343b49; outline-offset: 3px; }\r\n.starter-account-popover { position: absolute; right: 0; top: calc(100% + 15px); width: 280px; max-width: calc(100vw - 48px); padding: 7px; border: 1px solid #e2e5eb; border-radius: 8px; box-shadow: 0 12px 32px #1720331a; background: #fff; }\r\n.starter-account-summary { display: grid; gap: 5px; padding: 13px 12px 17px; margin-bottom: 5px; border-bottom: 1px solid #e7e9ed; }\r\n.starter-account-summary strong { font-size: .9rem; }\r\n.starter-account-summary span { font-size: .8rem; color: #626b79; overflow-wrap: anywhere; }\r\n.starter-account-popover :is(a,button) { display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px; border: 0; border-radius: 5px; color: #1a1d23; background: transparent; font: 500 .9rem Arial, sans-serif; text-decoration: none; text-align: left; cursor: pointer; }\r\n.starter-account-popover :is(a,button):hover, .starter-account-popover a.active { background: #f3f4f6; }\r\n.starter-account-popover button:disabled { opacity: .6; cursor: wait; }\r\n.starter-account-divider { height: 1px; background: #e7e9ed; margin: 5px; }\r\n.account-dashboard { padding-block: 36px 12px; }\r\n.account-page-heading { margin-bottom: 28px; }\r\n.account-page-heading h1 { margin: 0 0 8px; font-size: clamp(1.8rem, 2.6vw, 2.6rem); line-height: 1.15; font-weight: 750; letter-spacing: -.045em; }\r\n.account-page-heading p { margin: 0; color: #616977; font-size: 1rem; line-height: 1.5; }\r\n.account-grid { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(0,1fr); gap: 24px; }\r\n.account-column { display: flex; flex-direction: column; gap: 22px; min-width: 0; }\r\n.account-card { min-width: 0; padding: 24px; border: 1px solid #e4e7ec; border-radius: 8px; background: #fff; box-shadow: 0 4px 14px #1b243107; scroll-margin-top: 100px; }\r\n.account-card h2 { margin: 0 0 20px; font-size: 1.3rem; font-weight: 700; letter-spacing: -.03em; }\r\n.account-identity { display: flex; align-items: center; flex-wrap: wrap; gap: 22px; min-height: 162px; }\r\n.account-avatar { display: grid; place-items: center; flex: 0 0 88px; width: 88px; height: 88px; border-radius: 50%; background: #101112; color: #fff; font-size: 2.1rem; font-weight: 700; letter-spacing: -.04em; }\r\n.account-identity-copy { flex: 1 1 180px; min-width: 0; }\r\n.account-identity-copy h2 { margin-bottom: 7px; font-size: 1.8rem; overflow-wrap: anywhere; }\r\n.account-identity-copy p { margin: 0 0 13px; color: #616977; font-size: .95rem; overflow-wrap: anywhere; }\r\n.account-role { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 5px; background: #f0f1f3; color: #303642; font-size: .8rem; font-weight: 500; }\r\n.account-dashboard .tl-btn { min-height: 40px; gap: 8px; padding: 8px 14px; font-size: .85rem; }\r\n.account-edit { margin-left: auto; }\r\n.account-edit-form { flex-basis: 100%; display: grid; gap: 16px; padding-top: 20px; border-top: 1px solid #e7e9ed; }\r\n.account-edit-form .tl-input { min-height: 42px; }\r\n.account-form-actions { display: flex; justify-content: flex-end; gap: 10px; }\r\n.account-details { margin: 0; display: grid; gap: 20px; }\r\n.account-details > div { display: grid; grid-template-columns: minmax(145px,.8fr) minmax(0,1.2fr); align-items: start; gap: 16px; }\r\n.account-details dt { display: flex; align-items: center; gap: 13px; font-weight: 600; font-size: .9rem; }\r\n.account-details dd { margin: 0; font-size: .9rem; color: #4f5764; overflow-wrap: anywhere; line-height: 1.5; }\r\n.account-security-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; min-height: 46px; font-size: .9rem; }\r\n.account-security-row > span:first-child { display: inline-flex; align-items: center; gap: 13px; font-weight: 600; }\r\n.account-security-row button { display: inline-flex; align-items: center; gap: 8px; padding: 8px 0 8px 8px; border: 0; background: transparent; color: #4e5868; font: inherit; cursor: pointer; }\r\n.account-security-row button:hover { color: #090a0b; text-decoration: underline; }\r\n.account-reset-note { margin-block: 8px 16px; padding: 14px; border-radius: 6px; background: #f6f7f9; }\r\n.account-reset-note p { margin: 0 0 12px; font-size: .85rem; line-height: 1.5; color: #616977; }\r\n.account-quick-actions { display: grid; gap: 6px; }\r\n.account-quick-actions :is(a,button) { display: flex; align-items: center; gap: 14px; width: 100%; padding: 5px 0; border: 0; border-radius: 5px; background: transparent; color: #171b22; text-decoration: none; text-align: left; font: .95rem Arial, sans-serif; cursor: pointer; }\r\n.account-quick-actions :is(a,button):hover { background: #f6f7f9; }\r\n.account-quick-actions :is(a,button) > .account-icon:last-child { margin-left: auto; width: 17px; }\r\n.account-icon-tile { display: grid; place-items: center; flex: 0 0 38px; width: 38px; height: 38px; border-radius: 7px; background: #f2f3f5; }\r\n.account-activity { display: grid; gap: 18px; }\r\n.account-activity > div { display: flex; align-items: center; gap: 14px; }\r\n.account-activity p { margin: 0; display: grid; gap: 5px; font-size: .9rem; }\r\n.account-activity p span { color: #616977; font-size: .8rem; }\r\n.account-preference { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }\r\n.account-preference label { display: flex; align-items: center; gap: 12px; font-size: .9rem; font-weight: 600; }\r\n.account-preference .tl-select { width: auto; min-height: 38px; padding-block: 6px; font-size: .85rem; }\r\n.account-preference-note { margin: 18px 0 0; color: #616977; font-size: .8rem; line-height: 1.5; }\r\n@media (max-width: 1100px) {\r\n .account-grid { grid-template-columns: minmax(0,1.2fr) minmax(0,1fr); gap: 18px; }\r\n .account-card { padding: 20px; }\r\n .account-avatar { flex-basis: 68px; width: 68px; height: 68px; font-size: 1.6rem; }\r\n .account-identity-copy h2 { font-size: 1.5rem; }\r\n}\r\n@media (max-width: 767px) {\r\n .starter-nav .starter-brand { margin-right: auto; }\r\n .starter-nav .starter-account { order: 1; }\r\n .starter-nav .starter-menu-toggle { order: 2; }\r\n .starter-nav .starter-nav-panel { order: 3; }\r\n .starter-header .starter-nav { column-gap: 14px; }\r\n .starter-account-toggle { gap: 6px; }\r\n .starter-account-avatar { width: 36px; height: 36px; }\r\n .starter-account-popover { right: -44px; }\r\n .account-dashboard { padding-top: 28px; }\r\n .account-grid { grid-template-columns: minmax(0,1fr); }\r\n .account-card { padding: 20px; }\r\n .account-identity { display: grid; grid-template-columns: 68px minmax(0,1fr); gap: 16px; }\r\n .account-identity-copy h2 { font-size: 1.4rem; }\r\n .account-identity-copy p { font-size: .8rem; }\r\n .account-edit { grid-column: 1 / -1; justify-self: start; margin-left: 0; }\r\n .account-edit-form { grid-column: 1 / -1; }\r\n .account-details > div { grid-template-columns: minmax(0,1fr); gap: 7px; }\r\n .account-details dd { padding-left: 34px; }\r\n .account-card h2 { font-size: 1.2rem; }\r\n}\r\n\r\n/* The user overview is separate from profile and account settings. */\r\n.user-dashboard-summary { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 22px; margin-bottom: 26px; }\r\n.user-summary-card { display: flex; align-items: center; gap: 18px; padding-block: 25px; }\r\n.user-summary-card .account-icon-tile { flex-basis: 48px; width: 48px; height: 48px; }\r\n.user-summary-card p { margin: 0 0 8px; color: #616977; font-size: .85rem; }\r\n.user-summary-card h2 { display: flex; align-items: center; gap: 9px; margin: 0; font-size: 1.25rem; line-height: 1.4; }\r\n.user-session-dot { width: 8px; height: 8px; border-radius: 50%; background: #18844b; }\r\n.user-dashboard-grid { display: grid; grid-template-columns: minmax(0,1.6fr) minmax(0,1fr); gap: 24px; }\r\n.user-card-heading h2 { margin-bottom: 8px; }\r\n.user-card-heading > p { margin: 0 0 24px; color: #616977; font-size: .9rem; }\r\n.user-workspace-links { display: grid; }\r\n.user-workspace-links > a { display: flex; align-items: center; gap: 17px; padding: 20px 8px; color: #171b22; text-decoration: none; border-top: 1px solid #eceef2; border-radius: 5px; }\r\n.user-workspace-links > a:hover { background: #f7f8fa; }\r\n.user-workspace-links > a > div { flex: 1; min-width: 0; }\r\n.user-workspace-links h3 { margin: 0 0 7px; font-size: 1rem; letter-spacing: -.02em; }\r\n.user-workspace-links p { margin: 0; color: #616977; font-size: .875rem; line-height: 1.5; }\r\n.user-getting-started { display: flex; gap: 18px; align-items: flex-start; }\r\n.user-getting-started h2 { margin-bottom: 8px; font-size: 1.1rem; }\r\n.user-getting-started p { margin: 0 0 17px; color: #616977; font-size: .875rem; line-height: 1.6; }\r\n.user-getting-started a { display: inline-flex; align-items: center; gap: 8px; color: #171b22; font-size: .85rem; font-weight: 600; text-underline-offset: 4px; }\r\n.user-account-snapshot .account-avatar { width: 64px; height: 64px; font-size: 1.5rem; margin: 24px 0 18px; }\r\n.user-account-snapshot h3 { margin: 0 0 8px; font-size: 1.3rem; letter-spacing: -.03em; overflow-wrap: anywhere; }\r\n.user-account-snapshot > p { margin: 0 0 16px; font-size: .875rem; color: #616977; overflow-wrap: anywhere; }\r\n.user-account-snapshot .tl-btn { display: flex; width: fit-content; margin-top: 24px; text-decoration: none; }\r\n.user-dashboard-note { display: flex; align-items: flex-start; gap: 16px; background: #fafbfc; box-shadow: none; }\r\n.user-dashboard-note h2 { margin-bottom: 8px; font-size: 1rem; }\r\n.user-dashboard-note p { margin: 0; font-size: .85rem; color: #616977; line-height: 1.6; }\r\n@media (max-width: 900px) {\r\n .user-dashboard-summary { gap: 14px; }\r\n .user-summary-card { align-items: flex-start; flex-direction: column; gap: 14px; }\r\n .user-dashboard-grid { grid-template-columns: minmax(0,1.3fr) minmax(0,1fr); gap: 18px; }\r\n}\r\n@media (max-width: 767px) {\r\n .user-dashboard-summary, .user-dashboard-grid { grid-template-columns: minmax(0,1fr); }\r\n .user-summary-card { flex-direction: row; align-items: center; }\r\n .user-workspace-links > a { gap: 12px; padding-inline: 0; }\r\n .user-workspace-links h3 { font-size: .95rem; }\r\n .user-workspace-links p { font-size: .8rem; }\r\n .user-workspace-links > a > .account-icon:last-child { width: 16px; flex-basis: 16px; }\r\n}\r\n\r\n.starter-account-toggle > svg { position: absolute; right: 2px; bottom: 3px; display: block; box-sizing: border-box; width: 19px; height: 19px; padding: 2px; border: 2px solid #fff; border-radius: 50%; background: #e8ebef; color: #171b22; transition: transform .15s ease; }\r\n.starter-account-toggle[aria-expanded=\"true\"] > svg { transform: rotate(180deg); }\r\n@media (prefers-reduced-motion: reduce) { .starter-account-toggle > svg { transition: none; } }\r\n\r\n/* Shared Forbidden and Not Found pages: flat surfaces and clear recovery links. */\r\n.site-error { display: grid; grid-template-columns: 220px minmax(0,1fr); align-items: center; gap: 48px; width: 100%; max-width: 920px; min-height: min(620px,calc(100dvh - 150px)); margin: 0 auto; padding: 48px 20px; }\r\n.site-error-visual { display: grid; place-items: center; position: relative; width: 220px; height: 240px; border: 1px solid #e3e6ec; border-radius: 12px; background: linear-gradient(135deg,#fafbfc,#f0f2f5); overflow: hidden; }\r\n.site-error-code { color: #d4d9e1; font: 800 80px/1 Arial,sans-serif; letter-spacing: -.07em; transform: translateY(-20px); }\r\n.site-error-symbol { position: absolute; bottom: 28px; right: 26px; display: grid; place-items: center; width: 72px; height: 72px; border: 1px solid #dce1e8; border-radius: 10px; background: #fff; color: #202630; }\r\n.site-error-eyebrow { margin: 0 0 12px; color: #616977; font-size: .72rem; font-weight: 650; letter-spacing: .13em; text-transform: uppercase; }\r\n.site-error-content h1 { margin: 0 0 16px; color: #111418; font-size: clamp(2rem,3.3vw,2.8rem); font-weight: 750; letter-spacing: -.045em; line-height: 1.15; }\r\n.site-error-description { max-width: 510px; margin: 0; color: #555f6e; font-size: 1rem; line-height: 1.75; }\r\n.site-error-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 28px; }\r\n.site-error-actions .tl-btn { display: inline-flex; align-items: center; justify-content: center; gap: 16px; min-height: 42px; padding: 10px 16px; font-size: .875rem; font-weight: 600; border-radius: 6px; text-decoration: none; }\r\n.site-error-actions .tl-btn-primary { background: #101112; border-color: #101112; color: #fff; }\r\n.site-error-actions .tl-btn-primary:hover { background: #292c30; }\r\n.site-error-actions .tl-btn-outline { color: #222831; border-color: #d8dde5; background: #fff; }\r\n.site-error-actions .tl-btn-outline:hover { background: #f5f6f8; }\r\n.site-error-note { margin: 28px 0 0; padding-top: 18px; border-top: 1px solid #e7eaf0; color: #737c89; font-size: .8rem; line-height: 1.65; }\r\n@media (max-width: 767px) {\r\n .site-error { grid-template-columns: minmax(0,1fr); gap: 28px; min-height: 0; max-width: 480px; padding: 32px 0 48px; }\r\n .site-error-visual { width: 100%; height: 160px; }\r\n .site-error-code { font-size: 76px; transform: translate(-38px,0); }\r\n .site-error-symbol { width: 64px; height: 64px; right: calc(50% - 104px); bottom: 28px; }\r\n .site-error-content h1 { font-size: 2rem; }\r\n .site-error-description { font-size: .95rem; }\r\n .site-error-actions { gap: 10px; margin-top: 24px; }\r\n .site-error-actions .tl-btn { flex: 1 1 140px; padding-inline: 12px; }\r\n}\r\n\r\n/* Compact notifications share the same restrained sizing as app controls. */\r\n.starter-toaster { z-index: 1000; width: min(21rem, calc(100% - 2rem)); }\r\n.starter-toaster[data-compact] { --toast-default-padding: .6rem .55rem .6rem .8rem; }\r\n.starter-toast { gap: .65rem; border-left-width: 1px; border-radius: 6px; box-shadow: 0 3px 12px #10182812; }\r\n.starter-toast-icon { flex-shrink: 0; }\r\n.starter-toast p { color: var(--tl-text); font-size: .875rem; line-height: 1.4; font-weight: 500; }\r\n.starter-toast button { width: 1.75rem; height: 1.75rem; min-height: 0; color: var(--tl-text-muted); }\r\n\r\n.account-identity > .profile-photo-settings{grid-column:1/-1;width:100%}\r\n",
8
+ "src/App.tsx": "import { SessionSkeleton } from './components/session-skeleton'\r\nimport { BrowserRouter, useRoutes } from 'react-router'\r\nimport { routes } from './routes'\r\nimport { AuthBootstrap } from './app/auth'\r\nimport './App.css'\r\n\r\nfunction AppRoutes() {\r\n return useRoutes(routes)\r\n}\r\n\r\nexport default function App() {\r\n return (\r\n <BrowserRouter>\r\n <AuthBootstrap fallback={<SessionSkeleton />}>\r\n <AppRoutes />\r\n </AuthBootstrap>\r\n </BrowserRouter>\r\n )\r\n}\r\n",
9
+ "src/components/account-icon.tsx": "type AccountIconName = 'user' | 'dashboard' | 'shield' | 'logout' | 'edit' | 'mail' | 'calendar' | 'lock' | 'monitor' | 'arrow' | 'globe'\n\nexport function AccountIcon({ name }: { name: AccountIconName }) {\n const paths: Record<AccountIconName, string> = {\n user: 'M20 21v-2a8 8 0 0 0-16 0v2M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z',\n dashboard: 'M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z',\n shield: 'm12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6z',\n logout: 'M9 4H4v16h5m5-13 5 5-5 5m-6-5h11',\n edit: 'm16 3 5 5-12 12-6 1 1-6Zm-2 2 5 5',\n mail: 'M3 5h18v14H3zm0 1 9 7 9-7',\n calendar: 'M4 5h16v16H4zM8 3v4m8-4v4M4 10h16',\n lock: 'M5 10h14v11H5zm3 0V7a4 4 0 0 1 8 0v3m-4 5v2',\n monitor: 'M3 4h18v13H3zm5 17h8m-4-4v4',\n arrow: 'm9 5 7 7-7 7',\n globe: 'M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM3 12h18m-9-9c5 5 5 13 0 18-5-5-5-13 0-18Z',\n }\n return <svg className=\"account-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d={paths[name]} /></svg>\n}\n",
10
+ "src/components/auth-form.tsx": "import { useState, type FormEvent } from 'react'\r\nimport { Link, useLocation } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { authErrorMessage } from '../lib/auth-feedback'\r\nimport { AuthInputIcon } from './auth-input-icon'\r\nimport { PasswordField } from './password-field'\r\nimport { toast } from '../lib/toast'\r\n\r\nexport function AuthForm({ mode, successMessage }: { mode: 'login' | 'register'; successMessage?: string }) {\r\n const registering = mode === 'register'\r\n const { login, register, loginStatus, registerStatus } = useAuth()\r\n const location = useLocation()\r\n const [error, setError] = useState('')\r\n const pending = loginStatus.isLoading || registerStatus.isLoading\r\n\r\n async function submit(event: FormEvent<HTMLFormElement>) {\r\n event.preventDefault()\r\n if (pending) return\r\n const data = new FormData(event.currentTarget)\r\n const email = String(data.get('email') ?? '').trim()\r\n const password = String(data.get('password') ?? '')\r\n setError('')\r\n if (registering && password !== data.get('confirmPassword')) {\r\n setError('Passwords do not match.')\r\n return\r\n }\r\n try {\r\n if (registering) await register({ name: String(data.get('name') ?? '').trim(), email, password })\r\n else await login({ email, password })\r\n toast.success(successMessage?.trim() || (registering ? 'Account created.' : 'Logged in.'))\r\n // GuestOnly redirects to the intended page when the session changes.\r\n } catch (failure) { setError(authErrorMessage(failure)) }\r\n }\r\n\r\n return (\r\n <section className=\"auth-panel auth-account-form tl-stack\" aria-labelledby=\"auth-title\">\r\n <header className=\"auth-heading\">\r\n <h1 id=\"auth-title\" className=\"tl-heading-2\">{registering ? 'Create your account' : 'Welcome back'}</h1>\r\n <p>{registering ? 'Set up your Tilcayo account' : 'Sign in to your Tilcayo account'}</p>\r\n </header>\r\n <form onSubmit={submit} aria-busy={pending} className=\"tl-stack\">\r\n {error && <p className=\"tl-alert tl-alert-danger\" role=\"alert\">{error}</p>}\r\n <fieldset disabled={pending} className=\"auth-fields tl-stack\">\r\n {registering && <div className=\"tl-form-group\">\r\n <label className=\"tl-label\" htmlFor=\"name\">Full name</label>\r\n <div className=\"auth-input-wrap\"><AuthInputIcon kind=\"name\" /><input className=\"tl-input\" id=\"name\" name=\"name\" autoComplete=\"name\" placeholder=\"Your full name\" required minLength={2} maxLength={100} /></div>\r\n </div>}\r\n <div className=\"tl-form-group\">\r\n <label className=\"tl-label\" htmlFor=\"email\">Email</label>\r\n <div className=\"auth-input-wrap\"><AuthInputIcon kind=\"email\" /><input className=\"tl-input\" id=\"email\" name=\"email\" type=\"email\" autoComplete=\"email\" placeholder=\"you@example.com\" required maxLength={254} /></div>\r\n </div>\r\n <PasswordField name=\"password\" label=\"Password\" autoComplete={registering ? 'new-password' : 'current-password'} placeholder={registering ? 'Password (at least 8 characters)' : 'Enter your password'} />\r\n {registering && <PasswordField name=\"confirmPassword\" label=\"Confirm password\" autoComplete=\"new-password\" placeholder=\"Confirm password\" />}\r\n {!registering && <div className=\"auth-form-options\"><Link to=\"/forgot-password\">Forgot password?</Link></div>}\r\n <button className=\"tl-btn tl-btn-primary auth-submit\" type=\"submit\">\r\n {pending ? 'Please wait...' : registering ? 'Create account' : 'Log in'}{!pending && <span aria-hidden=\"true\">&#8594;</span>}\r\n </button>\r\n </fieldset>\r\n </form>\r\n <div className=\"auth-alternate\">\r\n <p className=\"auth-alternate-label\">{registering ? 'Already have an account?' : 'New to Tilcayo?'}</p>\r\n <Link className=\"tl-btn tl-btn-outline auth-alternate-link\" to={registering ? '/login' : '/register'} state={location.state}>{registering ? 'Log in' : 'Create an account'}</Link>\r\n </div>\r\n </section>\r\n )\r\n}\r\n",
11
+ "src/components/auth-input-icon.tsx": "export function AuthInputIcon({ kind }: { kind: 'email' | 'name' | 'lock' }) {\r\n const path = kind === 'email' ? 'M3 5h18v14H3Zm0 0 9 8 9-8' : kind === 'name' ? 'M20 21v-2a8 8 0 0 0-16 0v2M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z' : 'M5 10h14v11H5ZM8 10V6a4 4 0 0 1 8 0v4M12 14v3'\r\n return <svg className=\"auth-input-icon\" width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d={path} /></svg>\r\n}\r\n",
12
+ "src/components/code-preview.tsx": "import { useId, useState } from 'react'\r\n\r\nconst examples = {\r\n route: [\r\n \"import { defineRoutes } from '@tilcayo/core'\",\r\n \"import { z } from 'zod'\",\r\n '',\r\n 'const query = z.object({',\r\n \" name: z.string().default('Tilcayo'),\",\r\n '})',\r\n '',\r\n 'export default defineRoutes(router => {',\r\n \" router.get('/api/hello', ctx =>\",\r\n ' ctx.response.success({',\r\n ' message: `Hello, ${ctx.query.name}!`,',\r\n ' }), { validate: { query } })',\r\n '})',\r\n ],\r\n types: [\r\n 'export interface GreetingQuery {',\r\n ' name?: string',\r\n '}',\r\n '',\r\n 'export interface GreetingResponse {',\r\n ' success: true',\r\n ' message: string',\r\n ' data: {',\r\n ' message: string',\r\n ' }',\r\n '}',\r\n ],\r\n}\r\nfunction Highlight({ line }: { line: string }) {\r\n return <>{line.split(/('[^']*'|`[^`]*`|\\b(?:import|from|const|export|default|return|interface|type|string|number|true)\\b|\\b(?:defineRoutes|object|success)\\b)/g).map((token, index) => {\r\n const color = /^['`]/.test(token) ? 'string' : /^(import|from|const|export|default|return|interface|type)$/.test(token) ? 'keyword' : /^(string|number|true)$/.test(token) ? 'type' : /^(defineRoutes|object|success)$/.test(token) ? 'function' : undefined\r\n return <span key={index} className={color ? 'code-' + color : undefined}>{token}</span>\r\n })}</>\r\n}\r\nexport function CodePreview() {\r\n const [tab, setTab] = useState<'route' | 'types'>('route')\r\n const id = useId()\r\n return <section className=\"starter-code-editor\" aria-label=\"Tilcayo TypeScript code preview\">\r\n <div className=\"code-titlebar\" aria-hidden=\"true\"><i /><i /><i /><span>tilcayo-app</span></div>\r\n <div className=\"code-workspace\">\r\n <div className=\"code-activity\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 2h9l4 4v16H7ZM3 6v13m10-17v6h7\" /></svg>\r\n <svg viewBox=\"0 0 24 24\"><circle cx=\"10\" cy=\"10\" r=\"7\" /><path d=\"m15 15 6 6\" /></svg>\r\n <svg viewBox=\"0 0 24 24\"><circle cx=\"6\" cy=\"4\" r=\"2\" /><circle cx=\"6\" cy=\"20\" r=\"2\" /><circle cx=\"18\" cy=\"6\" r=\"2\" /><path d=\"M6 6v12m0-6c8 0 12-1 12-4\" /></svg>\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M3 3h7v7H3Zm11 0h7v7h-7ZM3 14h7v7H3Zm11 0h7v7h-7Z\" /></svg>\r\n </div>\r\n <div className=\"code-explorer\" aria-hidden=\"true\">\r\n <strong><span>{'\\u2304'}</span> tilcayo-app</strong>\r\n <div className=\"code-folder\"><span className=\"code-chevron\" /> <span>{'\\u25b1'}</span> src</div>\r\n <div className=\"code-folder code-indent\"><span className=\"code-chevron\" /> <span>{'\\u25b1'}</span> routes</div>\r\n <div className={'code-file code-indent-2' + (tab === 'route' ? ' selected' : '')}><small>TS</small> hello.routes.ts</div>\r\n <div className=\"code-file code-indent\"><small>TS</small> app.ts</div>\r\n <div className={'code-file code-indent' + (tab === 'types' ? ' selected' : '')}><small>TS</small> types.ts</div>\r\n <div className=\"code-file\"><small>{'{}'}</small> package.json</div>\r\n </div>\r\n <div className=\"code-main\">\r\n <div className=\"code-tabs\" role=\"tablist\" aria-label=\"Code examples\">\r\n {(['route', 'types'] as const).map(key => <button type=\"button\" key={key} id={`${id}-${key}`} role=\"tab\" aria-selected={tab === key} aria-controls={`${id}-panel`} tabIndex={tab === key ? 0 : -1} onClick={() => setTab(key)} onKeyDown={event => {\r\n if (!['ArrowLeft','ArrowRight','Home','End'].includes(event.key)) return\r\n event.preventDefault()\r\n const next = event.key === 'Home' ? 'route' : event.key === 'End' ? 'types' : key === 'route' ? 'types' : 'route'\r\n setTab(next)\r\n event.currentTarget.parentElement?.querySelector<HTMLButtonElement>(`[id=\"${id}-${next}\"]`)?.focus()\r\n }}><small>TS</small>{key === 'route' ? 'hello.routes.ts' : 'types.ts'}<span aria-hidden=\"true\">{'\\u00d7'}</span></button>)}\r\n </div>\r\n <div role=\"tabpanel\" id={`${id}-panel`} aria-labelledby={`${id}-${tab}`} tabIndex={0} className=\"code-content\">\r\n <pre><code>{examples[tab].map((line, index) => <span className=\"code-line\" key={index}><span className=\"code-number\" aria-hidden=\"true\">{index + 1}</span><span><Highlight line={line || ' '} /></span>{'\\n'}</span>)}</code></pre>\r\n </div>\r\n </div>\r\n </div>\r\n </section>\r\n}\r\n",
13
+ "src/components/elements/catalogue.ts": "import Example36 from './examples/file-upload'\r\nimport source36 from './examples/file-upload.tsx?raw'\r\nimport Example37 from './examples/document-upload'\r\nimport source37 from './examples/document-upload.tsx?raw'\r\nimport type { ElementExample } from './example-card'\r\nimport Example0 from './examples/button-variants'\r\nimport source0 from './examples/button-variants.tsx?raw'\r\nimport Example1 from './examples/icon-button'\r\nimport source1 from './examples/icon-button.tsx?raw'\r\nimport Example2 from './examples/disabled-button'\r\nimport source2 from './examples/disabled-button.tsx?raw'\r\nimport Example3 from './examples/loading-button'\r\nimport source3 from './examples/loading-button.tsx?raw'\r\nimport Example4 from './examples/text-input'\r\nimport source4 from './examples/text-input.tsx?raw'\r\nimport Example5 from './examples/email-input'\r\nimport source5 from './examples/email-input.tsx?raw'\r\nimport Example6 from './examples/password-input'\r\nimport source6 from './examples/password-input.tsx?raw'\r\nimport Example7 from './examples/search-input'\r\nimport source7 from './examples/search-input.tsx?raw'\r\nimport Example8 from './examples/textarea'\r\nimport source8 from './examples/textarea.tsx?raw'\r\nimport Example9 from './examples/select'\r\nimport source9 from './examples/select.tsx?raw'\r\nimport Example10 from './examples/checkbox'\r\nimport source10 from './examples/checkbox.tsx?raw'\r\nimport Example11 from './examples/radio-group'\r\nimport source11 from './examples/radio-group.tsx?raw'\r\nimport Example12 from './examples/switch'\r\nimport source12 from './examples/switch.tsx?raw'\r\nimport Example13 from './examples/contact-form'\r\nimport source13 from './examples/contact-form.tsx?raw'\r\nimport Example14 from './examples/login-form'\r\nimport source14 from './examples/login-form.tsx?raw'\r\nimport Example15 from './examples/profile-form'\r\nimport source15 from './examples/profile-form.tsx?raw'\r\nimport Example16 from './examples/validation-form'\r\nimport source16 from './examples/validation-form.tsx?raw'\r\nimport Example17 from './examples/alerts'\r\nimport source17 from './examples/alerts.tsx?raw'\r\nimport Example18 from './examples/toast'\r\nimport source18 from './examples/toast.tsx?raw'\r\nimport Example19 from './examples/badges'\r\nimport source19 from './examples/badges.tsx?raw'\r\nimport Example20 from './examples/spinner'\r\nimport source20 from './examples/spinner.tsx?raw'\r\nimport Example21 from './examples/empty-state'\r\nimport source21 from './examples/empty-state.tsx?raw'\r\nimport Example22 from './examples/basic-card'\r\nimport source22 from './examples/basic-card.tsx?raw'\r\nimport Example23 from './examples/image-card'\r\nimport source23 from './examples/image-card.tsx?raw'\r\nimport Example24 from './examples/statistics-card'\r\nimport source24 from './examples/statistics-card.tsx?raw'\r\nimport Example25 from './examples/divider'\r\nimport source25 from './examples/divider.tsx?raw'\r\nimport Example26 from './examples/tabs'\r\nimport source26 from './examples/tabs.tsx?raw'\r\nimport Example27 from './examples/breadcrumbs'\r\nimport source27 from './examples/breadcrumbs.tsx?raw'\r\nimport Example28 from './examples/dropdown'\r\nimport source28 from './examples/dropdown.tsx?raw'\r\nimport Example29 from './examples/pagination'\r\nimport source29 from './examples/pagination.tsx?raw'\r\nimport Example30 from './examples/modal'\r\nimport source30 from './examples/modal.tsx?raw'\r\nimport Example31 from './examples/confirmation'\r\nimport source31 from './examples/confirmation.tsx?raw'\r\nimport Example32 from './examples/basic-table'\r\nimport source32 from './examples/basic-table.tsx?raw'\r\nimport Example33 from './examples/status-table'\r\nimport source33 from './examples/status-table.tsx?raw'\r\nimport Example34 from './examples/searchable-list'\r\nimport source34 from './examples/searchable-list.tsx?raw'\r\nimport Example35 from './examples/row-actions'\r\nimport source35 from './examples/row-actions.tsx?raw'\r\n\r\nexport const elementSections = [{\"id\": \"actions\", \"title\": \"Buttons & actions\", \"label\": \"Actions\"}, {\"id\": \"inputs\", \"title\": \"Form inputs\", \"label\": \"Inputs\"}, { id: \"uploads\", title: \"File uploads\", label: \"Uploads\" }, {\"id\": \"choices\", \"title\": \"Choices & toggles\", \"label\": \"Choices\"}, {\"id\": \"forms\", \"title\": \"Complete forms\", \"label\": \"Forms\"}, {\"id\": \"feedback\", \"title\": \"Feedback & status\", \"label\": \"Feedback\"}, {\"id\": \"cards\", \"title\": \"Cards & layout\", \"label\": \"Cards\"}, {\"id\": \"navigation\", \"title\": \"Navigation\", \"label\": \"Navigation\"}, {\"id\": \"overlays\", \"title\": \"Overlays\", \"label\": \"Overlays\"}, {\"id\": \"data\", \"title\": \"Tables & lists\", \"label\": \"Tables\"}]\r\nexport const elementExamples: ElementExample[] = [\r\n { id: 'file-upload', title: 'Image upload', description: 'Drag and drop, preview, progress, cancel and retry. Local demonstration.', section: 'uploads', wide: false, Preview: Example36, source: source36 },\r\n { id: 'document-upload', title: 'Document picker', description: 'File type and size validation for documents. Local demonstration.', section: 'uploads', wide: false, Preview: Example37, source: source37 },\r\n { \"id\": \"button-variants\", \"title\": \"Button variants\", \"description\": \"Primary, outline, and ghost actions.\", \"section\": \"actions\", \"wide\": false, Preview: Example0, source: source0 },\r\n { \"id\": \"icon-button\", \"title\": \"Icon button\", \"description\": \"A compact action with an accessible label.\", \"section\": \"actions\", \"wide\": false, Preview: Example1, source: source1 },\r\n { \"id\": \"disabled-button\", \"title\": \"Disabled buttons\", \"description\": \"Make unavailable actions clear.\", \"section\": \"actions\", \"wide\": false, Preview: Example2, source: source2 },\r\n { \"id\": \"loading-button\", \"title\": \"Loading button\", \"description\": \"Prevent repeat clicks while work is pending.\", \"section\": \"actions\", \"wide\": false, Preview: Example3, source: source3 },\r\n { \"id\": \"text-input\", \"title\": \"Text input\", \"description\": \"A labeled field with a character limit.\", \"section\": \"inputs\", \"wide\": false, Preview: Example4, source: source4 },\r\n { \"id\": \"email-input\", \"title\": \"Email input\", \"description\": \"Email entry with inline format feedback.\", \"section\": \"inputs\", \"wide\": false, Preview: Example5, source: source5 },\r\n { \"id\": \"password-input\", \"title\": \"Password input\", \"description\": \"Show or hide the password as you type.\", \"section\": \"inputs\", \"wide\": false, Preview: Example6, source: source6 },\r\n { \"id\": \"search-input\", \"title\": \"Search input\", \"description\": \"Filter project names as you type.\", \"section\": \"inputs\", \"wide\": false, Preview: Example7, source: source7 },\r\n { \"id\": \"textarea\", \"title\": \"Textarea\", \"description\": \"Collect longer text with a character counter.\", \"section\": \"inputs\", \"wide\": false, Preview: Example8, source: source8 },\r\n { \"id\": \"select\", \"title\": \"Select\", \"description\": \"Choose a value from a native dropdown.\", \"section\": \"inputs\", \"wide\": false, Preview: Example9, source: source9 },\r\n { \"id\": \"checkbox\", \"title\": \"Checkbox\", \"description\": \"Opt in to a single preference.\", \"section\": \"choices\", \"wide\": false, Preview: Example10, source: source10 },\r\n { \"id\": \"radio-group\", \"title\": \"Radio group\", \"description\": \"Choose one option with native keyboard support.\", \"section\": \"choices\", \"wide\": false, Preview: Example11, source: source11 },\r\n { \"id\": \"switch\", \"title\": \"Switch\", \"description\": \"Turn a setting on or off.\", \"section\": \"choices\", \"wide\": false, Preview: Example12, source: source12 },\r\n { \"id\": \"contact-form\", \"title\": \"Contact form\", \"description\": \"A responsive form with native validation; submits locally.\", \"section\": \"forms\", \"wide\": true, Preview: Example13, source: source13 },\r\n { \"id\": \"login-form\", \"title\": \"Login form\", \"description\": \"A sign-in form pattern; it does not authenticate.\", \"section\": \"forms\", \"wide\": true, Preview: Example14, source: source14 },\r\n { \"id\": \"profile-form\", \"title\": \"Profile form\", \"description\": \"Edit example details and save them locally.\", \"section\": \"forms\", \"wide\": true, Preview: Example15, source: source15 },\r\n { \"id\": \"validation-form\", \"title\": \"Form validation\", \"description\": \"Show field errors and a successful validation state.\", \"section\": \"forms\", \"wide\": true, Preview: Example16, source: source16 },\r\n { \"id\": \"alerts\", \"title\": \"Alerts\", \"description\": \"Informational, success, warning, and error messages.\", \"section\": \"feedback\", \"wide\": false, Preview: Example17, source: source17 },\r\n { \"id\": \"toast\", \"title\": \"Toast notifications\", \"description\": \"Dismissible messages with automatic timeout.\", \"section\": \"feedback\", \"wide\": false, Preview: Example18, source: source18 },\r\n { \"id\": \"badges\", \"title\": \"Status badges\", \"description\": \"Small labels for states and categories.\", \"section\": \"feedback\", \"wide\": false, Preview: Example19, source: source19 },\r\n { \"id\": \"spinner\", \"title\": \"Loading indicator\", \"description\": \"Communicate progress and completion.\", \"section\": \"feedback\", \"wide\": false, Preview: Example20, source: source20 },\r\n { \"id\": \"empty-state\", \"title\": \"Empty state\", \"description\": \"Explain an empty view and offer a next step.\", \"section\": \"feedback\", \"wide\": false, Preview: Example21, source: source21 },\r\n { \"id\": \"basic-card\", \"title\": \"Basic card\", \"description\": \"Related content and an action in one container.\", \"section\": \"cards\", \"wide\": false, Preview: Example22, source: source22 },\r\n { \"id\": \"image-card\", \"title\": \"Image card\", \"description\": \"A self-contained image and content card.\", \"section\": \"cards\", \"wide\": false, Preview: Example23, source: source23 },\r\n { \"id\": \"statistics-card\", \"title\": \"Statistics card\", \"description\": \"A metric with supporting context.\", \"section\": \"cards\", \"wide\": false, Preview: Example24, source: source24 },\r\n { \"id\": \"divider\", \"title\": \"Divider\", \"description\": \"Separate related groups without adding visual noise.\", \"section\": \"cards\", \"wide\": false, Preview: Example25, source: source25 },\r\n { \"id\": \"tabs\", \"title\": \"Tabs\", \"description\": \"Switch panels with arrow keys, Home, and End.\", \"section\": \"navigation\", \"wide\": false, Preview: Example26, source: source26 },\r\n { \"id\": \"breadcrumbs\", \"title\": \"Breadcrumbs\", \"description\": \"Show the current location and its parent pages.\", \"section\": \"navigation\", \"wide\": false, Preview: Example27, source: source27 },\r\n { \"id\": \"dropdown\", \"title\": \"Dropdown menu\", \"description\": \"Keyboard-accessible actions with outside-click dismissal.\", \"section\": \"navigation\", \"wide\": false, Preview: Example28, source: source28 },\r\n { \"id\": \"pagination\", \"title\": \"Pagination\", \"description\": \"Move through a small set of example projects.\", \"section\": \"navigation\", \"wide\": false, Preview: Example29, source: source29 },\r\n { \"id\": \"modal\", \"title\": \"Modal dialog\", \"description\": \"A focused form with native focus containment.\", \"section\": \"overlays\", \"wide\": false, Preview: Example30, source: source30 },\r\n { \"id\": \"confirmation\", \"title\": \"Confirmation dialog\", \"description\": \"Confirm an action before changing the local example.\", \"section\": \"overlays\", \"wide\": false, Preview: Example31, source: source31 },\r\n { \"id\": \"basic-table\", \"title\": \"Basic table\", \"description\": \"Clear headings and a descriptive caption.\", \"section\": \"data\", \"wide\": true, Preview: Example32, source: source32 },\r\n { \"id\": \"status-table\", \"title\": \"Status table\", \"description\": \"Combine structured data and colored status labels.\", \"section\": \"data\", \"wide\": true, Preview: Example33, source: source33 },\r\n { \"id\": \"searchable-list\", \"title\": \"Searchable list\", \"description\": \"Filter a list and handle an empty result.\", \"section\": \"data\", \"wide\": false, Preview: Example34, source: source34 },\r\n { \"id\": \"row-actions\", \"title\": \"Editable table rows\", \"description\": \"Rename and remove local rows with confirmation.\", \"section\": \"data\", \"wide\": true, Preview: Example35, source: source35 },\r\n]\r\n",
14
+ "src/components/elements/elements-library.tsx": "import { useState } from 'react'\r\nimport { Button, EmptyState, Input } from '@tilcayo/ui'\r\nimport { ExampleCard } from './example-card'\r\nimport { elementExamples, elementSections } from './catalogue'\r\nimport './elements.css'\r\n\r\nexport default function ElementsLibrary() {\r\n const [category, setCategory] = useState('all'), [search, setSearch] = useState('')\r\n const term = search.trim().toLowerCase()\r\n const matching = elementExamples.filter(example => (category === 'all' || example.section === category) && `${example.title} ${example.description} ${elementSections.find(section => section.id === example.section)?.title}`.toLowerCase().includes(term))\r\n return <div className=\"elements-library\">\r\n <header className=\"elements-heading\"><div><h1>Elements</h1><p>Explore working components. Copy the code and make them yours.</p></div><span className=\"elements-count\" role=\"status\">{matching.length} {matching.length === 1 ? 'example' : 'examples'}</span></header>\r\n <div className=\"elements-toolbar\"><div className=\"elements-categories\" role=\"group\" aria-label=\"Component categories\"><button type=\"button\" aria-pressed={category === 'all'} onClick={() => setCategory('all')}>All</button>{elementSections.map(section => <button key={section.id} type=\"button\" aria-pressed={category === section.id} onClick={() => setCategory(section.id)}>{section.label}</button>)}</div><Input className=\"elements-search\" type=\"search\" aria-label=\"Search components\" placeholder=\"Search components...\" value={search} maxLength={80} onChange={event => setSearch(event.target.value)} /></div>\r\n {elementSections.map(section => {\r\n const examples = matching.filter(example => example.section === section.id)\r\n if (!examples.length) return null\r\n return <section key={section.id} className=\"element-section\" aria-labelledby={'section-' + section.id}><header className=\"element-section-heading\"><h2 id={'section-' + section.id}>{section.title}</h2><span>{examples.length} {examples.length === 1 ? 'example' : 'examples'}</span></header><div className=\"elements-grid\">{examples.map(example => <ExampleCard key={example.id} example={example} />)}</div></section>\r\n })}\r\n {!matching.length && <EmptyState title=\"No components found\" description=\"Try another search or choose a different section.\" action={<Button variant=\"outline\" onClick={() => { setCategory('all'); setSearch('') }}>Clear filters</Button>} />}\r\n <p className=\"elements-footnote\">Every example includes its imports, state, and handlers. Form demos run locally and do not change your account or send requests.</p>\r\n </div>\r\n}\r\n",
15
+ "src/components/elements/elements.css": ".elements-library { padding-top: 28px; }\r\n.elements-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }\r\n.elements-heading h1 { margin: 0 0 9px; font-size: 2rem; font-weight: 750; letter-spacing: -.045em; line-height: 1.15; }\r\n.elements-heading p { margin: 0; color: #616977; font-size: .95rem; line-height: 1.6; }\r\n.elements-count { flex-shrink: 0; padding-top: 9px; font-size: .8rem; color: #6b7280; }\r\n.elements-toolbar { position: sticky; top: 76px; z-index: 15; display: flex; align-items: center; gap: 20px; padding: 12px 0; background: #fff; border-top: 1px solid #edf0f3; border-bottom: 1px solid #e4e7ec; }\r\n.elements-categories { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: thin; }\r\n.elements-categories > button { flex-shrink: 0; min-height: 36px; padding: 8px 12px; border: 1px solid transparent; border-radius: 5px; color: #616977; background: transparent; font: 500 .8rem Arial, sans-serif; white-space: nowrap; cursor: pointer; }\r\n.elements-categories > button:hover { color: #101112; background: #f6f7f9; }\r\n.elements-categories > button[aria-pressed=\"true\"] { color: #fff; background: #101112; border-color: #101112; }\r\n.elements-categories > button:focus-visible, .element-code-toggle:focus-visible { outline: 2px solid #596579; outline-offset: -2px; }\r\n.elements-toolbar .elements-search { flex: 0 0 220px; width: 220px; min-height: 38px; padding-block: 8px; border-radius: 6px; font-size: .85rem; }\r\n.element-section { margin-top: 30px; scroll-margin-top: 150px; }\r\n.element-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }\r\n.element-section-heading h2 { margin: 0; color: #14171c; font-size: 1.15rem; font-weight: 700; letter-spacing: -.025em; }\r\n.element-section-heading > span { color: #7a8290; font-size: .75rem; }\r\n.elements-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 18px; align-items: start; }\r\n.element-example { min-width: 0; border: 1px solid #e1e5eb; border-radius: 8px; background: #fff; }\r\n.element-example-wide { grid-column: 1 / -1; }\r\n.element-example-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 20px 20px 17px; }\r\n.element-example-heading > div { min-width: 0; }\r\n.element-example-heading h3 { margin: 0 0 6px; font-size: 1rem; letter-spacing: -.02em; font-weight: 650; }\r\n.element-example-heading p { margin: 0; color: #6b7280; font-size: .8rem; line-height: 1.6; }\r\n.element-code-toggle { display: inline-flex; align-items: center; justify-content: center; gap: 7px; flex-shrink: 0; min-height: 32px; padding: 5px 9px; border: 1px solid #dfe3e9; border-radius: 5px; background: #fff; color: #4e5868; font: 500 .75rem Arial, sans-serif; cursor: pointer; white-space: nowrap; }\r\n.element-code-toggle:hover, .element-code-toggle[aria-expanded=\"true\"] { color: #111418; background: #f4f5f7; border-color: #c7cdd7; }\r\n.element-code-toggle > span { font-family: Consolas, monospace; font-size: .8rem; }\r\n.element-preview { min-width: 0; min-height: 110px; margin: 0 20px 20px; padding: 20px; border: 1px solid #edf0f3; border-radius: 6px; background: #fafbfc; color: #191c22; font-size: .9rem; }\r\n.element-preview .tl-btn { font-size: .85rem; }\r\n.element-preview .tl-label { font-size: .875rem; }\r\n.element-preview :is(.tl-input,.tl-select,.tl-textarea) { font-size: .9rem; }\r\n.element-preview .tl-table { font-size: .85rem; }\r\n.element-preview:has(.tl-table) { padding: 0; border: 0; background: transparent; }\r\n.element-code { border-top: 1px solid #34393f; border-radius: 0 0 7px 7px; background: #17191d; color: #e3e8ef; }\r\n.element-code-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 46px; padding: 8px 16px; border-bottom: 1px solid #30343c; color: #9aa4b2; font: .75rem Arial, sans-serif; }\r\n.element-code-toolbar > button { min-width: 80px; padding: 6px 10px; border: 1px solid #434a55; border-radius: 4px; background: #262b32; color: #e6ebf2; font: 500 .75rem Arial, sans-serif; cursor: pointer; }\r\n.element-code-toolbar > button:hover { background: #343b45; }\r\n.element-code pre { max-height: 440px; margin: 0; padding: 20px; overflow: auto; border-radius: 0 0 7px 7px; font: 12px/1.8 Consolas, 'Courier New', monospace; tab-size: 2; }\r\n.element-code code { white-space: pre; font: inherit; color: inherit; }\r\n.element-token-keyword { color: #c7a1e8; }\r\n.element-token-string { color: #d7b384; }\r\n.element-token-type { color: #8ccbd8; }\r\n.element-token-number { color: #a8c99a; }\r\n.element-token-comment { color: #8a95a4; }\r\n.element-copy-error { display: block; padding: 0 20px 16px; color: #ffb3b3; font-size: .8rem; }\r\n.elements-footnote { margin: 34px 0 0; padding-top: 18px; border-top: 1px solid #e7eaf0; color: #737b88; font-size: .8rem; line-height: 1.7; }\r\n@media (max-width: 1100px) {\r\n .elements-toolbar { gap: 12px; }\r\n .elements-toolbar .elements-search { flex-basis: 190px; width: 190px; }\r\n .element-example-heading { gap: 10px; }\r\n .element-code-toggle > span { display: none; }\r\n}\r\n@media (max-width: 767px) {\r\n .elements-library { padding-top: 24px; }\r\n .elements-heading { gap: 12px; margin-bottom: 20px; }\r\n .elements-heading h1 { font-size: 1.8rem; }\r\n .elements-heading p { font-size: .85rem; }\r\n .elements-count { font-size: .7rem; padding-top: 7px; }\r\n .elements-toolbar { top: 68px; flex-direction: column; align-items: stretch; gap: 10px; }\r\n .elements-toolbar .elements-search { order: -1; flex-basis: auto; width: 100%; }\r\n .elements-categories { flex: auto; width: 100%; padding-bottom: 3px; }\r\n .elements-categories > button { min-height: 34px; padding: 7px 10px; }\r\n .elements-grid { grid-template-columns: minmax(0,1fr); gap: 14px; }\r\n .element-section { margin-top: 26px; }\r\n .element-example-heading { padding: 17px 16px 15px; }\r\n .element-example-heading h3 { font-size: .95rem; }\r\n .element-preview { margin: 0 16px 16px; padding: 16px; }\r\n .element-code pre { padding: 16px; font-size: 11px; }\r\n}\r\n",
16
+ "src/components/elements/example-card.tsx": "import { useEffect, useRef, useState } from 'react'\r\nimport type { ComponentType } from 'react'\r\n\r\nexport interface ElementExample { id: string; title: string; description: string; section: string; wide?: boolean; Preview: ComponentType; source: string }\r\n\r\nfunction HighlightedCode({ source }: { source: string }) {\r\n const tokens = source.split(/(\\/\\*[\\s\\S]*?\\*\\/|\\/\\/[^\\n]*|\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`(?:\\\\.|[^`\\\\])*`|\\b(?:import|from|export|default|function|return|const|let|if|else|async|await|type|true|false|null|undefined|new|as)\\b|\\b\\d+(?:\\.\\d+)?\\b|\\b[A-Z][A-Za-z0-9_]*\\b)/g)\r\n return <>{tokens.map((token, index) => {\r\n const kind = /^\\/[/*]/.test(token) ? 'comment' : /^[\"'`]/.test(token) ? 'string' : /^(import|from|export|default|function|return|const|let|if|else|async|await|type|true|false|null|undefined|new|as)$/.test(token) ? 'keyword' : /^\\d/.test(token) ? 'number' : /^[A-Z]\\w*$/.test(token) ? 'type' : ''\r\n return kind ? <span key={index} className={'element-token-' + kind}>{token}</span> : token\r\n })}</>\r\n}\r\nasync function copyText(source: string) {\r\n try { if (navigator.clipboard) { await navigator.clipboard.writeText(source); return } } catch { /* Try the selection-based fallback. */ }\r\n const area = document.createElement('textarea')\r\n const focused = document.activeElement as HTMLElement | null\r\n area.value = source\r\n area.style.cssText = 'position:fixed;left:-9999px;top:0'\r\n document.body.appendChild(area)\r\n try { area.select(); if (!document.execCommand('copy')) throw new Error('Copy unavailable') }\r\n finally { area.remove(); focused?.focus() }\r\n}\r\nexport function ExampleCard({ example }: { example: ElementExample }) {\r\n const [open, setOpen] = useState(false), [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle')\r\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\r\n useEffect(() => () => clearTimeout(timer.current), [])\r\n const titleId = 'element-' + example.id + '-title', codeId = 'element-' + example.id + '-code'\r\n const Preview = example.Preview\r\n return <article className={`element-example${example.wide ? ' element-example-wide' : ''}`} aria-labelledby={titleId} data-example={example.id}>\r\n <header className=\"element-example-heading\"><div><h3 id={titleId}>{example.title}</h3><p>{example.description}</p></div><button className=\"element-code-toggle\" type=\"button\" aria-expanded={open} aria-controls={codeId} onClick={() => setOpen(!open)}><span aria-hidden=\"true\">{'</>'}</span>{open ? 'Hide code' : 'Show code'}</button></header>\r\n <div className=\"element-preview\" aria-label={example.title + ' preview'}><Preview /></div>\r\n <div className=\"element-code\" id={codeId} hidden={!open}>\r\n {open && <><div className=\"element-code-toolbar\"><span>React · TypeScript</span><button type=\"button\" onClick={async () => {\r\n clearTimeout(timer.current)\r\n try { await copyText(example.source); setCopyState('copied') } catch { setCopyState('error') }\r\n timer.current = setTimeout(() => setCopyState('idle'), 2500)\r\n }}>{copyState === 'copied' ? 'Copied' : 'Copy code'}</button></div><pre tabIndex={0} aria-label={example.title + ' source code'}><code><HighlightedCode source={example.source} /></code></pre><span className={copyState === 'error' ? 'element-copy-error' : 'tl-sr-only'} role=\"status\">{copyState === 'copied' ? 'Code copied to clipboard.' : copyState === 'error' ? 'Unable to copy. Select the code and copy it manually.' : ''}</span></>}\r\n </div>\r\n </article>\r\n}\r\n",
17
+ "src/components/elements/examples/alerts.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Alert } from '@tilcayo/ui'\r\n\r\nexport default function AlertsExample() {\r\n return <div className=\"tl-stack\">\r\n <Alert variant=\"info\" role=\"status\">Your export is ready to download.</Alert>\r\n <Alert variant=\"success\" role=\"status\">Changes saved successfully.</Alert>\r\n <Alert variant=\"warning\">Some required details are missing.</Alert>\r\n <Alert variant=\"danger\">Unable to save. Please try again.</Alert>\r\n </div>\r\n}\r\n",
18
+ "src/components/elements/examples/badges.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Badge } from '@tilcayo/ui'\r\n\r\nexport default function BadgesExample() {\r\n return <div className=\"tl-flex tl-flex-wrap tl-gap-2\"><Badge>Draft</Badge><Badge variant=\"success\">Active</Badge><Badge variant=\"warning\">Pending</Badge><Badge variant=\"danger\">Failed</Badge><Badge variant=\"primary\">New</Badge></div>\r\n}\r\n",
19
+ "src/components/elements/examples/basic-card.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Button, Card } from '@tilcayo/ui'\r\n\r\nexport default function BasicCardExample() {\r\n const [saved, setSaved] = useState(false)\r\n return <Card className=\"tl-stack\"><h3 style={{ margin: 0 }}>A place for your next idea</h3><p style={{ margin: 0 }}>Group related content and actions inside a simple container.</p><div><Button variant=\"outline\" aria-pressed={saved} onClick={() => setSaved(!saved)}>{saved ? 'Saved to favorites' : 'Save to favorites'}</Button></div></Card>\r\n}\r\n",
20
+ "src/components/elements/examples/basic-table.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Table } from '@tilcayo/ui'\r\n\r\nexport default function BasicTableExample() {\r\n return <Table><caption>Example projects</caption><thead><tr><th scope=\"col\">Project</th><th scope=\"col\">Owner</th><th scope=\"col\">Updated</th></tr></thead><tbody><tr><td>Design system</td><td>Alex Morgan</td><td>Today</td></tr><tr><td>Marketing site</td><td>Sam Lee</td><td>Yesterday</td></tr><tr><td>Customer portal</td><td>Taylor Kim</td><td>Monday</td></tr></tbody></Table>\r\n}\r\n",
21
+ "src/components/elements/examples/breadcrumbs.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Breadcrumbs } from '@tilcayo/ui'\r\n\r\nexport default function BreadcrumbsExample() {\r\n return <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Components', href: '/elements' }, { label: 'Breadcrumbs' }]} />\r\n}\r\n",
22
+ "src/components/elements/examples/button-variants.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Button } from '@tilcayo/ui'\r\n\r\nexport default function ButtonVariantsExample() {\r\n const [message, setMessage] = useState('Choose an action.')\r\n return <div className=\"tl-stack\"><div className=\"tl-flex tl-flex-wrap tl-gap-2\">\r\n <Button onClick={() => setMessage('Primary action selected.')}>Primary</Button>\r\n <Button variant=\"outline\" onClick={() => setMessage('Outline action selected.')}>Outline</Button>\r\n <Button variant=\"ghost\" onClick={() => setMessage('Ghost action selected.')}>Ghost</Button>\r\n </div><p className=\"tl-help-text\" role=\"status\">{message}</p></div>\r\n}\r\n",
23
+ "src/components/elements/examples/checkbox.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Checkbox } from '@tilcayo/ui'\r\n\r\nexport default function CheckboxExample() {\r\n const [checked, setChecked] = useState(false)\r\n return <div className=\"tl-stack\"><Checkbox label=\"Send me project updates\" checked={checked} onChange={event => setChecked(event.target.checked)} /><p role=\"status\" className=\"tl-help-text\">{checked ? 'Project updates enabled.' : 'Project updates disabled.'}</p></div>\r\n}\r\n",
24
+ "src/components/elements/examples/confirmation.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, ConfirmDialog } from '@tilcayo/ui'\r\n\r\nexport default function ConfirmationExample() {\r\n const [open, setOpen] = useState(false)\r\n const [deleted, setDeleted] = useState(false)\r\n return <div className=\"tl-stack\">{deleted ? <><Alert variant=\"success\" role=\"status\">The example project was removed.</Alert><div><Button variant=\"outline\" onClick={() => setDeleted(false)}>Restore example</Button></div></> : <div><Button variant=\"outline\" onClick={() => setOpen(true)}>Delete example project</Button></div>}\r\n <ConfirmDialog open={open} title=\"Delete project?\" message=\"This removes the local example project.\" confirmText=\"Delete project\" destructive onClose={() => setOpen(false)} onConfirm={() => setDeleted(true)} />\r\n </div>\r\n}\r\n",
25
+ "src/components/elements/examples/contact-form.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, FormField, Input, Textarea } from '@tilcayo/ui'\r\n\r\nexport default function ContactFormExample() {\r\n const [message, setMessage] = useState('')\r\n return <form className=\"tl-stack\" onSubmit={event => {\r\n event.preventDefault()\r\n const data = new FormData(event.currentTarget)\r\n setMessage(`Message preview ready for ${String(data.get('name'))}. Nothing was sent.`)\r\n }}>\r\n <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))', gap: 16 }}>\r\n <FormField label=\"Your name\">{props => <Input {...props} name=\"name\" required minLength={2} autoComplete=\"name\" placeholder=\"Alex Morgan\" />}</FormField>\r\n <FormField label=\"Your email\">{props => <Input {...props} name=\"email\" type=\"email\" required autoComplete=\"email\" placeholder=\"alex@example.com\" />}</FormField>\r\n </div>\r\n <FormField label=\"Message\">{props => <Textarea {...props} name=\"message\" required minLength={10} rows={3} placeholder=\"How can we help?\" />}</FormField>\r\n <div><Button type=\"submit\">Preview message</Button></div>\r\n {message && <Alert variant=\"success\" role=\"status\">{message}</Alert>}\r\n </form>\r\n}\r\n",
26
+ "src/components/elements/examples/disabled-button.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Button } from '@tilcayo/ui'\r\n\r\nexport default function DisabledButtonExample() {\r\n return <div className=\"tl-stack\"><div className=\"tl-flex tl-flex-wrap tl-gap-2\"><Button disabled>Publish</Button><Button variant=\"outline\" disabled>Export</Button></div><p className=\"tl-help-text\">Disabled actions cannot be clicked or focused.</p></div>\r\n}\r\n",
27
+ "src/components/elements/examples/divider.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Divider } from '@tilcayo/ui'\r\n\r\nexport default function DividerExample() {\r\n return <div><p style={{ margin: 0 }}>Personal information</p><Divider /><p style={{ margin: 0 }}>Workspace preferences</p></div>\r\n}\r\n",
28
+ "src/components/elements/examples/document-upload.tsx": "import { FileUpload } from '@tilcayo/ui'\nimport '@tilcayo/styles/index.css'\n\nexport default function DocumentUploadExample() {\n return <FileUpload label=\"Choose a document\" accept=\".pdf,.docx,.xlsx,.zip\" maxBytes={20 * 1024 * 1024} description=\"Local demo · PDF, DOCX, XLSX or ZIP · Up to 20 MB. No file leaves your browser.\" onUpload={async (_file, { signal, onProgress }) => {\n if (signal.aborted) throw new DOMException('Cancelled', 'AbortError')\n onProgress(50)\n await new Promise<void>(resolve => setTimeout(resolve, 500))\n if (signal.aborted) throw new DOMException('Cancelled', 'AbortError')\n onProgress(100)\n }} />\n}\n",
29
+ "src/components/elements/examples/dropdown.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { DropdownMenu } from '@tilcayo/ui'\r\n\r\nexport default function DropdownExample() {\r\n const [message, setMessage] = useState('Choose a project action.')\r\n return <div className=\"tl-stack\"><div><DropdownMenu label=\"Project actions\" items={[\r\n { label: 'Rename', onSelect: () => setMessage('Rename selected.') },\r\n { label: 'Duplicate', onSelect: () => setMessage('Duplicate selected.') },\r\n { label: 'Archive', danger: true, onSelect: () => setMessage('Archive selected.') },\r\n ]} /></div><p role=\"status\" className=\"tl-help-text\">{message}</p></div>\r\n}\r\n",
30
+ "src/components/elements/examples/email-input.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { FormField, Input } from '@tilcayo/ui'\r\n\r\nexport default function EmailInputExample() {\r\n const [error, setError] = useState('')\r\n return <FormField label=\"Email address\" hint=\"We will use this to contact you.\" error={error}>{props => <Input {...props} type=\"email\" autoComplete=\"email\" placeholder=\"alex@example.com\" onBlur={event => setError(event.target.validity.typeMismatch ? 'Enter a valid email address.' : '')} onChange={() => setError('')} />}</FormField>\r\n}\r\n",
31
+ "src/components/elements/examples/empty-state.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, EmptyState } from '@tilcayo/ui'\r\n\r\nexport default function EmptyStateExample() {\r\n const [created, setCreated] = useState(false)\r\n return created ? <div className=\"tl-stack\"><Alert variant=\"success\" role=\"status\">Your first project is ready.</Alert><div><Button variant=\"outline\" onClick={() => setCreated(false)}>Reset example</Button></div></div> : <EmptyState title=\"No projects yet\" description=\"Create a project to give your next idea a home.\" action={<Button onClick={() => setCreated(true)}>Create project</Button>} />\r\n}\r\n",
32
+ "src/components/elements/examples/file-upload.tsx": "import { FileUpload } from '@tilcayo/ui'\nimport '@tilcayo/styles/index.css'\n\nexport default function FileUploadExample() {\n return <FileUpload label=\"Upload an image\" description=\"Local demo · JPG, PNG or WebP · Up to 2 MB. No file leaves your browser.\" onUpload={async (_file, { signal, onProgress }) => {\n for (let percent = 0; percent <= 100; percent += 20) {\n if (signal.aborted) throw new DOMException('Cancelled', 'AbortError')\n await new Promise<void>(resolve => setTimeout(resolve, 150))\n onProgress(percent)\n }\n }} />\n}\n",
33
+ "src/components/elements/examples/icon-button.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { IconButton } from '@tilcayo/ui'\r\n\r\nexport default function IconButtonExample() {\r\n const [saved, setSaved] = useState(false)\r\n return <div className=\"tl-flex tl-items-center tl-gap-3\">\r\n <IconButton label={saved ? 'Remove bookmark' : 'Bookmark project'} variant=\"outline\" aria-pressed={saved} onClick={() => setSaved(!saved)}>\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill={saved ? 'currentColor' : 'none'} stroke=\"currentColor\" strokeWidth=\"1.7\" aria-hidden=\"true\"><path d=\"M6 3h12v18l-6-4-6 4Z\" /></svg>\r\n </IconButton><span role=\"status\">{saved ? 'Bookmarked' : 'Save this project'}</span>\r\n </div>\r\n}\r\n",
34
+ "src/components/elements/examples/image-card.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Card } from '@tilcayo/ui'\r\n\r\nconst landscape = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 800 360\"><rect width=\"800\" height=\"360\" fill=\"#edf0f3\"/><circle cx=\"650\" cy=\"85\" r=\"40\" fill=\"#fff\"/><path d=\"M0 360 210 75 440 360Z\" fill=\"#abb3bc\"/><path d=\"m180 360 280-310 340 310Z\" fill=\"#333b45\"/><path d=\"m385 135 75-85 92 84-65-18-30 25-28-25Z\" fill=\"#eef0f2\"/></svg>')\r\n\r\nexport default function ImageCardExample() {\r\n return <Card style={{ padding: 0, overflow: 'hidden' }}><img src={landscape} alt=\"Minimal mountain landscape\" width={800} height={360} style={{ width: '100%', height: 160, objectFit: 'cover', display: 'block' }} /><div style={{ padding: 20 }}><h3 style={{ margin: '0 0 8px' }}>Room to explore</h3><p style={{ margin: 0 }}>An image, a title, and a little context.</p></div></Card>\r\n}\r\n",
35
+ "src/components/elements/examples/loading-button.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useEffect, useRef, useState } from 'react'\r\nimport { LoadingButton } from '@tilcayo/ui'\r\n\r\nexport default function LoadingButtonExample() {\r\n const [loading, setLoading] = useState(false)\r\n const [message, setMessage] = useState('')\r\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\r\n useEffect(() => () => clearTimeout(timer.current), [])\r\n return <div className=\"tl-stack\"><div><LoadingButton loading={loading} loadingText=\"Saving...\" onClick={() => {\r\n setLoading(true); setMessage('')\r\n timer.current = setTimeout(() => { setLoading(false); setMessage('Changes saved locally.') }, 800)\r\n }}>Save changes</LoadingButton></div><p role=\"status\" className=\"tl-help-text\">{message || 'A short local demo; no network request.'}</p></div>\r\n}\r\n",
36
+ "src/components/elements/examples/login-form.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, Checkbox, FormField, Input, PasswordInput } from '@tilcayo/ui'\r\n\r\nexport default function LoginFormExample() {\r\n const [message, setMessage] = useState('')\r\n return <form className=\"tl-stack\" style={{ maxWidth: 440 }} onSubmit={event => {\r\n event.preventDefault()\r\n const data = new FormData(event.currentTarget)\r\n setMessage(`Form validated for ${String(data.get('email'))}. This demo does not sign you in.`)\r\n }}>\r\n <FormField label=\"Email\">{props => <Input {...props} name=\"email\" type=\"email\" autoComplete=\"username\" required placeholder=\"alex@example.com\" />}</FormField>\r\n <FormField label=\"Password\">{props => <PasswordInput {...props} name=\"password\" autoComplete=\"current-password\" minLength={8} required placeholder=\"At least 8 characters\" />}</FormField>\r\n <Checkbox label=\"Remember this device\" name=\"remember\" />\r\n <div><Button type=\"submit\">Validate sign-in</Button></div>\r\n {message && <Alert variant=\"success\" role=\"status\">{message}</Alert>}\r\n </form>\r\n}\r\n",
37
+ "src/components/elements/examples/modal.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Button, FormField, Input, Modal } from '@tilcayo/ui'\r\n\r\nexport default function ModalExample() {\r\n const [open, setOpen] = useState(false)\r\n const [message, setMessage] = useState('')\r\n return <div className=\"tl-stack\"><div><Button onClick={() => setOpen(true)}>Open project dialog</Button></div><p role=\"status\" className=\"tl-help-text\">{message || 'Create a project in a focused dialog.'}</p>\r\n <Modal open={open} title=\"New project\" onClose={() => setOpen(false)}><form className=\"tl-stack\" onSubmit={event => {\r\n event.preventDefault()\r\n setMessage(`Created ${String(new FormData(event.currentTarget).get('name'))} locally.`)\r\n setOpen(false)\r\n }}><FormField label=\"Project name\">{props => <Input {...props} name=\"name\" required minLength={2} placeholder=\"My next project\" />}</FormField><div className=\"tl-flex tl-gap-2\"><Button variant=\"outline\" onClick={() => setOpen(false)}>Cancel</Button><Button type=\"submit\">Create project</Button></div></form></Modal>\r\n </div>\r\n}\r\n",
38
+ "src/components/elements/examples/pagination.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Pagination } from '@tilcayo/ui'\r\n\r\nconst projects = ['Design system', 'Marketing site', 'Mobile app', 'Brand refresh', 'Documentation', 'Customer portal']\r\n\r\nexport default function PaginationExample() {\r\n const [page, setPage] = useState(1)\r\n const pageSize = 2\r\n return <div className=\"tl-stack\"><ul style={{ margin: 0, paddingLeft: 20 }}>{projects.slice((page - 1) * pageSize, page * pageSize).map(name => <li key={name}>{name}</li>)}</ul><Pagination page={page} totalPages={Math.ceil(projects.length / pageSize)} onChange={setPage} /></div>\r\n}\r\n",
39
+ "src/components/elements/examples/password-input.tsx": "import '@tilcayo/styles/index.css'\r\nimport { FormField, PasswordInput } from '@tilcayo/ui'\r\n\r\nexport default function PasswordInputExample() {\r\n return <FormField label=\"Password\" hint=\"Use at least 8 characters.\">{props => <PasswordInput {...props} minLength={8} autoComplete=\"new-password\" placeholder=\"Enter a password\" />}</FormField>\r\n}\r\n",
40
+ "src/components/elements/examples/profile-form.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, FormField, Input, Switch } from '@tilcayo/ui'\r\n\r\nexport default function ProfileFormExample() {\r\n const [updates, setUpdates] = useState(true)\r\n const [message, setMessage] = useState('')\r\n return <form className=\"tl-stack\" onSubmit={event => {\r\n event.preventDefault()\r\n const data = new FormData(event.currentTarget)\r\n setMessage(`Saved ${String(data.get('name'))} locally. Updates ${updates ? 'enabled' : 'disabled'}.`)\r\n }}>\r\n <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))', gap: 16 }}>\r\n <FormField label=\"Full name\">{props => <Input {...props} name=\"name\" defaultValue=\"Alex Morgan\" required minLength={2} maxLength={80} autoComplete=\"name\" />}</FormField>\r\n <FormField label=\"Contact email\">{props => <Input {...props} name=\"email\" type=\"email\" defaultValue=\"alex@example.com\" required autoComplete=\"email\" />}</FormField>\r\n </div>\r\n <Switch label=\"Receive product updates\" checked={updates} onCheckedChange={setUpdates} />\r\n <div><Button type=\"submit\">Save profile</Button></div>\r\n {message && <Alert variant=\"success\" role=\"status\">{message}</Alert>}\r\n </form>\r\n}\r\n",
41
+ "src/components/elements/examples/radio-group.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { RadioGroup } from '@tilcayo/ui'\r\n\r\nexport default function RadioGroupExample() {\r\n const [billing, setBilling] = useState('monthly')\r\n return <div className=\"tl-stack\"><RadioGroup label=\"Billing frequency\" value={billing} onChange={setBilling} options={[{ value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' }]} /><p role=\"status\" className=\"tl-help-text\">Selected: {billing}</p></div>\r\n}\r\n",
42
+ "src/components/elements/examples/row-actions.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Button, ConfirmDialog, Input, Table } from '@tilcayo/ui'\r\n\r\nexport default function RowActionsExample() {\r\n const [rows, setRows] = useState([{ id: 1, name: 'Design system' }, { id: 2, name: 'Marketing site' }])\r\n const [editing, setEditing] = useState<number | null>(null)\r\n const [draft, setDraft] = useState('')\r\n const [removing, setRemoving] = useState<number | null>(null)\r\n const [message, setMessage] = useState('')\r\n return <div className=\"tl-stack\"><Table><caption>Editable projects</caption><thead><tr><th scope=\"col\">Project</th><th scope=\"col\">Actions</th></tr></thead><tbody>{rows.map(row => <tr key={row.id}>\r\n <td>{editing === row.id ? <Input aria-label=\"Edit project name\" value={draft} maxLength={80} onChange={event => setDraft(event.target.value)} /> : row.name}</td>\r\n <td><div className=\"tl-flex tl-flex-wrap tl-gap-2\">{editing === row.id ? <><Button variant=\"outline\" disabled={!draft.trim()} onClick={() => { setRows(rows.map(item => item.id === row.id ? { ...item, name: draft.trim() } : item)); setEditing(null); setMessage('Project renamed.') }}>Save</Button><Button variant=\"ghost\" onClick={() => setEditing(null)}>Cancel</Button></> : <><Button variant=\"outline\" aria-label={`Edit ${row.name}`} onClick={() => { setDraft(row.name); setEditing(row.id) }}>Edit</Button><Button variant=\"ghost\" aria-label={`Delete ${row.name}`} onClick={() => setRemoving(row.id)}>Delete</Button></>}</div></td>\r\n </tr>)}{!rows.length && <tr><td colSpan={2}>No projects remain.</td></tr>}</tbody></Table><p role=\"status\" className=\"tl-help-text\">{message || 'Edit or remove a local example row.'}</p><ConfirmDialog open={removing !== null} title=\"Delete row?\" message=\"Remove this project from the local example?\" confirmText=\"Delete row\" destructive onClose={() => setRemoving(null)} onConfirm={() => { setRows(rows.filter(row => row.id !== removing)); setMessage('Project removed.') }} /></div>\r\n}\r\n",
43
+ "src/components/elements/examples/search-input.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { FormField, Input } from '@tilcayo/ui'\r\n\r\nexport default function SearchInputExample() {\r\n const [query, setQuery] = useState('')\r\n const matches = ['Design system', 'Marketing site', 'Mobile app'].filter(item => item.toLowerCase().includes(query.toLowerCase()))\r\n return <div className=\"tl-stack\"><FormField label=\"Search projects\">{props => <Input {...props} type=\"search\" value={query} onChange={event => setQuery(event.target.value)} placeholder=\"Try design...\" />}</FormField><p className=\"tl-help-text\" role=\"status\">{matches.length ? matches.join(', ') : 'No matching projects.'}</p></div>\r\n}\r\n",
44
+ "src/components/elements/examples/searchable-list.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { EmptyState, FormField, Input } from '@tilcayo/ui'\r\n\r\nconst projects = ['Design system', 'Marketing site', 'Mobile app', 'Documentation']\r\n\r\nexport default function SearchableListExample() {\r\n const [search, setSearch] = useState('')\r\n const filtered = projects.filter(name => name.toLowerCase().includes(search.toLowerCase()))\r\n return <div className=\"tl-stack\"><FormField label=\"Find a project\">{props => <Input {...props} type=\"search\" value={search} onChange={event => setSearch(event.target.value)} placeholder=\"Search projects...\" />}</FormField><p className=\"tl-help-text\" role=\"status\">{filtered.length} matching projects</p>{filtered.length ? <ul style={{ margin: 0, paddingLeft: 20 }}>{filtered.map(name => <li key={name}>{name}</li>)}</ul> : <EmptyState title=\"No matches\" description=\"Try another project name.\" />}</div>\r\n}\r\n",
45
+ "src/components/elements/examples/select.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { FormField, Select } from '@tilcayo/ui'\r\n\r\nexport default function SelectExample() {\r\n const [category, setCategory] = useState('personal')\r\n return <FormField label=\"Workspace\" hint={`Selected: ${category}`}>{props => <Select {...props} value={category} onChange={event => setCategory(event.target.value)}><option value=\"personal\">Personal</option><option value=\"team\">Team</option><option value=\"organization\">Organization</option></Select>}</FormField>\r\n}\r\n",
46
+ "src/components/elements/examples/spinner.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, Spinner } from '@tilcayo/ui'\r\n\r\nexport default function SpinnerExample() {\r\n const [loading, setLoading] = useState(true)\r\n return <div className=\"tl-stack\">{loading ? <Spinner label=\"Loading projects...\" /> : <Alert variant=\"success\" role=\"status\">Projects are ready.</Alert>}<div><Button variant=\"outline\" onClick={() => setLoading(!loading)}>{loading ? 'Finish loading' : 'Restart loading'}</Button></div></div>\r\n}\r\n",
47
+ "src/components/elements/examples/statistics-card.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Button, StatCard } from '@tilcayo/ui'\r\n\r\nexport default function StatisticsCardExample() {\r\n const [projects, setProjects] = useState(12)\r\n return <div className=\"tl-stack\"><StatCard label=\"Active projects\" value={projects} note=\"Example workspace data\" /><div><Button variant=\"outline\" onClick={() => setProjects(projects + 1)}>Add a project</Button></div></div>\r\n}\r\n",
48
+ "src/components/elements/examples/status-table.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Badge, Table } from '@tilcayo/ui'\r\n\r\nconst projects = [{ name: 'Design system', status: 'Active' }, { name: 'Marketing site', status: 'Draft' }, { name: 'Customer portal', status: 'Pending' }] as const\r\n\r\nexport default function StatusTableExample() {\r\n return <Table><caption>Project status</caption><thead><tr><th scope=\"col\">Project</th><th scope=\"col\">Status</th></tr></thead><tbody>{projects.map(project => <tr key={project.name}><td>{project.name}</td><td><Badge variant={project.status === 'Active' ? 'success' : project.status === 'Pending' ? 'warning' : 'neutral'}>{project.status}</Badge></td></tr>)}</tbody></Table>\r\n}\r\n",
49
+ "src/components/elements/examples/switch.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Switch } from '@tilcayo/ui'\r\n\r\nexport default function SwitchExample() {\r\n const [enabled, setEnabled] = useState(false)\r\n return <div className=\"tl-stack\"><Switch label=\"Email notifications\" checked={enabled} onCheckedChange={setEnabled} /><p role=\"status\" className=\"tl-help-text\">Notifications are {enabled ? 'on' : 'off'}.</p></div>\r\n}\r\n",
50
+ "src/components/elements/examples/tabs.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Tabs } from '@tilcayo/ui'\r\n\r\nexport default function TabsExample() {\r\n return <Tabs label=\"Project details\" defaultValue=\"overview\" tabs={[\r\n { id: 'overview', label: 'Overview', content: <p>Your project at a glance.</p> },\r\n { id: 'activity', label: 'Activity', content: <p>Your project was created today.</p> },\r\n { id: 'settings', label: 'Settings', content: <p>Manage your project preferences.</p> },\r\n ]} />\r\n}\r\n",
51
+ "src/components/elements/examples/text-input.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { FormField, Input } from '@tilcayo/ui'\r\n\r\nexport default function TextInputExample() {\r\n const [name, setName] = useState('')\r\n return <FormField label=\"Display name\" hint={`${name.length}/40 characters`}>{props => <Input {...props} value={name} maxLength={40} onChange={event => setName(event.target.value)} placeholder=\"Alex Morgan\" autoComplete=\"name\" />}</FormField>\r\n}\r\n",
52
+ "src/components/elements/examples/textarea.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { FormField, Textarea } from '@tilcayo/ui'\r\n\r\nexport default function TextareaExample() {\r\n const [description, setDescription] = useState('')\r\n return <FormField label=\"Description\" hint={`${description.length}/240 characters`}>{props => <Textarea {...props} rows={3} maxLength={240} value={description} onChange={event => setDescription(event.target.value)} placeholder=\"Tell us about your project...\" />}</FormField>\r\n}\r\n",
53
+ "src/components/elements/examples/toast.tsx": "import '@tilcayo/styles/index.css'\r\nimport { Button, ToastProvider, useToast } from '@tilcayo/ui'\r\n\r\nfunction ToastActions() {\r\n const notify = useToast()\r\n return <div className=\"tl-flex tl-flex-wrap tl-gap-2\"><Button onClick={() => notify({ message: 'Project saved successfully.', variant: 'success' })}>Show success toast</Button><Button variant=\"outline\" onClick={() => notify({ message: 'Your download is ready.', variant: 'info' })}>Show info toast</Button></div>\r\n}\r\nexport default function ToastExample() {\r\n return <ToastProvider duration={4000}><ToastActions /></ToastProvider>\r\n}\r\n",
54
+ "src/components/elements/examples/validation-form.tsx": "import '@tilcayo/styles/index.css'\r\nimport { useState } from 'react'\r\nimport { Alert, Button, FormField, Input } from '@tilcayo/ui'\r\n\r\nexport default function ValidationFormExample() {\r\n const [errors, setErrors] = useState<{ name?: string; email?: string }>({})\r\n const [saved, setSaved] = useState(false)\r\n return <form className=\"tl-stack\" noValidate onSubmit={event => {\r\n event.preventDefault(); setSaved(false)\r\n const data = new FormData(event.currentTarget)\r\n const name = String(data.get('name') ?? '').trim()\r\n const email = String(data.get('email') ?? '').trim()\r\n const next = { name: name.length < 2 ? 'Enter at least 2 characters.' : undefined, email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ? undefined : 'Enter a valid email address.' }\r\n setErrors(next)\r\n if (!next.name && !next.email) setSaved(true)\r\n }}>\r\n <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))', gap: 16 }}>\r\n <FormField label=\"Name\" error={errors.name}>{props => <Input {...props} name=\"name\" required placeholder=\"Try submitting an empty form\" />}</FormField>\r\n <FormField label=\"Email address\" error={errors.email}>{props => <Input {...props} name=\"email\" type=\"email\" required placeholder=\"alex@example.com\" />}</FormField>\r\n </div>\r\n <div><Button type=\"submit\">Validate form</Button></div>\r\n {saved && <Alert variant=\"success\" role=\"status\">Both fields are valid. No data was sent.</Alert>}\r\n </form>\r\n}\r\n",
55
+ "src/components/error-page.tsx": "import { useId } from 'react'\r\nimport { Link } from 'react-router'\r\n\r\ntype Action = { to: string; label: string }\r\nexport function ErrorPage({ code, title, description, primary, secondary, note }: { code: '403' | '404'; title: string; description: string; primary: Action; secondary: Action; note: string }) {\r\n const titleId = useId()\r\n return <section className=\"site-error\" aria-labelledby={titleId}>\r\n <div className=\"site-error-visual\" aria-hidden=\"true\"><span className=\"site-error-code\">{code}</span><span className=\"site-error-symbol\"><svg width=\"38\" height=\"38\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">{code === '403' ? <><path d=\"m12 3 8 4v5c0 5-8 9-8 9s-8-4-8-9V7Z\" /><path d=\"M12 8v5m0 3h.01\" /></> : <><path d=\"M13 3H5v18h6M13 3l5 5h-5V3M8 8h2m-2 4h3\" /><circle cx=\"16\" cy=\"16\" r=\"4\" /><path d=\"m19 19 3 3\" /></>}</svg></span></div>\r\n <div className=\"site-error-content\"><p className=\"site-error-eyebrow\">{code === '403' ? 'Access restricted' : 'A missing page'}</p><h1 id={titleId}>{title}</h1><p className=\"site-error-description\">{description}</p><div className=\"site-error-actions\"><Link className=\"tl-btn tl-btn-primary\" to={primary.to}>{primary.label}<span aria-hidden=\"true\">&#8594;</span></Link><Link className=\"tl-btn tl-btn-outline\" to={secondary.to}>{secondary.label}</Link></div><p className=\"site-error-note\">{note}</p></div>\r\n </section>\r\n}\r\n",
56
+ "src/components/navigation.tsx": "import { Skeleton } from '@tilcayo/ui'\r\nimport { useEffect, useId, useRef, useState } from 'react'\r\nimport { Link, NavLink } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { BrandMark } from './starter-visuals'\r\nimport { AccountIcon } from './account-icon'\r\n\r\nconst adminEnabled = false\r\n\r\nexport function Navigation() {\r\n const { user, initialized, isAuthenticated, logout, logoutStatus } = useAuth()\r\n const [notice, setNotice] = useState('')\r\n const [isOpen, setIsOpen] = useState(false)\r\n const [accountOpen, setAccountOpen] = useState(false)\r\n const toggleRef = useRef<HTMLButtonElement>(null)\r\n const accountRef = useRef<HTMLDivElement>(null)\r\n const accountToggleRef = useRef<HTMLButtonElement>(null)\r\n const accountId = useId()\r\n const canAdmin = adminEnabled && ((user?.platformRole === 'superadmin' && !user?.tenant) || user?.roles?.includes('admin') || !!user?.adminTabs?.length)\r\n const close = () => { setIsOpen(false); setAccountOpen(false) }\r\n async function signOut() {\r\n close(); setNotice('')\r\n try { await logout() } catch { setNotice('Signed out on this device. The server could not confirm sign-out; please try again when connected.') }\r\n }\r\n useEffect(() => {\r\n const desktop = window.matchMedia('(min-width: 48rem)')\r\n const closeOnResize = () => setIsOpen(false)\r\n const closeOutside = (event: PointerEvent) => { if (!accountRef.current?.contains(event.target as Node)) setAccountOpen(false) }\r\n desktop.addEventListener('change', closeOnResize)\r\n document.addEventListener('pointerdown', closeOutside)\r\n return () => { desktop.removeEventListener('change', closeOnResize); document.removeEventListener('pointerdown', closeOutside) }\r\n }, [])\r\n return <nav className=\"starter-nav\" aria-label=\"Main navigation\" onKeyDown={event => {\r\n if (event.key === 'Escape') {\r\n if (accountOpen) { setAccountOpen(false); accountToggleRef.current?.focus() }\r\n else if (isOpen) { setIsOpen(false); toggleRef.current?.focus() }\r\n }\r\n }} onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) close() }}>\r\n <Link className=\"starter-brand\" to=\"/\" onClick={close}><BrandMark />Tilcayo</Link>\r\n <button ref={toggleRef} className=\"starter-menu-toggle\" type=\"button\" aria-label={isOpen ? 'Close navigation menu' : 'Open navigation menu'} aria-expanded={isOpen} aria-controls=\"starter-navigation-links\" onClick={() => { setIsOpen(!isOpen); setAccountOpen(false) }}>\r\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" strokeLinecap=\"round\" aria-hidden=\"true\"><path d={isOpen ? 'M6 6l12 12M6 18L18 6' : 'M4 6h16M4 12h16M4 18h16'} /></svg>\r\n </button>\r\n <div id=\"starter-navigation-links\" className={`starter-nav-panel${isOpen ? ' is-open' : ''}`}>\r\n <div className=\"starter-nav-links\">\r\n <NavLink to=\"/\" end onClick={close}>Home</NavLink>\r\n <NavLink to=\"/elements\" onClick={close}>Elements</NavLink>\r\n <NavLink to=\"/about\" onClick={close}>About</NavLink>\r\n </div>\r\n {initialized && !isAuthenticated && <div className=\"starter-nav-actions\">\r\n <NavLink to=\"/login\" onClick={close}>Log in</NavLink>\r\n <NavLink className=\"starter-get-started\" to=\"/register\" onClick={close}>Get Started</NavLink>\r\n </div>}\r\n </div>\r\n {!initialized && <div className=\"starter-account\" aria-label=\"Checking session\"><Skeleton width={38} height={38} circle /></div>}\r\n {isAuthenticated && <div className=\"starter-account\" ref={accountRef} onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) setAccountOpen(false) }}>\r\n <button ref={accountToggleRef} className=\"starter-account-toggle\" type=\"button\" aria-label=\"Open account menu\" aria-expanded={accountOpen} aria-controls={accountId} onClick={() => { setAccountOpen(!accountOpen); setIsOpen(false) }}>\r\n <span className=\"starter-account-avatar\">{user?.avatarUrl ? <img src={user.avatarUrl} alt=\"\" /> : user?.name?.trim().split(/\\s+/).slice(0, 2).map(part => part[0]).join('').toUpperCase() || 'Me'}</span><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\" /></svg>\r\n </button>\r\n {accountOpen && <div className=\"starter-account-popover\" id={accountId}>\r\n <div className=\"starter-account-summary\"><strong>{user?.name}</strong><span>{user?.email}</span></div>\r\n <NavLink to=\"/dashboard\" onClick={close}><AccountIcon name=\"dashboard\" /> My dashboard</NavLink>\r\n {user?.platformRole === 'superadmin' && !user.tenant && <><NavLink to=\"/admin\" onClick={close}><AccountIcon name=\"shield\" /> Platform administration</NavLink><NavLink to=\"/admin/workspaces\" onClick={close}><AccountIcon name=\"dashboard\" /> Tenant administration</NavLink></>}\r\n {canAdmin && !(user?.platformRole === 'superadmin' && !user.tenant) && <NavLink to=\"/admin\" onClick={close}><AccountIcon name=\"shield\" /> {user?.tenancyEnabled ? 'Workspace administration' : 'Admin'}</NavLink>}\r\n <NavLink to=\"/profile\" onClick={close}><AccountIcon name=\"user\" /> Profile &amp; settings</NavLink>\r\n <div className=\"starter-account-divider\" />\r\n <button type=\"button\" onClick={() => void signOut()} disabled={logoutStatus.isLoading}><AccountIcon name=\"logout\" /> {logoutStatus.isLoading ? 'Signing out...' : 'Sign out'}</button>\r\n </div>}\r\n </div>}\r\n {notice && <p className=\"starter-nav-notice\" role=\"status\">{notice}</p>}\r\n </nav>\r\n}\r\n",
57
+ "src/components/password-field.tsx": "import { useState } from 'react'\r\nimport { AuthInputIcon } from './auth-input-icon'\r\n\r\ntype PasswordFieldProps = {\r\n name: string\r\n label: string\r\n autoComplete: 'new-password' | 'current-password'\r\n placeholder?: string\r\n}\r\n\r\nexport function PasswordField({ name, label, autoComplete, placeholder }: PasswordFieldProps) {\r\n const [visible, setVisible] = useState(false)\r\n\r\n return (\r\n <div className=\"tl-form-group\">\r\n <label className=\"tl-label\" htmlFor={name}>{label}</label>\r\n <div className=\"auth-password auth-input-wrap\">\r\n <AuthInputIcon kind=\"lock\" />\r\n <input className=\"tl-input\" id={name} name={name} type={visible ? 'text' : 'password'} autoComplete={autoComplete} placeholder={placeholder} required minLength={8} maxLength={72} />\r\n <button className=\"auth-password-toggle\" type=\"button\" aria-label={`${visible ? 'Hide' : 'Show'} ${label.toLowerCase()}`} aria-controls={name} onClick={() => setVisible(!visible)}>\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\r\n <path d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z\" />\r\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\r\n {visible && <path d=\"m3 3 18 18\" />}\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n )\r\n}\r\n",
58
+ "src/components/profile-photo.tsx": "import { useState } from 'react'\r\nimport { FileUpload } from '@tilcayo/ui'\r\nimport { useAuth } from '../app/auth'\r\n\r\nexport function ProfilePhoto() {\r\n const { user, uploadAvatar, removeAvatar } = useAuth()\r\n const [editing, setEditing] = useState(false)\r\n const [removing, setRemoving] = useState(false)\r\n const [error, setError] = useState('')\r\n return <div className=\"profile-photo-settings\">\r\n <div className=\"profile-photo-heading\"><div><strong>Profile photo</strong><p>JPG, PNG or WebP · Up to 2 MB</p></div><button className=\"tl-btn tl-btn-outline\" type=\"button\" onClick={() => { setEditing(!editing); setError('') }} aria-expanded={editing}>{editing ? 'Close' : 'Change photo'}</button></div>\r\n {editing && <FileUpload label=\"Choose your profile photo\" description=\"JPG, PNG or WebP · Up to 2 MB\" onUpload={async (file, options) => { await uploadAvatar(file, options) }} />}\r\n {user?.avatarUrl && <button type=\"button\" className=\"tl-btn tl-btn-ghost\" disabled={removing} onClick={async () => { setRemoving(true); setError(''); try { await removeAvatar() } catch { setError('Could not remove your photo. Try again.') } finally { setRemoving(false) } }}>{removing ? 'Removing…' : 'Remove photo'}</button>}\r\n {error && <p role=\"alert\">{error}</p>}\r\n </div>\r\n}\r\n",
59
+ "src/components/session-skeleton.tsx": "import { useLocation } from 'react-router'\r\nimport { PageSkeleton } from '@tilcayo/ui'\r\nimport { SiteHeader } from './site-header'\r\n\r\nexport function SessionSkeleton() {\r\n const { pathname } = useLocation()\r\n if (/^\\/(login|register|forgot-password)(?:\\/|$)/.test(pathname)) return <PageSkeleton variant=\"auth\" label=\"Checking your session\" />\r\n return <div className=\"starter-site\"><SiteHeader /><main className=\"tl-container starter-main\"><PageSkeleton variant=\"content\" label=\"Checking your session\" /></main></div>\r\n}\r\n",
60
+ "src/components/site-header.tsx": "import { useEffect, useState } from 'react'\r\nimport { useLocation } from 'react-router'\r\nimport { Navigation } from './navigation'\r\n\r\nexport function SiteHeader() {\r\n const { pathname } = useLocation()\r\n const [scrolled, setScrolled] = useState(false)\r\n useEffect(() => { window.scrollTo(0, 0) }, [pathname])\r\n useEffect(() => {\r\n const update = () => setScrolled(window.scrollY > 12)\r\n update()\r\n window.addEventListener('scroll', update, { passive: true })\r\n return () => window.removeEventListener('scroll', update)\r\n }, [])\r\n return <header className={`starter-header${scrolled ? ' is-scrolled' : ''}`}><Navigation /></header>\r\n}\r\n",
61
+ "src/components/starter-visuals.tsx": "type IconName = 'layers' | 'cube' | 'cursor' | 'code' | 'shield' | 'terminal'\r\n\r\nexport function BrandMark() {\r\n return <svg className=\"starter-brand-mark\" viewBox=\"0 0 32 32\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M3 4h12l-5 7H3l5-7Zm14 0h12L10 28H0L17 4Z\" /></svg>\r\n}\r\nexport function ArrowIcon({ diagonal = false }: { diagonal?: boolean }) {\r\n return <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d={diagonal ? 'M6 18 18 6M6 6h12v12' : 'M4 12h16m-6-6 6 6-6 6'} /></svg>\r\n}\r\nexport function StarterIcon({ name }: { name: IconName }) {\r\n const paths: Record<IconName, string> = {\r\n layers: 'm12 3 10 6-10 6L2 9Zm-9 10 9 5 9-5M3 17l9 5 9-5',\r\n cube: 'm12 2 10 6v10l-10 5L2 18V8Zm0 11v10M2 8l10 5L22 8',\r\n cursor: 'm8 7 11 5-5 2-2 5ZM3 3l2 2m5-5v3M0 10h3m13-7-2 2M3 16l2-2m12 3 4 4',\r\n code: 'm8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18',\r\n terminal: 'm4 5 7 7-7 7m10 0h7',\r\n shield: 'm12 2 9 4v7c0 5-9 9-9 9s-9-4-9-9V6Z',\r\n }\r\n return <span className=\"starter-feature-icon\" aria-hidden=\"true\"><svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d={paths[name]} /></svg></span>\r\n}\r\n",
62
+ "src/components/toaster.tsx": "import { useEffect, useSyncExternalStore } from 'react'\r\nimport { toast, toastStore } from '../lib/toast'\r\n\r\ntype ToasterProps = {\r\n /** Milliseconds before dismissal. Set to 0 to dismiss manually. */\r\n duration?: number\r\n position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\r\n compact?: boolean\r\n className?: string\r\n}\r\n\r\nexport function Toaster({ duration = 4000, position = 'bottom-right', compact = true, className = '' }: ToasterProps) {\r\n const notification = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot, () => null)\r\n\r\n useEffect(() => {\r\n if (!notification || duration <= 0 || !Number.isFinite(duration)) return\r\n const timer = setTimeout(toast.dismiss, duration)\r\n return () => clearTimeout(timer)\r\n }, [notification, duration])\r\n\r\n return (\r\n <aside className={`starter-toaster ${className}`} data-position={position} data-compact={compact || undefined} aria-label=\"Notifications\">\r\n <div role=\"status\" aria-atomic=\"true\">\r\n {notification && <div className={`starter-toast starter-toast-${notification.kind}`}>\r\n <svg className=\"starter-toast-icon\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d={notification.kind === 'success' ? 'm8 12 3 3 5-6' : notification.kind === 'error' ? 'm9 9 6 6m0-6-6 6' : 'M12 7v6m0 4h.01'} /></svg>\r\n <p>{notification.message}</p>\r\n <button type=\"button\" aria-label=\"Dismiss notification\" onClick={toast.dismiss}>\r\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"m6 6 12 12M6 18 18 6\" /></svg>\r\n </button>\r\n </div>}\r\n </div>\r\n </aside>\r\n )\r\n}\r\n",
63
+ "src/features/auth/auth.routes.tsx": "import type { RouteObject } from 'react-router'\r\nimport { AuthLayout } from '../../layouts/auth-layout'\r\nimport { RequireAuth, GuestOnly } from '../../middleware/auth'\r\nimport { LoginPage } from '../../pages/login.page'\r\nimport { RegisterPage } from '../../pages/register.page'\r\nimport { ForgotPasswordPage } from '../../pages/forgot-password.page'\r\nimport { DashboardPage } from '../../pages/dashboard.page'\r\nimport { ProfilePage } from '../../pages/profile.page'\r\nimport { ForbiddenPage } from '../../pages/forbidden.page'\r\n\r\nexport const authRoutes: RouteObject[] = [\r\n { element: <GuestOnly />, children: [{ element: <AuthLayout />, children: [\r\n { path: '/login', element: <LoginPage /> },\r\n { path: '/register', element: <RegisterPage /> },\r\n { path: '/forgot-password', element: <ForgotPasswordPage /> },\r\n ] }] },\r\n { element: <RequireAuth />, children: [\r\n { path: '/dashboard', element: <DashboardPage /> },\r\n { path: '/profile', element: <ProfilePage /> },\r\n ] },\r\n { path: '/forbidden', element: <ForbiddenPage /> },\r\n]\r\n",
64
+ "src/layouts/admin-layout.tsx": "import { NavLink, Outlet } from 'react-router'\n\nexport function AdminLayout() {\n return <section className=\"tl-stack\">\n <nav aria-label=\"Admin\"><NavLink to=\"/admin\">Dashboard</NavLink>{' '}<NavLink to=\"/admin/profile\">Profile</NavLink></nav>\n <Outlet />\n </section>\n}\n",
65
+ "src/layouts/app-layout.tsx": "import { Outlet } from 'react-router'\r\nimport { SiteHeader } from '../components/site-header'\r\nimport { Toaster } from '../components/toaster'\r\n\r\nexport function AppLayout() {\r\n return (\r\n <div className=\"starter-site\">\r\n <a className=\"starter-skip\" href=\"#main\">Skip to content</a>\r\n <SiteHeader />\r\n <main id=\"main\" className=\"tl-container starter-main\" tabIndex={-1}>\r\n <Outlet />\r\n </main>\r\n <Toaster />\r\n </div>\r\n )\r\n}\r\n",
66
+ "src/layouts/auth-layout.tsx": "import { useId } from 'react'\r\nimport { Link, Outlet, useLocation } from 'react-router'\r\nimport { BrandMark } from '../components/starter-visuals'\r\nimport { authArchitectureImage } from '../lib/starter-images'\r\n\r\nexport function AuthLayout() {\r\n const { pathname } = useLocation()\r\n const clipId = useId()\r\n return <div className=\"auth-page\"><section className={`auth-scene${pathname === '/register' ? ' auth-scene-register' : ''}`} aria-label=\"Authentication\">\r\n <div className=\"auth-scene-content\">\r\n <div className=\"auth-topbar\"><Link className=\"starter-brand auth-brand\" to=\"/\" aria-label=\"Tilcayo home\"><BrandMark />Tilcayo</Link></div>\r\n <div className=\"auth-scene-form\"><Outlet /></div>\r\n </div>\r\n <div className=\"auth-scene-art\" aria-hidden=\"true\" style={{ clipPath: `url(#${clipId})` }}>\r\n <svg width=\"0\" height=\"0\" focusable=\"false\"><defs><clipPath id={clipId} clipPathUnits=\"objectBoundingBox\"><path d=\"M.72 0H.97Q1 0 1 .03V.97Q1 1 .97 1H.21C.025 1-.07 .95 .045 .78L.59 .08Q.65 0 .72 0Z\" /></clipPath></defs></svg>\r\n <img src={authArchitectureImage} alt=\"\" />\r\n </div>\r\n </section></div>\r\n}\r\n",
67
+ "src/lib/auth-feedback.ts": "export function authErrorMessage(error: unknown): string {\r\n if (error && typeof error === 'object') {\r\n if ('kind' in error && error.kind === 'network') return 'Unable to reach the server. Please try again.'\r\n if ('message' in error && typeof error.message === 'string') return error.message\r\n }\r\n return 'Something went wrong. Please try again.'\r\n}\r\n\r\ntype ReturnUser = { roles?: readonly string[]; adminTabs?: readonly string[]; platformRole?: string; tenant?: unknown }\r\n\r\nexport function returnPath(state: unknown, user?: ReturnUser | null): string {\r\n const fallback = '/dashboard'\r\n const from = state && typeof state === 'object' && 'from' in state ? state.from : undefined\r\n if (typeof from !== 'string' || !from.startsWith('/') || from.startsWith('//') || from.includes('\\\\')) return fallback\r\n let url: URL, path: string\r\n try {\r\n url = new URL(from, 'http://tilcayo.local')\r\n if (url.origin !== 'http://tilcayo.local') return fallback\r\n path = (decodeURIComponent(url.pathname).replace(/\\/+$/, '') || '/').toLowerCase()\r\n } catch { return fallback }\r\n if (path.includes('\\\\') || path.startsWith('//') || /^\\/(login|register|forgot-password|forbidden)(\\/|$)/.test(path)) return fallback\r\n if (path === '/admin' || path.startsWith('/admin/')) {\r\n const platform = user?.platformRole === 'superadmin' && !user.tenant\r\n const administrator = !!user?.roles?.includes('admin')\r\n const tabs = user?.adminTabs ?? []\r\n const can = (tab: string) => tab === 'tenants' ? platform : administrator || tabs.includes(tab)\r\n if (!platform && !administrator && !tabs.length) return fallback\r\n const section = url.searchParams.get('section')\r\n let required: string | undefined\r\n if ((path === '/admin' || path === '/admin/dashboard') && section) required = section === 'users' ? 'users' : section === 'inventory' ? 'inventory' : 'overview'\r\n if (path === '/admin/users') required = 'users'\r\n if (path === '/admin/workspaces') required = 'tenants'\r\n if (path === '/admin/access') required = 'access'\r\n if (path === '/admin/trash') required = 'trash'\r\n if (path === '/admin/settings' && section) required = section === 'tenants' ? 'tenants' : section === 'users' ? 'users' : section === 'trash' ? 'trash' : 'access'\r\n if (path === '/admin/products') {\r\n const productTabs = ['catalog', 'inventory', 'categories', 'orders']\r\n if (!section) { if (!productTabs.some(can)) return fallback }\r\n else required = productTabs.includes(section) ? section : 'catalog'\r\n }\r\n if (required && !can(required)) return fallback\r\n }\r\n return from\r\n}\r\n",
68
+ "src/lib/starter-images.ts": "export const architectureImage = '/tilcayo-architecture.webp'\r\nexport const authArchitectureImage = '/tilcayo-auth-architecture.webp'\r\n",
69
+ "src/lib/toast.ts": "type Toast = { message: string; kind: 'success' | 'error' | 'warning' | 'info' }\n\nlet current: Toast | null = null\nconst listeners = new Set<() => void>()\n\nfunction update(value: Toast | null) {\n current = value\n listeners.forEach(listener => listener())\n}\n\nexport const toast = {\n success: (message: string) => update({ message, kind: 'success' }),\n error: (message: string) => update({ message, kind: 'error' }),\n warning: (message: string) => update({ message, kind: 'warning' }),\n info: (message: string) => update({ message, kind: 'info' }),\n dismiss: () => update(null),\n}\n\nexport const toastStore = {\n getSnapshot: () => current,\n subscribe(listener: () => void) {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n}\n",
70
+ "src/middleware/auth.tsx": "import { SessionSkeleton } from '../components/session-skeleton'\r\nimport { Navigate, Outlet, useLocation } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { returnPath } from '../lib/auth-feedback'\r\n\r\n// These guards control navigation. The API must still authorize every request.\r\nexport function RequireAuth({ role }: { role?: string }) {\r\n const { initialized, isAuthenticated, user } = useAuth()\r\n const location = useLocation()\r\n if (!initialized) return <SessionSkeleton />\r\n if (!isAuthenticated) return <Navigate to=\"/login\" replace state={{ from: location.pathname + location.search + location.hash }} />\r\n if (role && !user?.roles?.includes(role)) return <Navigate to=\"/forbidden\" replace />\r\n return <Outlet />\r\n}\r\n\r\nexport const ProtectedRoute = RequireAuth\r\n\r\nexport function GuestOnly() {\r\n const { initialized, isAuthenticated, user } = useAuth()\r\n const location = useLocation()\r\n if (!initialized) return <SessionSkeleton />\r\n return isAuthenticated ? <Navigate to={returnPath(location.state, user)} replace /> : <Outlet />\r\n}\r\n",
71
+ "src/pages/about.page.tsx": "import { useId } from 'react'\r\nimport { Link } from 'react-router'\r\nimport { ArrowIcon, StarterIcon } from '../components/starter-visuals'\r\nimport { authArchitectureImage } from '../lib/starter-images'\r\n\r\nexport function AboutPage() {\r\n const clipId = useId()\r\n return <>\r\n <section className=\"starter-hero starter-hero-about\">\r\n <div className=\"starter-hero-copy\">\r\n <p className=\"starter-eyebrow\">About Tilcayo</p>\r\n <h1>A TypeScript framework for modern web apps.</h1>\r\n <p className=\"starter-lead\">Tilcayo brings together an API, a clean architecture, and developer tools to help you build, secure, and ship applications faster.</p>\r\n <div className=\"starter-actions\">\r\n <Link className=\"tl-btn tl-btn-primary\" to=\"/elements\">Get Started <ArrowIcon /></Link>\r\n <a className=\"tl-btn tl-btn-outline\" href=\"https://github.com/jomobrain1/tilcayo\" target=\"_blank\" rel=\"noreferrer\">View on GitHub <ArrowIcon diagonal /></a>\r\n </div>\r\n </div>\r\n <div className=\"starter-hero-media starter-about-art\" style={{ clipPath: `url(#${clipId})` }}>\r\n <svg width=\"0\" height=\"0\" aria-hidden=\"true\" focusable=\"false\"><defs><clipPath id={clipId} clipPathUnits=\"objectBoundingBox\"><path d=\"M.63 0H1V.95H.21C.025.95-.07.91.045.75Z\" /></clipPath></defs></svg>\r\n <img src={authArchitectureImage} alt=\"Contemporary dark building with a sharp roofline and warm interior lighting\" />\r\n </div>\r\n </section>\r\n <div className=\"starter-feature-strip\">\r\n <section><StarterIcon name=\"code\" /><div><h2>Type-safe API</h2><p>Express + TypeScript with Zod validation.</p></div></section>\r\n <section><StarterIcon name=\"shield\" /><div><h2>Authentication</h2><p>JWT with protected routes and roles.</p></div></section>\r\n <section><StarterIcon name=\"layers\" /><div><h2>CLI &amp; Scaffolding</h2><p>Generate modules, controllers, and routes.</p></div></section>\r\n </div>\r\n </>\r\n}\r\n",
72
+ "src/pages/dashboard.page.tsx": "import { Link } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { AccountIcon } from '../components/account-icon'\r\n\r\nexport function DashboardPage() {\r\n const { user } = useAuth()\r\n const firstName = user?.name.trim().split(/\\s+/)[0] || 'there'\r\n const role = user?.platformRole === 'superadmin' && !user?.tenant ? 'Super Admin' : user?.roles?.includes('admin') ? 'Administrator' : user?.adminTabs?.length ? 'Staff' : 'Member'\r\n const joined = user?.createdAt && Number.isFinite(Date.parse(user.createdAt))\r\n ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(new Date(user.createdAt)) : 'Not available'\r\n return <section className=\"account-dashboard user-dashboard\" aria-labelledby=\"dashboard-title\">\r\n <header className=\"account-page-heading\"><h1 id=\"dashboard-title\">Dashboard</h1><p>Welcome back, {firstName}. Here’s your workspace at a glance.</p></header>\r\n <div className=\"user-dashboard-summary\">\r\n <section className=\"account-card user-summary-card\"><span className=\"account-icon-tile\"><AccountIcon name=\"user\" /></span><div><p>Account type</p><h2>{role}</h2></div></section>\r\n <section className=\"account-card user-summary-card\"><span className=\"account-icon-tile\"><AccountIcon name=\"calendar\" /></span><div><p>Member since</p><h2>{joined}</h2></div></section>\r\n <section className=\"account-card user-summary-card\"><span className=\"account-icon-tile\"><AccountIcon name=\"shield\" /></span><div><p>Current session</p><h2><span className=\"user-session-dot\" /> Signed in</h2></div></section>\r\n </div>\r\n <div className=\"user-dashboard-grid\">\r\n <div className=\"account-column\">\r\n <section className=\"account-card user-workspace\"><div className=\"user-card-heading\"><h2>Your workspace</h2><p>Pick up where you left off.</p></div>\r\n <div className=\"user-workspace-links\">\r\n <Link to=\"/elements\"><span className=\"account-icon-tile\"><AccountIcon name=\"dashboard\" /></span><div><h3>Explore elements</h3><p>Find the building blocks for your next idea.</p></div><AccountIcon name=\"arrow\" /></Link>\r\n <Link to=\"/profile\"><span className=\"account-icon-tile\"><AccountIcon name=\"user\" /></span><div><h3>Profile &amp; settings</h3><p>Update your details and account preferences.</p></div><AccountIcon name=\"arrow\" /></Link>\r\n <Link to=\"/profile#account-security\"><span className=\"account-icon-tile\"><AccountIcon name=\"lock\" /></span><div><h3>Account security</h3><p>Review your sign-in and password options.</p></div><AccountIcon name=\"arrow\" /></Link>\r\n </div>\r\n </section>\r\n <section className=\"account-card user-getting-started\"><span className=\"account-icon-tile\"><AccountIcon name=\"globe\" /></span><div><h2>Get to know Tilcayo</h2><p>Learn more about the toolkit behind your workspace.</p><Link to=\"/about\">About Tilcayo <AccountIcon name=\"arrow\" /></Link></div></section>\r\n </div>\r\n <aside className=\"account-column\" aria-label=\"Account overview\">\r\n <section className=\"account-card user-account-snapshot\"><h2>Account overview</h2><span className=\"account-avatar\" aria-hidden=\"true\">{user?.avatarUrl ? <img src={user.avatarUrl} alt=\"\" /> : user?.name.trim().split(/\\s+/).slice(0,2).map(part => part[0]).join('').toUpperCase() || 'Me'}</span><h3>{user?.name}</h3><p>{user?.email}</p><span className=\"account-role\">{role}</span><Link className=\"tl-btn tl-btn-outline\" to=\"/profile\">View profile &amp; settings <AccountIcon name=\"arrow\" /></Link></section>\r\n <section className=\"account-card user-dashboard-note\"><span className=\"account-icon-tile\"><AccountIcon name=\"shield\" /></span><div><h2>You’re in control</h2><p>Your profile, password reset, and browser sign-out are together in Profile &amp; settings.</p></div></section>\r\n </aside>\r\n </div>\r\n </section>\r\n}\r\n",
73
+ "src/pages/elements.page.tsx": "import { PageSkeleton } from '@tilcayo/ui'\r\nimport { lazy, Suspense } from 'react'\r\n\r\nconst ElementsLibrary = lazy(() => import('../components/elements/elements-library'))\r\n\r\nexport function ElementsPage() {\r\n return <Suspense fallback={<PageSkeleton variant=\"content\" label=\"Loading components\" />}><ElementsLibrary /></Suspense>\r\n}\r\n",
74
+ "src/pages/forbidden.page.tsx": "import { useAuth } from '../app/auth'\r\nimport { ErrorPage } from '../components/error-page'\r\n\r\nexport function ForbiddenPage() {\r\n const { isAuthenticated } = useAuth()\r\n return <ErrorPage code=\"403\" title=\"Access denied\" description={isAuthenticated ? \"You're signed in, but this page needs permissions your account doesn't have. Your dashboard is still available.\" : 'Sign in with an account that has permission to view this page, or return to the home page.'} primary={{ to: isAuthenticated ? '/dashboard' : '/login', label: isAuthenticated ? 'My dashboard' : 'Log in' }} secondary={{ to: '/', label: 'Back to home' }} note=\"Need access? Contact your administrator for help.\" />\r\n}\r\n",
75
+ "src/pages/forgot-password.page.tsx": "import { useState, type FormEvent } from 'react'\r\nimport { Link, useNavigate } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { AuthInputIcon } from '../components/auth-input-icon'\r\nimport { PasswordField } from '../components/password-field'\r\nimport { authErrorMessage } from '../lib/auth-feedback'\r\nimport { toast } from '../lib/toast'\r\n\r\nexport function ForgotPasswordPage() {\r\n const { forgotPassword, verifyResetCode, resetPassword } = useAuth()\r\n const navigate = useNavigate()\r\n const [step, setStep] = useState<'email' | 'code' | 'password'>('email')\r\n const [email, setEmail] = useState('')\r\n const [resetToken, setResetToken] = useState('')\r\n const [pending, setPending] = useState(false)\r\n const [error, setError] = useState('')\r\n const [message, setMessage] = useState('')\r\n\r\n async function submit(event: FormEvent<HTMLFormElement>) {\r\n event.preventDefault()\r\n if (pending) return\r\n const data = new FormData(event.currentTarget)\r\n setError('')\r\n setPending(true)\r\n try {\r\n if (step === 'email') {\r\n const address = String(data.get('email') ?? '').trim().toLowerCase()\r\n const response = await forgotPassword({ email: address })\r\n setEmail(address)\r\n setMessage(response.message)\r\n setStep('code')\r\n } else if (step === 'code') {\r\n const response = await verifyResetCode({ email, code: String(data.get('code') ?? '').trim() })\r\n setResetToken(response.data.resetToken)\r\n setMessage('Choose your new password.')\r\n setStep('password')\r\n } else {\r\n const password = String(data.get('password') ?? '')\r\n if (password !== data.get('confirmPassword')) {\r\n setError('Passwords do not match.')\r\n return\r\n }\r\n await resetPassword({ resetToken, password })\r\n setResetToken('')\r\n toast.success('Password reset.')\r\n navigate('/login', { replace: true })\r\n }\r\n } catch (failure) { setError(authErrorMessage(failure)) }\r\n finally { setPending(false) }\r\n }\r\n\r\n return (\r\n <section className=\"auth-panel auth-reset-form tl-stack\" aria-labelledby=\"reset-title\">\r\n <header className=\"auth-heading\">\r\n <h1 id=\"reset-title\" className=\"tl-heading-2\">{step === 'email' ? 'Forgot password?' : step === 'code' ? 'Check your email' : 'Reset your password'}</h1>\r\n <p>{step === 'email' ? 'Get back into your Tilcayo account' : message}</p>\r\n </header>\r\n <form onSubmit={submit} aria-busy={pending} className=\"tl-stack\">\r\n {error && <p className=\"tl-alert tl-alert-danger\" role=\"alert\">{error}</p>}\r\n <fieldset disabled={pending} className=\"auth-fields tl-stack\">\r\n {step === 'email' && <div className=\"tl-form-group\">\r\n <label className=\"tl-label\" htmlFor=\"reset-email\">Email</label>\r\n <div className=\"auth-input-wrap\"><AuthInputIcon kind=\"email\" /><input className=\"tl-input\" id=\"reset-email\" name=\"email\" type=\"email\" autoComplete=\"email\" placeholder=\"Enter your email\" defaultValue={email} required maxLength={254} /></div>\r\n </div>}\r\n {step === 'code' && <div className=\"tl-form-group\">\r\n <label className=\"tl-label\" htmlFor=\"reset-code\">Six-digit code</label>\r\n <input className=\"tl-input\" id=\"reset-code\" placeholder=\"Six-digit code\" name=\"code\" type=\"text\" inputMode=\"numeric\" autoComplete=\"one-time-code\" pattern=\"[0-9]{6}\" minLength={6} maxLength={6} required />\r\n <p className=\"tl-text-muted\">The code expires in 10 minutes. You have five attempts. Wait 60 seconds before requesting another code.</p>\r\n </div>}\r\n {step === 'password' && <>\r\n <PasswordField name=\"password\" label=\"New password\" placeholder=\"New password\" autoComplete=\"new-password\" />\r\n <PasswordField name=\"confirmPassword\" label=\"Confirm password\" placeholder=\"Confirm password\" autoComplete=\"new-password\" />\r\n </>}\r\n <button className=\"tl-btn tl-btn-primary auth-submit\" type=\"submit\">{pending ? 'Please wait...' : step === 'email' ? 'Send reset code' : step === 'code' ? 'Verify code' : 'Reset password'}{!pending && <span aria-hidden=\"true\">&#8594;</span>}</button>\r\n {step !== 'email' && <button className=\"tl-btn\" type=\"button\" onClick={() => { setStep('email'); setResetToken(''); setError(''); setMessage('') }}>Request a new code</button>}\r\n </fieldset>\r\n </form>\r\n <div className=\"auth-alternate\"><p className=\"auth-alternate-label\">Remember your password?</p><Link className=\"tl-btn tl-btn-outline auth-alternate-link\" to=\"/login\"><span aria-hidden=\"true\">&#8592;</span> Back to login</Link></div>\r\n </section>\r\n )\r\n}\r\n",
76
+ "src/pages/home.page.tsx": "import { Link } from 'react-router'\r\nimport { ArrowIcon, StarterIcon } from '../components/starter-visuals'\r\nimport { CodePreview } from '../components/code-preview'\r\n\r\nexport function HomePage() {\r\n return <>\r\n <section className=\"starter-hero starter-toolkit-hero\">\r\n <div className=\"starter-hero-copy\">\r\n <p className=\"starter-eyebrow\">The full-stack TypeScript toolkit</p>\r\n <h1>Build full-stack<br className=\"starter-hero-break\" /> apps with Tilcayo.</h1>\r\n <p className=\"starter-lead\">A modern TypeScript toolkit and framework for building, shipping, and scaling full-stack applications.</p>\r\n <div className=\"starter-actions\">\r\n <Link className=\"tl-btn tl-btn-primary\" to=\"/elements\">Get Started <ArrowIcon /></Link>\r\n <a className=\"tl-btn tl-btn-outline\" href=\"https://github.com/jomobrain1/tilcayo#readme\" target=\"_blank\" rel=\"noreferrer\">View Docs</a>\r\n </div>\r\n </div>\r\n <CodePreview />\r\n </section>\r\n <div className=\"starter-toolkit-features\">\r\n <section className=\"starter-toolkit-feature\"><StarterIcon name=\"code\" /><div><h2>Type-safe APIs</h2><p>Build typed APIs with validation and predictable responses.</p></div></section>\r\n <section className=\"starter-toolkit-feature\"><StarterIcon name=\"shield\" /><div><h2>Authentication ready</h2><p>Protected routes, roles, and session handling built in.</p></div></section>\r\n <section className=\"starter-toolkit-feature\"><StarterIcon name=\"terminal\" /><div><h2>CLI scaffolding</h2><p>Create an app and generate resources with the Tilcayo CLI.</p></div></section>\r\n </div>\r\n </>\r\n}\r\n",
77
+ "src/pages/login.page.tsx": "import { AuthForm } from '../components/auth-form'\n\nexport function LoginPage() { return <AuthForm mode=\"login\" /> }\n",
78
+ "src/pages/not-found.page.tsx": "import { ErrorPage } from '../components/error-page'\r\n\r\nexport function NotFoundPage() {\r\n return <ErrorPage code=\"404\" title=\"Page not found\" description=\"We couldn't find the page you're looking for. It may have moved, or the address might be incorrect.\" primary={{ to: '/', label: 'Back to home' }} secondary={{ to: '/elements', label: 'Explore elements' }} note=\"Check the address, or use the navigation to find your way.\" />\r\n}\r\n",
79
+ "src/pages/profile.page.tsx": "import { ProfilePhoto } from '../components/profile-photo'\r\nimport { useEffect, useState, type FormEvent } from 'react'\r\nimport { useLocation, useNavigate } from 'react-router'\r\nimport { useAuth } from '../app/auth'\r\nimport { AccountIcon } from '../components/account-icon'\r\nimport { authErrorMessage } from '../lib/auth-feedback'\r\nimport { toast } from '../lib/toast'\r\n\r\nexport function ProfilePage() {\r\n const { user, updateProfile, updateProfileStatus, logout, logoutStatus } = useAuth()\r\n const navigate = useNavigate()\r\n const { hash } = useLocation()\r\n useEffect(() => {\r\n if (hash !== '#account-security' && hash !== '#account-details') return\r\n const frame = requestAnimationFrame(() => document.getElementById(hash.slice(1))?.scrollIntoView({ block: 'start' }))\r\n return () => cancelAnimationFrame(frame)\r\n }, [hash])\r\n const [editing, setEditing] = useState(false)\r\n const [resetting, setResetting] = useState(false)\r\n const [error, setError] = useState('')\r\n const [locale, setLocale] = useState(() => {\r\n try { return localStorage.getItem('tilcayo-date-format') === 'en-GB' ? 'en-GB' : 'en-US' } catch { return 'en-US' }\r\n })\r\n const initials = user?.name.trim().split(/\\s+/).slice(0, 2).map(part => part[0]).join('').toUpperCase() || 'Me'\r\n const role = user?.platformRole === 'superadmin' && !user?.tenant ? 'Super Admin' : user?.roles?.includes('admin') ? 'Administrator' : user?.adminTabs?.length ? 'Staff' : 'Member'\r\n const date = (value?: string) => value && Number.isFinite(Date.parse(value)) ? new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(value)) : 'Not available'\r\n async function saveProfile(event: FormEvent<HTMLFormElement>) {\r\n event.preventDefault()\r\n setError('')\r\n try {\r\n await updateProfile({ name: String(new FormData(event.currentTarget).get('name') ?? '').trim() })\r\n setEditing(false)\r\n toast.success('Profile updated.')\r\n } catch (failure) { setError(authErrorMessage(failure)) }\r\n }\r\n async function signOut(reset = false) {\r\n try { await logout() } catch { toast.warning('Signed out on this browser. The server could not confirm sign-out.') }\r\n navigate(reset ? '/forgot-password' : '/login')\r\n }\r\n return <section className=\"account-dashboard\" aria-labelledby=\"account-title\">\r\n <header className=\"account-page-heading\"><h1 id=\"account-title\">Profile &amp; settings</h1><p>Manage your profile, security, and activity.</p></header>\r\n <div className=\"account-grid\">\r\n <div className=\"account-column\">\r\n <section className=\"account-card account-identity\" aria-label=\"Your profile\">\r\n <span className=\"account-avatar\" aria-hidden=\"true\">{user?.avatarUrl ? <img src={user.avatarUrl} alt=\"\" /> : initials}</span>\r\n <div className=\"account-identity-copy\"><h2>{user?.name}</h2><p>{user?.email}</p><span className=\"account-role\">{role}</span></div>\r\n <button className=\"tl-btn tl-btn-primary account-edit\" onClick={() => { setEditing(!editing); setError('') }} aria-expanded={editing} aria-controls=\"account-edit-form\"><AccountIcon name=\"edit\" /> Edit profile</button>\r\n {editing && <form id=\"account-edit-form\" className=\"account-edit-form\" onSubmit={saveProfile}>\r\n <div className=\"tl-form-group\"><label className=\"tl-label\" htmlFor=\"account-name\">Display name</label><input className=\"tl-input\" id=\"account-name\" name=\"name\" defaultValue={user?.name} required minLength={2} maxLength={100} autoComplete=\"name\" autoFocus /></div>\r\n {error && <p className=\"tl-alert tl-alert-danger\" role=\"alert\">{error}</p>}\r\n <div className=\"account-form-actions\"><button className=\"tl-btn tl-btn-outline\" type=\"button\" onClick={() => setEditing(false)}>Cancel</button><button className=\"tl-btn tl-btn-primary\" disabled={updateProfileStatus.isLoading}>{updateProfileStatus.isLoading ? 'Saving...' : 'Save changes'}</button></div>\r\n </form>}\r\n <ProfilePhoto />\r\n </section>\r\n <section className=\"account-card\" id=\"account-details\"><h2>Account details</h2>\r\n <dl className=\"account-details\">\r\n <div><dt><AccountIcon name=\"mail\" /> Email</dt><dd>{user?.email}</dd></div>\r\n <div><dt><AccountIcon name=\"user\" /> Display name</dt><dd>{user?.name}</dd></div>\r\n <div><dt><AccountIcon name=\"calendar\" /> Joined</dt><dd>{date(user?.createdAt)}</dd></div>\r\n </dl>\r\n </section>\r\n <section className=\"account-card\" id=\"account-security\"><h2>Security</h2>\r\n <div className=\"account-security-row\"><span><AccountIcon name=\"lock\" /> Password</span><button onClick={() => setResetting(!resetting)} aria-expanded={resetting} aria-controls=\"account-reset\">Reset password <AccountIcon name=\"arrow\" /></button></div>\r\n {resetting && <div id=\"account-reset\" className=\"account-reset-note\"><p>Continue to sign out and reset your password by email.</p><button className=\"tl-btn tl-btn-outline\" disabled={logoutStatus.isLoading} onClick={() => void signOut(true)}>Continue to password reset</button></div>}\r\n <div className=\"account-security-row\"><span><AccountIcon name=\"shield\" /> Access level</span><span className=\"account-role\">{role}</span></div>\r\n <div className=\"account-security-row\"><span><AccountIcon name=\"monitor\" /> This browser</span><button disabled={logoutStatus.isLoading} onClick={() => void signOut()}>Sign out <AccountIcon name=\"arrow\" /></button></div>\r\n </section>\r\n </div>\r\n <div className=\"account-column\">\r\n <section className=\"account-card\"><h2>Quick actions</h2><div className=\"account-quick-actions\">\r\n <button onClick={() => { setEditing(true); setError('') }}><span className=\"account-icon-tile\"><AccountIcon name=\"edit\" /></span><span>Edit profile</span><AccountIcon name=\"arrow\" /></button>\r\n <a href=\"#account-security\" onClick={() => setResetting(true)}><span className=\"account-icon-tile\"><AccountIcon name=\"lock\" /></span><span>Reset password</span><AccountIcon name=\"arrow\" /></a>\r\n <a href=\"#account-security\"><span className=\"account-icon-tile\"><AccountIcon name=\"monitor\" /></span><span>Review security</span><AccountIcon name=\"arrow\" /></a>\r\n </div></section>\r\n <section className=\"account-card\"><h2>Account activity</h2><div className=\"account-activity\">\r\n <div><span className=\"account-icon-tile\"><AccountIcon name=\"monitor\" /></span><p><strong>Current session</strong><span>Signed in on this browser</span></p></div>\r\n {user?.updatedAt && <div><span className=\"account-icon-tile\"><AccountIcon name=\"edit\" /></span><p><strong>Account last updated</strong><span>{date(user.updatedAt)}</span></p></div>}\r\n {user?.createdAt && <div><span className=\"account-icon-tile\"><AccountIcon name=\"user\" /></span><p><strong>Account created</strong><span>{date(user.createdAt)}</span></p></div>}\r\n </div></section>\r\n <section className=\"account-card\"><h2>Preferences</h2><div className=\"account-preference\"><label htmlFor=\"account-date-format\"><AccountIcon name=\"globe\" /> Date format</label><select id=\"account-date-format\" className=\"tl-select\" value={locale} onChange={event => { setLocale(event.target.value); try { localStorage.setItem('tilcayo-date-format', event.target.value) } catch { /* Preference works for this visit. */ } }}><option value=\"en-US\">English (US)</option><option value=\"en-GB\">English (UK)</option></select></div><p className=\"account-preference-note\">Used for dates on this profile page.</p></section>\r\n </div>\r\n </div>\r\n </section>\r\n}\r\n",
80
+ "src/pages/register.page.tsx": "import { AuthForm } from '../components/auth-form'\n\nexport function RegisterPage() { return <AuthForm mode=\"register\" /> }\n",
81
+ "src/routes.tsx": "import type { RouteObject } from 'react-router'\r\nimport { AppLayout } from './layouts/app-layout'\r\nimport { HomePage } from './pages/home.page'\r\nimport { ElementsPage } from './pages/elements.page'\r\nimport { AboutPage } from './pages/about.page'\r\nimport { NotFoundPage } from './pages/not-found.page'\r\nimport { authRoutes } from './features/auth/auth.routes'\n\r\nexport const routes: RouteObject[] = [\r\n {\r\n element: <AppLayout />,\r\n children: [\r\n { path: '/', element: <HomePage /> },\r\n { path: '/elements', element: <ElementsPage /> },\r\n { path: '/about', element: <AboutPage /> },\r\n ...authRoutes,\n { path: '*', element: <NotFoundPage /> },\r\n ],\r\n },\r\n]\r\n",
82
+ "vite.config.ts": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [react()],\n resolve: { dedupe: ['react', 'react-dom', 'react-redux', '@reduxjs/toolkit', 'react-router'] },\n server: { proxy: { '/api': 'http://127.0.0.1:9149' } },\n})\n"
83
+ }