litestar-vite-plugin 0.18.4 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,29 @@
1
1
  # Litestar Vite
2
2
 
3
- Litestar Vite connects the Litestar backend to a Vite toolchain. It supports SPA, Template, and Inertia flows, and can proxy Vite dev traffic through your ASGI port or run Vite directly.
3
+ Litestar Vite connects the Litestar backend to a Vite toolchain. It supports SPA, Template, Inertia, and meta-framework workflows, and it can proxy Vite dev traffic through your ASGI port or run Vite directly.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - One-port dev: proxies Vite HTTP + WS/HMR through Litestar by default; switch to two-port with `VITE_PROXY_MODE=direct`.
8
- - SSR framework support: use `mode="ssr"` for Astro, Nuxt, SvelteKit - proxies everything except your API routes.
9
- - Production assets: reads Vite manifest from `public/manifest.json` (configurable) and serves under `asset_url`.
10
- - Type-safe frontends: optional OpenAPI/routes export + `@hey-api/openapi-ts` via the Vite plugin.
11
- - Inertia support: v2 protocol with session middleware and optional SPA mode.
8
+ - Framework-mode support: use `mode="framework"` (alias `mode="ssr"`) for Astro, Nuxt, and SvelteKit. Litestar proxies everything except your API routes.
9
+ - Production assets: reads the Vite manifest from `public/manifest.json` (configurable) and serves under `asset_url`.
10
+ - Type-safe frontends: optional OpenAPI/routes export plus `@hey-api/openapi-ts` via the Vite plugin.
11
+ - Inertia support: stable v2 protocol with session middleware, optional script-element bootstrap, and optional SSR.
12
12
 
13
- ## Quick Start (SPA)
13
+ ## Install
14
14
 
15
15
  ```bash
16
16
  pip install litestar-vite
17
+ npm install litestar-vite-plugin
17
18
  ```
18
19
 
20
+ ## Quick Start
21
+
19
22
  ```python
20
23
  import os
21
24
  from pathlib import Path
22
25
  from litestar import Litestar
23
- from litestar_vite import VitePlugin, ViteConfig, PathConfig
26
+ from litestar_vite import PathConfig, ViteConfig, VitePlugin
24
27
 
25
28
  DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
26
29
 
@@ -33,155 +36,47 @@ app = Litestar(
33
36
  ```
34
37
 
35
38
  ```bash
36
- litestar run --reload # Vite dev server is proxied automatically
37
- ```
38
-
39
- Scaffold a frontend: `litestar assets init --template vue` (or `react`, `svelte`, `htmx`, `react-inertia`, `vue-inertia`, `angular`, `astro`, `nuxt`, `sveltekit`).
40
-
41
- ## Development
42
-
43
- To contribute or run the development project:
44
-
45
- ```bash
46
- # Install all dependencies and build packages
47
- make install && make build
48
-
49
- # Install frontend dependencies for an example
50
- uv run litestar --app-dir examples/vue-inertia assets install
51
-
52
- # Run the development server
53
- uv run litestar --app-dir examples/vue-inertia run
54
- ```
55
-
56
- Replace `vue-inertia` with any other example: `vue`, `react`, `svelte`, `react-inertia`, `htmx`, `angular`, `astro`, `nuxt`, or `sveltekit`.
57
-
58
- ## Template / HTMX
59
-
60
- ```python
61
- from pathlib import Path
62
- from litestar import Litestar
63
- from litestar.contrib.jinja import JinjaTemplateEngine
64
- from litestar.template import TemplateConfig
65
- from litestar_vite import VitePlugin, ViteConfig, PathConfig
66
-
67
- here = Path(__file__).parent
68
-
69
- app = Litestar(
70
- template_config=TemplateConfig(directory=here / "templates", engine=JinjaTemplateEngine),
71
- plugins=[VitePlugin(config=ViteConfig(
72
- dev_mode=True,
73
- paths=PathConfig(root=here),
74
- ))],
75
- )
76
- ```
77
-
78
- ## Inertia (v2)
79
-
80
- Requires session middleware (32-char secret).
81
-
82
- ```python
83
- import os
84
- from pathlib import Path
85
- from litestar import Litestar
86
- from litestar.middleware.session.client_side import CookieBackendConfig
87
- from litestar_vite import VitePlugin, ViteConfig, PathConfig
88
- from litestar_vite.inertia import InertiaConfig
89
-
90
- here = Path(__file__).parent
91
- SECRET_KEY = os.environ.get("SECRET_KEY", "development-only-secret-32-chars")
92
- session = CookieBackendConfig(secret=SECRET_KEY.encode("utf-8"))
93
-
94
- app = Litestar(
95
- middleware=[session.middleware],
96
- plugins=[VitePlugin(config=ViteConfig(
97
- dev_mode=True,
98
- paths=PathConfig(root=here),
99
- inertia=InertiaConfig(root_template="index.html"),
100
- ))],
101
- )
39
+ litestar assets init --template vue
40
+ litestar assets install
41
+ litestar run --reload
102
42
  ```
103
43
 
104
- ## Meta-frameworks (Astro, Nuxt, SvelteKit)
105
-
106
- Use `mode="ssr"` (or `mode="framework"`) to proxy non-API routes to the framework's dev server:
107
-
108
- ```python
109
- import os
110
- from pathlib import Path
111
- from litestar import Litestar
112
- from litestar_vite import VitePlugin, ViteConfig, PathConfig
113
-
114
- here = Path(__file__).parent
115
- DEV_MODE = os.getenv("VITE_DEV_MODE", "true").lower() in ("true", "1", "yes")
116
-
117
- app = Litestar(
118
- plugins=[
119
- VitePlugin(config=ViteConfig(
120
- mode="ssr",
121
- dev_mode=DEV_MODE,
122
- paths=PathConfig(root=here),
123
- ))
124
- ],
125
- )
126
- ```
44
+ ## Documentation
127
45
 
128
- ### Proxy Modes
46
+ - **[Usage Guide](https://litestar-org.github.io/litestar-vite/latest/usage/)**: Core concepts, configuration, and type generation.
47
+ - **[Inertia](https://litestar-org.github.io/litestar-vite/latest/inertia/)**: Fullstack protocols and SSR.
48
+ - **[Frameworks](https://litestar-org.github.io/litestar-vite/latest/frameworks/)**: Guides for React, Vue, Svelte, Angular, Astro, and Nuxt.
49
+ - **[Reference](https://litestar-org.github.io/litestar-vite/latest/reference/)**: API and CLI documentation.
129
50
 
130
- | Mode | Alias | Use Case |
131
- |------|-------|----------|
132
- | `vite` | - | SPAs - proxies Vite assets only (default) |
133
- | `direct` | - | Two-port dev - expose Vite port directly |
134
- | `proxy` | `ssr` | Meta-frameworks - proxies everything except API routes |
51
+ For a full list of changes, see the [Changelog](https://litestar-org.github.io/litestar-vite/latest/changelog.html).
135
52
 
136
- ### Production Deployment
53
+ ## Common Commands
137
54
 
138
- **Astro (static):** Astro generates static HTML by default. Build and serve with Litestar:
55
+ - `litestar assets init --template <name>`: scaffold a frontend or framework app
56
+ - `litestar assets install`: install frontend dependencies with the configured executor
57
+ - `litestar assets build`: build production assets
58
+ - `litestar assets serve`: run the frontend toolchain directly
59
+ - `litestar assets generate-types`: export OpenAPI, routes, and Inertia page-prop metadata
60
+ - `litestar assets doctor`: inspect and optionally repair config drift
139
61
 
140
- ```bash
141
- litestar --app-dir examples/astro assets install
142
- litestar --app-dir examples/astro assets build
143
- VITE_DEV_MODE=false litestar --app-dir examples/astro run
144
- ```
145
-
146
- **Nuxt/SvelteKit (SSR):** These run their own Node servers. Deploy as two services:
62
+ ## Development
147
63
 
148
64
  ```bash
149
- # Terminal 1: SSR server
150
- litestar --app-dir examples/nuxt assets build
151
- litestar --app-dir examples/nuxt assets serve
152
-
153
- # Terminal 2: Litestar API
154
- VITE_DEV_MODE=false litestar --app-dir examples/nuxt run --port 8001
155
- ```
156
-
157
- ## Type generation
158
-
159
- ```python
160
- VitePlugin(config=ViteConfig(types=True)) # enable exports
161
- ```
65
+ # Install Python, docs, and JS dependencies; build package artifacts
66
+ make install && make build
162
67
 
163
- ```bash
164
- litestar assets generate-types # one-off or CI
68
+ # Run an example app
69
+ uv run litestar --app-dir examples/vue-inertia assets install
70
+ uv run litestar --app-dir examples/vue-inertia run
165
71
  ```
166
72
 
167
- ## CLI cheat sheet
168
-
169
- - `litestar assets doctor` — diagnose/fix config
170
- - `litestar assets init --template react|vue|svelte|...` — scaffold frontend
171
- - `litestar assets build` / `serve` — build or watch
172
- - `litestar assets deploy --storage gcs://bucket/assets` — upload via fsspec
173
- - `litestar assets generate-types` — OpenAPI + routes → TS types
174
- - `litestar assets install` — install frontend deps with the configured executor
175
-
176
- ### Doctor command highlights
73
+ Replace `vue-inertia` with any other example app: `vue`, `react`, `svelte`, `react-inertia`, `htmx`, `angular`, `astro`, `nuxt`, or `sveltekit`.
177
74
 
178
- - Prints Python vs Vite config snapshot (asset URLs, bundle/hot paths, ports, modes).
179
- - Flags missing hot file (dev proxy), missing manifest (prod), type-gen exports, env/config mismatches, and plugin install issues.
180
- - `--fix` can rewrite simple vite.config values (assetUrl, bundleDir, hotFile, type paths) after creating a backup.
75
+ For Inertia script-element bootstrap, enable `InertiaConfig(use_script_element=True)` on the Python side and keep `createInertiaApp({ defaults: { future: { useScriptElementForInitialPage: true } } })` aligned in the browser entry and SSR entry when `ssr=True` is enabled.
181
76
 
182
77
  ## Links
183
78
 
184
- - Docs: <https://litestar-org.github.io/litestar-vite/>
185
- - Examples: `examples/` (react, vue, svelte, react-inertia, vue-inertia, astro, nuxt, sveltekit, htmx)
186
- - Real-world example: [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack) - Full-featured application using litestar-vite
79
+ - Docs: <https://litestar-org.github.io/litestar-vite/latest/>
80
+ - Examples: `examples/` (React, Vue, Svelte, HTMX, Inertia, Astro, Nuxt, SvelteKit, Angular)
81
+ - Real-world example: [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack)
187
82
  - Issues: <https://github.com/litestar-org/litestar-vite/issues/>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Astro integration for Litestar-Vite.
2
+ * Astro 5 integration for Litestar-Vite.
3
3
  *
4
4
  * This integration enables seamless development with Astro as the frontend framework
5
5
  * and Litestar as the API backend. It provides:
@@ -16,7 +16,7 @@
16
16
  * export default defineConfig({
17
17
  * integrations: [
18
18
  * litestar({
19
- * apiProxy: 'http://localhost:8000',
19
+ * apiProxy: 'http://127.0.0.1:8000',
20
20
  * types: true,
21
21
  * }),
22
22
  * ],
@@ -178,7 +178,7 @@ export interface LitestarAstroConfig {
178
178
  /**
179
179
  * URL of the Litestar API backend for proxying requests during development.
180
180
  *
181
- * @example 'http://localhost:8000'
181
+ * @example 'http://127.0.0.1:8000'
182
182
  * @default 'http://localhost:8000'
183
183
  */
184
184
  apiProxy?: string;
@@ -224,7 +224,7 @@ export interface LitestarAstroConfig {
224
224
  * export default defineConfig({
225
225
  * integrations: [
226
226
  * litestar({
227
- * apiProxy: 'http://localhost:8000',
227
+ * apiProxy: 'http://127.0.0.1:8000',
228
228
  * apiPrefix: '/api',
229
229
  * types: {
230
230
  * enabled: true,
package/dist/js/astro.js CHANGED
@@ -187,7 +187,9 @@ function litestarAstro(userConfig = {}) {
187
187
  }
188
188
  }
189
189
  updateConfig(configUpdate);
190
- logger.info(`Litestar integration configured - proxying ${config.apiPrefix}/* to ${config.apiProxy}`);
190
+ if (config.verbose) {
191
+ logger.info(`Litestar integration configured - proxying ${config.apiPrefix}/* to ${config.apiProxy}`);
192
+ }
191
193
  },
192
194
  "astro:server:setup": ({ server, logger }) => {
193
195
  if (config.verbose) {
@@ -216,8 +218,10 @@ function litestarAstro(userConfig = {}) {
216
218
  }
217
219
  },
218
220
  "astro:build:start": ({ logger }) => {
219
- logger.info("Building with Litestar integration");
220
- logger.info(` Make sure your Litestar backend is accessible at: ${config.apiProxy}`);
221
+ if (config.verbose) {
222
+ logger.info("Building with Litestar integration");
223
+ logger.info(` Make sure your Litestar backend is accessible at: ${config.apiProxy}`);
224
+ }
221
225
  }
222
226
  }
223
227
  };
@@ -4,7 +4,9 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Litestar Vite Dev Server</title>
7
- <style rel="stylesheet" crossorigin>@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-lg:0 4px 4px #00000026;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-brand-primary:oklch(22% .03 265);--color-brand-accent:oklch(78% .16 85);--color-brand-accent-light:oklch(85% .12 85);--color-brand-gray:oklch(90% .01 265);--color-brand-success:oklch(65% .2 145);--color-brand-dots:oklch(26% .03 265);--animate-blob:blob 7s infinite ease-in-out}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.-right-4{right:calc(var(--spacing) * -4)}.-bottom-8{bottom:calc(var(--spacing) * -8)}.-left-4{left:calc(var(--spacing) * -4)}.left-20{left:calc(var(--spacing) * 20)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-2{margin-left:calc(var(--spacing) * 2)}.flex{display:flex}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1{height:calc(var(--spacing) * 1)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-72{height:calc(var(--spacing) * 72)}.h-full{height:100%}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-72{width:calc(var(--spacing) * 72)}.w-auto{width:auto}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.animate-blob{animation:var(--animate-blob)}.animate-ping{animation:var(--animate-ping)}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.overflow-hidden{overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500) 10%,transparent)}}.bg-brand-accent\/10{background-color:#e6ad001a}@supports (color:color-mix(in lab,red,red)){.bg-brand-accent\/10{background-color:color-mix(in oklab,var(--color-brand-accent) 10%,transparent)}}.bg-brand-accent\/20{background-color:#e6ad0033}@supports (color:color-mix(in lab,red,red)){.bg-brand-accent\/20{background-color:color-mix(in oklab,var(--color-brand-accent) 20%,transparent)}}.bg-brand-accent\/30{background-color:#e6ad004d}@supports (color:color-mix(in lab,red,red)){.bg-brand-accent\/30{background-color:color-mix(in oklab,var(--color-brand-accent) 30%,transparent)}}.bg-brand-success{background-color:var(--color-brand-success)}.bg-gray-900\/60{background-color:#10182899}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/60{background-color:color-mix(in oklab,var(--color-gray-900) 60%,transparent)}}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/10{background-color:color-mix(in oklab,var(--color-purple-500) 10%,transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/20{background-color:color-mix(in oklab,var(--color-purple-500) 20%,transparent)}}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.from-brand-accent{--tw-gradient-from:var(--color-brand-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-brand-accent{--tw-gradient-via:var(--color-brand-accent);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-brand-accent-light{--tw-gradient-to:var(--color-brand-accent-light);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-6{padding:calc(var(--spacing) * 6)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-8{padding-top:calc(var(--spacing) * 8)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-brand-accent{color:var(--color-brand-accent)}.text-brand-gray{color:var(--color-brand-gray)}.text-brand-gray\/30{color:#dbdee54d}@supports (color:color-mix(in lab,red,red)){.text-brand-gray\/30{color:color-mix(in oklab,var(--color-brand-gray) 30%,transparent)}}.text-brand-gray\/60{color:#dbdee599}@supports (color:color-mix(in lab,red,red)){.text-brand-gray\/60{color:color-mix(in oklab,var(--color-brand-gray) 60%,transparent)}}.text-brand-gray\/80{color:#dbdee5cc}@supports (color:color-mix(in lab,red,red)){.text-brand-gray\/80{color:color-mix(in oklab,var(--color-brand-gray) 80%,transparent)}}.text-brand-success{color:var(--color-brand-success)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.mix-blend-multiply{mix-blend-mode:multiply}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.animation-delay-2000{animation-delay:2s}.animation-delay-4000{animation-delay:4s}@media(hover:hover){.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:bg-brand-accent:is(:where(.group):hover *){background-color:var(--color-brand-accent)}.group-hover\:text-brand-accent-light:is(:where(.group):hover *){color:var(--color-brand-accent-light)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-brand-accent\/50:hover{border-color:#e6ad0080}@supports (color:color-mix(in lab,red,red)){.hover\:border-brand-accent\/50:hover{border-color:color-mix(in oklab,var(--color-brand-accent) 50%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}}@media(min-width:40rem){.sm\:px-12{padding-inline:calc(var(--spacing) * 12)}.sm\:py-12{padding-block:calc(var(--spacing) * 12)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}}@keyframes blob{0%,to{transform:translate(0)scale(1)}33%{transform:translate(30px,-50px)scale(1.1)}66%{transform:translate(-20px,20px)scale(.9)}}body{background-color:var(--color-brand-primary);background-image:radial-gradient(var(--color-brand-dots) 1px,transparent 1px);color:var(--color-brand-gray);background-size:24px 24px}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}</style>
7
+ <style rel="stylesheet" crossorigin>/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
8
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-lg:0 4px 4px #00000026;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-brand-primary:oklch(22% .03 265);--color-brand-accent:oklch(78% .16 85);--color-brand-accent-light:oklch(85% .12 85);--color-brand-gray:oklch(90% .01 265);--color-brand-success:oklch(65% .2 145);--color-brand-dots:oklch(26% .03 265);--animate-blob:blob 7s infinite ease-in-out}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.-right-4{right:calc(var(--spacing) * -4)}.-bottom-8{bottom:calc(var(--spacing) * -8)}.-left-4{left:calc(var(--spacing) * -4)}.left-20{left:calc(var(--spacing) * 20)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-2{margin-left:calc(var(--spacing) * 2)}.flex{display:flex}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1{height:calc(var(--spacing) * 1)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-72{height:calc(var(--spacing) * 72)}.h-full{height:100%}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-72{width:calc(var(--spacing) * 72)}.w-auto{width:auto}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.animate-blob{animation:var(--animate-blob);animation:var(--animate-blob)}.animate-ping{animation:var(--animate-ping)}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.overflow-hidden{overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab, red, red)){.bg-blue-500\/10{background-color:color-mix(in oklab, var(--color-blue-500) 10%, transparent)}}.bg-brand-accent\/10{background-color:#e6ad001a}@supports (color:color-mix(in lab, red, red)){.bg-brand-accent\/10{background-color:color-mix(in oklab, var(--color-brand-accent) 10%, transparent)}}.bg-brand-accent\/20{background-color:#e6ad0033}@supports (color:color-mix(in lab, red, red)){.bg-brand-accent\/20{background-color:color-mix(in oklab, var(--color-brand-accent) 20%, transparent)}}.bg-brand-accent\/30{background-color:#e6ad004d}@supports (color:color-mix(in lab, red, red)){.bg-brand-accent\/30{background-color:color-mix(in oklab, var(--color-brand-accent) 30%, transparent)}}.bg-brand-success{background-color:var(--color-brand-success)}.bg-gray-900\/60{background-color:#10182899}@supports (color:color-mix(in lab, red, red)){.bg-gray-900\/60{background-color:color-mix(in oklab, var(--color-gray-900) 60%, transparent)}}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/10{background-color:color-mix(in oklab, var(--color-purple-500) 10%, transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/20{background-color:color-mix(in oklab, var(--color-purple-500) 20%, transparent)}}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.bg-white\/5{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.from-brand-accent{--tw-gradient-from:var(--color-brand-accent);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-brand-accent{--tw-gradient-via:var(--color-brand-accent);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-brand-accent-light{--tw-gradient-to:var(--color-brand-accent-light);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-6{padding:calc(var(--spacing) * 6)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-8{padding-top:calc(var(--spacing) * 8)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-brand-accent{color:var(--color-brand-accent)}.text-brand-gray{color:var(--color-brand-gray)}.text-brand-gray\/30{color:#dbdee54d}@supports (color:color-mix(in lab, red, red)){.text-brand-gray\/30{color:color-mix(in oklab, var(--color-brand-gray) 30%, transparent)}}.text-brand-gray\/60{color:#dbdee599}@supports (color:color-mix(in lab, red, red)){.text-brand-gray\/60{color:color-mix(in oklab, var(--color-brand-gray) 60%, transparent)}}.text-brand-gray\/80{color:#dbdee5cc}@supports (color:color-mix(in lab, red, red)){.text-brand-gray\/80{color:color-mix(in oklab, var(--color-brand-gray) 80%, transparent)}}.text-brand-success{color:var(--color-brand-success)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.mix-blend-multiply{mix-blend-mode:multiply}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.animation-delay-2000{animation-delay:2s}.animation-delay-4000{animation-delay:4s}@media (hover:hover){.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:bg-brand-accent:is(:where(.group):hover *){background-color:var(--color-brand-accent)}.group-hover\:text-brand-accent-light:is(:where(.group):hover *){color:var(--color-brand-accent-light)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-brand-accent\/50:hover{border-color:#e6ad0080}@supports (color:color-mix(in lab, red, red)){.hover\:border-brand-accent\/50:hover{border-color:color-mix(in oklab, var(--color-brand-accent) 50%, transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}}@media (width>=40rem){.sm\:px-12{padding-inline:calc(var(--spacing) * 12)}.sm\:py-12{padding-block:calc(var(--spacing) * 12)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}}@keyframes blob{0%,to{transform:translate(0)scale(1)}33%{transform:translate(30px,-50px)scale(1.1)}66%{transform:translate(-20px,20px)scale(.9)}}body{background-color:var(--color-brand-primary);background-image:radial-gradient(var(--color-brand-dots) 1px, transparent 1px);color:var(--color-brand-gray);background-size:24px 24px}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}
9
+ /*$vite$:1*/</style>
8
10
  </head>
9
11
  <body class="antialiased min-h-screen flex flex-col items-center justify-center overflow-hidden">
10
12
 
@@ -58,7 +60,7 @@
58
60
  </div>
59
61
 
60
62
  <!-- Vite Logo -->
61
- <a href="https://vitejs.dev" target="_blank" class="group transition-transform hover:scale-110 duration-300 relative">
63
+ <a href="https://vite.dev" target="_blank" class="group transition-transform hover:scale-110 duration-300 relative">
62
64
  <div class="absolute inset-0 bg-purple-500/20 blur-xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
63
65
  <svg viewBox="0 0 410 404" class="relative h-16 w-auto drop-shadow-lg" fill="none" xmlns="http://www.w3.org/2000/svg">
64
66
  <path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
@@ -105,7 +105,11 @@ export function registerHtmxExtension() {
105
105
  swapJson(target, JSON.parse(frag.textContent ?? ""));
106
106
  }
107
107
  catch (e) {
108
- target.innerHTML = `<div style="color:red;padding:1rem">${e}</div>`;
108
+ const errDiv = document.createElement("div");
109
+ errDiv.style.cssText = "color:red;padding:1rem";
110
+ errDiv.textContent = String(e);
111
+ target.textContent = "";
112
+ target.appendChild(errDiv);
109
113
  }
110
114
  return [target];
111
115
  }
@@ -268,7 +272,7 @@ const directives = [
268
272
  };
269
273
  },
270
274
  },
271
- // ls-html="expr" - innerHTML (use carefully)
275
+ // ls-html="expr" - sanitized innerHTML
272
276
  {
273
277
  match: (a) => a.name === "ls-html",
274
278
  create(_, a) {
@@ -276,7 +280,7 @@ const directives = [
276
280
  if (!g)
277
281
  return null;
278
282
  return (ctx, el) => {
279
- el.innerHTML = String(g(ctx) ?? "");
283
+ el.innerHTML = sanitizeHtml(String(g(ctx) ?? ""));
280
284
  };
281
285
  },
282
286
  },
@@ -449,14 +453,55 @@ function childCtx(parent, data, index, key) {
449
453
  // =============================================================================
450
454
  // Expression Compiler
451
455
  // =============================================================================
456
+ /** Identifiers that must never appear as standalone words in expressions */
457
+ const BLOCKED_GLOBALS = [
458
+ "window",
459
+ "document",
460
+ "globalThis",
461
+ "self",
462
+ "top",
463
+ "frames",
464
+ "Function",
465
+ "eval",
466
+ "setTimeout",
467
+ "setInterval",
468
+ "constructor",
469
+ "__proto__",
470
+ "prototype",
471
+ "import",
472
+ "require",
473
+ ];
474
+ /** Build a single regex: matches any blocked word at a word boundary */
475
+ const BLOCKED_RE = new RegExp(`\\b(${BLOCKED_GLOBALS.join("|")})\\b`);
476
+ /** Strip string literals and template strings before checking for blocked patterns */
477
+ function stripStrings(s) {
478
+ return s
479
+ .replace(/`(?:[^`\\]|\\.)*`/g, "") // template literals
480
+ .replace(/"(?:[^"\\]|\\.)*"/g, "") // double-quoted strings
481
+ .replace(/'(?:[^'\\]|\\.)*'/g, ""); // single-quoted strings
482
+ }
483
+ function isExpressionSafe(s) {
484
+ const stripped = stripStrings(s);
485
+ return !BLOCKED_RE.test(stripped);
486
+ }
452
487
  function expr(s) {
453
488
  if (!s)
454
489
  return null;
455
490
  const cached = expressionCache.get(s);
456
491
  if (cached !== undefined) {
492
+ // LRU promotion: move to most-recent position
493
+ expressionCache.delete(s);
494
+ expressionCache.set(s, cached);
457
495
  return cached;
458
496
  }
497
+ if (!isExpressionSafe(s)) {
498
+ if (debug)
499
+ console.warn(`[litestar] blocked expression: ${s}`);
500
+ cacheExpression(s, null);
501
+ return null;
502
+ }
459
503
  try {
504
+ // Expression validated by isExpressionSafe() above — dangerous globals/constructors blocked
460
505
  const fn = new Function("ctx", `with(ctx){return(${s})}`);
461
506
  cacheExpression(s, fn);
462
507
  return fn;
@@ -475,6 +520,42 @@ function compileTextExpr(t) {
475
520
  return expr(`\`${escaped}\``);
476
521
  }
477
522
  // =============================================================================
523
+ // HTML Sanitizer
524
+ // =============================================================================
525
+ const DANGEROUS_TAGS = new Set(["script", "style", "iframe", "object", "embed", "form", "meta", "link", "base"]);
526
+ const DANGEROUS_ATTR_RE = /^on/i;
527
+ const DANGEROUS_PROTO_RE = /^\s*javascript\s*:/i;
528
+ function sanitizeHtml(html) {
529
+ const tpl = document.createElement("template");
530
+ tpl.innerHTML = html;
531
+ sanitizeNode(tpl.content);
532
+ return tpl.innerHTML;
533
+ }
534
+ function sanitizeNode(node) {
535
+ const toRemove = [];
536
+ for (let i = 0; i < node.childNodes.length; i++) {
537
+ const child = node.childNodes[i];
538
+ if (child.nodeType === 1) {
539
+ const el = child;
540
+ if (DANGEROUS_TAGS.has(el.tagName.toLowerCase())) {
541
+ toRemove.push(el);
542
+ continue;
543
+ }
544
+ for (const attr of Array.from(el.attributes)) {
545
+ if (DANGEROUS_ATTR_RE.test(attr.name)) {
546
+ el.removeAttribute(attr.name);
547
+ }
548
+ else if ((attr.name === "href" || attr.name === "src" || attr.name === "action") && DANGEROUS_PROTO_RE.test(attr.value)) {
549
+ el.removeAttribute(attr.name);
550
+ }
551
+ }
552
+ sanitizeNode(el);
553
+ }
554
+ }
555
+ for (const n of toRemove)
556
+ n.parentNode?.removeChild(n);
557
+ }
558
+ // =============================================================================
478
559
  // Utilities
479
560
  // =============================================================================
480
561
  function memo(node, key, fn) {
package/dist/js/index.js CHANGED
@@ -9,6 +9,7 @@ import { readBridgeConfig } from "./shared/bridge-schema.js";
9
9
  import { DEBOUNCE_MS } from "./shared/constants.js";
10
10
  import { createLogger } from "./shared/logger.js";
11
11
  import { createLitestarTypeGenPlugin } from "./shared/typegen-plugin.js";
12
+ import { buildInputOptions, resolveUserBuildInput } from "./shared/vite-compat.js";
12
13
  let exitHandlersBound = false;
13
14
  let warnedMissingRuntimeConfig = false;
14
15
  const MAX_TRANSFORM_PAYLOAD_BYTES = 1e6;
@@ -125,9 +126,7 @@ function resolveLitestarPlugin(pluginConfig) {
125
126
  manifest: userConfig.build?.manifest ?? (ssr ? false : "manifest.json"),
126
127
  ssrManifest: userConfig.build?.ssrManifest ?? (ssr ? "ssr-manifest.json" : false),
127
128
  outDir: userConfig.build?.outDir ?? resolveOutDir(pluginConfig, ssr),
128
- rollupOptions: {
129
- input: userConfig.build?.rollupOptions?.input ?? resolveInput(pluginConfig, ssr)
130
- },
129
+ ...buildInputOptions(resolveUserBuildInput(userConfig.build) ?? resolveInput(pluginConfig, ssr)),
131
130
  assetsInlineLimit: userConfig.build?.assetsInlineLimit ?? 0
132
131
  },
133
132
  server: {
@@ -518,7 +517,7 @@ function formatMissingConfigWarning() {
518
517
  `${y("\u2502")} ${d(" types: false,")} ${y("\u2502")}`,
519
518
  `${y("\u2502")} ${d("})")} ${y("\u2502")}`,
520
519
  `${y("\u2502")} ${y("\u2502")}`,
521
- `${y("\u2502")} Docs: ${c("https://docs.litestar.dev/vite/getting-started")} ${y("\u2502")}`,
520
+ `${y("\u2502")} Docs: ${c("https://litestar-org.github.io/litestar-vite/latest/usage/vite/")} ${y("\u2502")}`,
522
521
  `${y("\u2502")} ${y("\u2502")}`,
523
522
  y("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"),
524
523
  "",
@@ -709,7 +708,7 @@ function validateAgainstPythonDefaults(resolved, pythonDefaults, userConfig) {
709
708
  colors.yellow("[litestar-vite] Configuration mismatch detected:\n") + warnings.map((w) => ` ${colors.dim("\u2022")} ${w}`).join("\n") + `
710
709
 
711
710
  ${colors.dim("Precedence: vite.config.ts > .litestar.json > defaults")}
712
- ` + colors.dim("See: https://docs.litestar.dev/vite/config-precedence\n")
711
+ ` + colors.dim("See: https://litestar-org.github.io/litestar-vite/latest/usage/vite/\n")
713
712
  );
714
713
  }
715
714
  }
package/dist/js/nuxt.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Nuxt module for Litestar-Vite.
2
+ * Nuxt 4 module for Litestar-Vite.
3
3
  *
4
- * This module provides seamless integration between Nuxt 3+ and Litestar backend.
4
+ * This module provides seamless integration between Nuxt 4 and a Litestar backend.
5
5
  * It enables:
6
6
  * - API proxy configuration for dev server
7
7
  * - Type generation integration (shares @hey-api/openapi-ts output)
@@ -13,7 +13,7 @@
13
13
  * export default defineNuxtConfig({
14
14
  * modules: ['litestar-vite-plugin/nuxt'],
15
15
  * litestar: {
16
- * apiProxy: 'http://localhost:8000',
16
+ * apiProxy: 'http://127.0.0.1:8000',
17
17
  * apiPrefix: '/api',
18
18
  * types: true,
19
19
  * },
@@ -114,7 +114,7 @@ export interface LitestarNuxtConfig {
114
114
  /**
115
115
  * URL of the Litestar API backend for proxying requests during development.
116
116
  *
117
- * @example 'http://localhost:8000'
117
+ * @example 'http://127.0.0.1:8000'
118
118
  * @default 'http://localhost:8000'
119
119
  */
120
120
  apiProxy?: string;
@@ -160,7 +160,7 @@ export interface LitestarNuxtConfig {
160
160
  * export default defineNuxtConfig({
161
161
  * modules: ['litestar-vite-plugin/nuxt'],
162
162
  * litestar: {
163
- * apiProxy: 'http://localhost:8000',
163
+ * apiProxy: 'http://127.0.0.1:8000',
164
164
  * apiPrefix: '/api',
165
165
  * types: {
166
166
  * enabled: true,
@@ -172,7 +172,7 @@ export interface LitestarNuxtConfig {
172
172
  *
173
173
  * @example Using generated types in a composable
174
174
  * ```typescript
175
- * // composables/useApi.ts
175
+ * // app/composables/useApi.ts
176
176
  * import type { User } from '~/generated/api/types.gen';
177
177
  * import { route } from '~/generated/routes';
178
178
  *
package/dist/js/nuxt.js CHANGED
@@ -168,21 +168,23 @@ function createProxyPlugin(config) {
168
168
  console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`HMR Hotfile written: ${hmrHotFile} -> ${hmrUrl}`));
169
169
  }
170
170
  }
171
- server.httpServer?.once("listening", () => {
172
- setTimeout(() => {
173
- console.log("");
174
- console.log(` ${colors.cyan("[litestar-nuxt]")} ${colors.green("Integration active")}`);
175
- console.log(` ${colors.dim("\u251C\u2500")} API Proxy: ${colors.yellow(config.apiProxy)}`);
176
- console.log(` ${colors.dim("\u251C\u2500")} API Prefix: ${colors.yellow(config.apiPrefix)}`);
177
- console.log(` ${colors.dim("\u251C\u2500")} HMR Port: ${colors.yellow(hmrPort)}`);
178
- if (config.types !== false && config.types.enabled) {
179
- console.log(` ${colors.dim("\u2514\u2500")} Types Output: ${colors.yellow(config.types.output)}`);
180
- } else {
181
- console.log(` ${colors.dim("\u2514\u2500")} Types: ${colors.dim("disabled")}`);
182
- }
183
- console.log("");
184
- }, 100);
185
- });
171
+ if (config.verbose) {
172
+ server.httpServer?.once("listening", () => {
173
+ setTimeout(() => {
174
+ console.log("");
175
+ console.log(` ${colors.cyan("[litestar-nuxt]")} ${colors.green("Integration active")}`);
176
+ console.log(` ${colors.dim("\u251C\u2500")} API Proxy: ${colors.yellow(config.apiProxy)}`);
177
+ console.log(` ${colors.dim("\u251C\u2500")} API Prefix: ${colors.yellow(config.apiPrefix)}`);
178
+ console.log(` ${colors.dim("\u251C\u2500")} HMR Port: ${colors.yellow(hmrPort)}`);
179
+ if (config.types !== false && config.types.enabled) {
180
+ console.log(` ${colors.dim("\u2514\u2500")} Types Output: ${colors.yellow(config.types.output)}`);
181
+ } else {
182
+ console.log(` ${colors.dim("\u2514\u2500")} Types: ${colors.dim("disabled")}`);
183
+ }
184
+ console.log("");
185
+ }, 100);
186
+ });
187
+ }
186
188
  }
187
189
  };
188
190
  }
@@ -232,7 +234,7 @@ function litestarNuxtModule(userOptions, nuxt) {
232
234
  console.log(JSON.stringify(nuxt.options.nitro.devProxy, null, 2));
233
235
  }
234
236
  if (config.hotFile && config.devPort) {
235
- const rawHost = process.env.NUXT_HOST || process.env.HOST || "localhost";
237
+ const rawHost = process.env.NUXT_HOST || process.env.HOST || "127.0.0.1";
236
238
  const host = normalizeHost(rawHost);
237
239
  const url = `http://${host}:${config.devPort}`;
238
240
  fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
@@ -245,8 +247,9 @@ function litestarNuxtModule(userOptions, nuxt) {
245
247
  nuxt.hook("listen", (_server, listener) => {
246
248
  const info = listener;
247
249
  if (info && typeof info.port === "number") {
248
- const host = normalizeHost(info.host || "localhost");
250
+ const host = normalizeHost(info.host || "127.0.0.1");
249
251
  const url = `http://${host}:${info.port}`;
252
+ fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
250
253
  fs.writeFileSync(config.hotFile, url);
251
254
  if (config.verbose) {
252
255
  console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile updated via listen hook: ${url}`));
@@ -254,13 +257,15 @@ function litestarNuxtModule(userOptions, nuxt) {
254
257
  }
255
258
  });
256
259
  }
257
- console.log(colors.cyan("[litestar-nuxt]"), "Module initialized");
260
+ if (config.verbose) {
261
+ console.log(colors.cyan("[litestar-nuxt]"), "Module initialized");
262
+ }
258
263
  }
259
264
  litestarNuxtModule.meta = {
260
265
  name: "litestar-vite",
261
266
  configKey: "litestar",
262
267
  compatibility: {
263
- nuxt: ">=3.0.0"
268
+ nuxt: ">=4.0.0"
264
269
  }
265
270
  };
266
271
  litestarNuxtModule.getOptions = () => ({
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Parsed major version of the running Vite instance.
3
+ */
4
+ export declare const viteMajor: number;
5
+ /**
6
+ * Whether the running Vite version is 8+, which uses Rolldown
7
+ * instead of Rollup and introduces `rolldownOptions` / `rolldownOptions`
8
+ * in place of the deprecated `rollupOptions` / `esbuildOptions`.
9
+ */
10
+ export declare const isVite8Plus: boolean;
11
+ /**
12
+ * Returns a `build` config fragment with the input placed under
13
+ * the correct key for the running Vite version.
14
+ *
15
+ * Vite 8+ uses `rolldownOptions`; Vite 7 uses `rollupOptions`.
16
+ * Both accept the same `input` shape.
17
+ */
18
+ export declare function buildInputOptions(input: string | string[] | undefined): Record<string, unknown>;
19
+ /**
20
+ * Reads the user-provided `build.rollupOptions.input` or
21
+ * `build.rolldownOptions.input`, preferring the version-appropriate key.
22
+ */
23
+ export declare function resolveUserBuildInput(userBuild: Record<string, any> | undefined): string | string[] | undefined;
24
+ /**
25
+ * Returns a `build` config fragment with arbitrary options placed under
26
+ * the correct bundler key for the running Vite version.
27
+ */
28
+ export declare function buildBundlerOptions(options: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,26 @@
1
+ import { version } from "vite";
2
+ const viteMajor = Number(version.split(".")[0]);
3
+ const isVite8Plus = viteMajor >= 8;
4
+ function buildInputOptions(input) {
5
+ if (input === void 0) return {};
6
+ const key = isVite8Plus ? "rolldownOptions" : "rollupOptions";
7
+ return { [key]: { input } };
8
+ }
9
+ function resolveUserBuildInput(userBuild) {
10
+ if (!userBuild) return void 0;
11
+ if (isVite8Plus) {
12
+ return userBuild.rolldownOptions?.input ?? userBuild.rollupOptions?.input;
13
+ }
14
+ return userBuild.rollupOptions?.input ?? userBuild.rolldownOptions?.input;
15
+ }
16
+ function buildBundlerOptions(options) {
17
+ const key = isVite8Plus ? "rolldownOptions" : "rollupOptions";
18
+ return { [key]: options };
19
+ }
20
+ export {
21
+ buildBundlerOptions,
22
+ buildInputOptions,
23
+ isVite8Plus,
24
+ resolveUserBuildInput,
25
+ viteMajor
26
+ };
@@ -27,7 +27,6 @@
27
27
  *
28
28
  * @module
29
29
  */
30
- import type { Plugin } from "vite";
31
30
  /**
32
31
  * Configuration for TypeScript type generation in SvelteKit.
33
32
  */
@@ -200,5 +199,5 @@ export interface LitestarSvelteKitConfig {
200
199
  * };
201
200
  * ```
202
201
  */
203
- export declare function litestarSvelteKit(userConfig?: LitestarSvelteKitConfig): Plugin[];
202
+ export declare function litestarSvelteKit(userConfig?: LitestarSvelteKitConfig): any[];
204
203
  export default litestarSvelteKit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litestar-vite-plugin",
3
- "version": "0.18.4",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "Litestar plugin for Vite.",
6
6
  "keywords": [
@@ -90,7 +90,7 @@
90
90
  "build-plugin": "rm -rf dist/js && npm run build-plugin-types && npm run build-plugin-esm && npm run build-dev-server",
91
91
  "build-dev-server": "vite build --config src/js/src/dev-server/vite.config.ts && mv dist/js/index.html dist/js/dev-server-index.html",
92
92
  "build-plugin-types": "tsc --project src/js/tsconfig.json --emitDeclarationOnly",
93
- "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js && esbuild src/js/src/install-hint.ts --platform=node --format=esm --outfile=dist/js/install-hint.js && esbuild src/js/src/litestar-meta.ts --platform=node --format=esm --outfile=dist/js/litestar-meta.js && esbuild src/js/src/typegen-cli.ts --platform=node --format=esm --outfile=dist/js/typegen-cli.js --banner:js='#!/usr/bin/env node' && mkdir -p dist/js/shared && esbuild src/js/src/shared/bridge-schema.ts src/js/src/shared/constants.ts src/js/src/shared/debounce.ts src/js/src/shared/format-path.ts src/js/src/shared/logger.ts src/js/src/shared/emit-page-props-types.ts src/js/src/shared/emit-schemas-types.ts src/js/src/shared/emit-static-props-types.ts src/js/src/shared/typegen-plugin.ts src/js/src/shared/typegen-core.ts src/js/src/shared/write-if-changed.ts src/js/src/shared/typegen-cache.ts src/js/src/shared/network.ts --platform=node --format=esm --outdir=dist/js/shared",
93
+ "build-plugin-esm": "esbuild src/js/src/index.ts --platform=node --format=esm --outfile=dist/js/index.js && esbuild src/js/src/install-hint.ts --platform=node --format=esm --outfile=dist/js/install-hint.js && esbuild src/js/src/litestar-meta.ts --platform=node --format=esm --outfile=dist/js/litestar-meta.js && esbuild src/js/src/typegen-cli.ts --platform=node --format=esm --outfile=dist/js/typegen-cli.js --banner:js='#!/usr/bin/env node' && mkdir -p dist/js/shared && esbuild src/js/src/shared/bridge-schema.ts src/js/src/shared/constants.ts src/js/src/shared/debounce.ts src/js/src/shared/format-path.ts src/js/src/shared/logger.ts src/js/src/shared/emit-page-props-types.ts src/js/src/shared/emit-schemas-types.ts src/js/src/shared/emit-static-props-types.ts src/js/src/shared/typegen-plugin.ts src/js/src/shared/typegen-core.ts src/js/src/shared/write-if-changed.ts src/js/src/shared/typegen-cache.ts src/js/src/shared/network.ts src/js/src/shared/vite-compat.ts --platform=node --format=esm --outdir=dist/js/shared",
94
94
  "build-helpers": "rm -rf dist/js/helpers && tsc --project src/js/tsconfig.helpers.json",
95
95
  "build-inertia-helpers": "rm -rf dist/js/inertia-helpers && tsc --project src/js/tsconfig.inertia-helpers.json",
96
96
  "build-integrations": "esbuild src/js/src/astro.ts src/js/src/sveltekit.ts src/js/src/nuxt.ts src/js/src/inertia-types.ts --platform=node --format=esm --outdir=dist/js",
@@ -102,16 +102,16 @@
102
102
  "@tailwindcss/vite": "^4.0.0",
103
103
  "@types/node": "^22.15.3",
104
104
  "@vitest/coverage-v8": "^3.2.4",
105
- "esbuild": "0.25.3",
105
+ "esbuild": "0.27.4",
106
106
  "happy-dom": "^20.0.2",
107
107
  "tailwindcss": "^4.0.0",
108
108
  "typescript": "^5.8.3",
109
- "vite": "^7.1.2",
109
+ "vite": "^8.0.0",
110
110
  "vite-plugin-singlefile": "^2.3.0",
111
111
  "vitest": "^3.1.2"
112
112
  },
113
113
  "peerDependencies": {
114
- "vite": "^6.0.0 || ^7.0.0"
114
+ "vite": "^7.0.0 || ^8.0.0"
115
115
  },
116
116
  "engines": {
117
117
  "node": "^20.19.0 || >=22.12.0"