weifuwu 0.27.11 → 0.27.13
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 +44 -60
- package/dist/docs/ssr/ui.md +472 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +40 -29
- package/dist/ssr/assets.d.ts +3 -18
- package/dist/ssr/ui/assets.d.ts +2 -0
- package/dist/template/AGENTS.md +30 -0
- package/dist/template/app.ts +7 -10
- package/dist/template/locales/en.json +2 -2
- package/dist/template/locales/zh-CN.json +2 -2
- package/dist/template/ui/app/globals.css +2 -8
- package/dist/template/ui/app/layout.ts +32 -37
- package/dist/template/ui/app/page.ts +20 -36
- package/dist/weifuwu-ui.css +646 -0
- package/dist/weifuwu-ui.js +976 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -66,36 +66,27 @@ app.use(rateLimit({ window: 60 }))
|
|
|
66
66
|
## Full-stack SSR
|
|
67
67
|
|
|
68
68
|
Server-rendered HTML with zero frontend build tools. Uses `html()` tagged templates
|
|
69
|
-
for safe HTML rendering,
|
|
69
|
+
for safe HTML rendering, and `weifuwu-ui.js` for client-side interactions.
|
|
70
70
|
|
|
71
71
|
```ts
|
|
72
|
-
import { Router, serve, html, raw, layout, view,
|
|
72
|
+
import { Router, serve, html, raw, layout, view, wfuwAssets, theme, i18n, flash } from 'weifuwu'
|
|
73
73
|
|
|
74
74
|
const app = new Router()
|
|
75
75
|
|
|
76
76
|
// Middleware
|
|
77
77
|
app.use(theme())
|
|
78
78
|
app.use(i18n({ dir: './locales' }))
|
|
79
|
-
app.use(
|
|
79
|
+
app.use(flash())
|
|
80
|
+
|
|
81
|
+
// weifuwu-ui frontend runtime
|
|
82
|
+
app.use('/', wfuwAssets())
|
|
80
83
|
|
|
81
84
|
// Layout (wraps all pages)
|
|
82
85
|
app.use(layout('./ui/app/layout.ts'))
|
|
83
86
|
|
|
84
|
-
// Static assets (HTMX, Alpine — served locally, no CDN)
|
|
85
|
-
app.use(assetRouter())
|
|
86
|
-
|
|
87
|
-
// CSS serving
|
|
88
|
-
app.use('/', cssRouter('./ui'))
|
|
89
|
-
|
|
90
87
|
// Page
|
|
91
88
|
app.get('/', view('./ui/app/page.ts'))
|
|
92
89
|
|
|
93
|
-
// HTMX fragment handler
|
|
94
|
-
app.get('/users/table', async (req, ctx) => {
|
|
95
|
-
const users = await ctx.sql`SELECT * FROM users`
|
|
96
|
-
return html`${users.map((u) => html`<div>${u.name}</div>`)}`
|
|
97
|
-
})
|
|
98
|
-
|
|
99
90
|
// API
|
|
100
91
|
app.get('/api/ping', () => Response.json({ pong: true }))
|
|
101
92
|
|
|
@@ -139,15 +130,17 @@ import { html, raw } from 'weifuwu'
|
|
|
139
130
|
// ui/app/layout.ts
|
|
140
131
|
export default function (body: string, ctx: any) {
|
|
141
132
|
return html`<!DOCTYPE html>
|
|
142
|
-
<html>
|
|
133
|
+
<html data-theme="${ctx.theme?.value || 'light'}">
|
|
143
134
|
<head>
|
|
144
135
|
<meta charset="utf-8" />
|
|
145
|
-
<
|
|
146
|
-
<script
|
|
136
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
137
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
138
|
+
<script id="__wfw-i18n" type="application/json">
|
|
139
|
+
${raw(JSON.stringify(ctx.i18n?.messages || {}))}
|
|
140
|
+
</script>
|
|
147
141
|
</head>
|
|
148
|
-
<body
|
|
142
|
+
<body data-locale="${ctx.i18n?.locale || 'en'}">
|
|
149
143
|
${raw(body)}
|
|
150
|
-
<!-- ← use raw() for page content -->
|
|
151
144
|
</body>
|
|
152
145
|
</html>`
|
|
153
146
|
}
|
|
@@ -167,29 +160,23 @@ export default function (ctx: any) {
|
|
|
167
160
|
}
|
|
168
161
|
```
|
|
169
162
|
|
|
170
|
-
###
|
|
163
|
+
### UI frontend runtime (weifuwu-ui)
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/* ui/app/globals.css */
|
|
176
|
-
@import 'tailwindcss';
|
|
177
|
-
@custom-variant dark (&:is(.dark *));
|
|
178
|
-
```
|
|
165
|
+
weifuwu-ui is a zero-dependency frontend runtime (~5KB) that ships with weifuwu.
|
|
166
|
+
One `<script>` + `<link>` covers AJAX loading, state binding, SSE streaming,
|
|
167
|
+
WebSocket, theme/i18n/flash integration, and UI components.
|
|
179
168
|
|
|
180
169
|
```ts
|
|
181
|
-
|
|
182
|
-
app.use(cssRouter('./ui')) // serve /__wfw/style/:hash.css
|
|
183
|
-
```
|
|
170
|
+
import { wfuwAssets } from 'weifuwu'
|
|
184
171
|
|
|
185
|
-
|
|
172
|
+
app.use(wfuwAssets()) // serve /__wfw/js/weifuwu-ui.js + /__wfw/css/weifuwu-ui.css
|
|
173
|
+
```
|
|
186
174
|
|
|
187
|
-
|
|
188
|
-
No external network requests.
|
|
175
|
+
In your layout:
|
|
189
176
|
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
177
|
+
```html
|
|
178
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
179
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
193
180
|
```
|
|
194
181
|
|
|
195
182
|
---
|
|
@@ -567,24 +554,20 @@ import { view } from 'weifuwu'
|
|
|
567
554
|
app.get('/', view('./ui/app/page.ts'))
|
|
568
555
|
```
|
|
569
556
|
|
|
570
|
-
####
|
|
557
|
+
#### wfuwAssets()
|
|
571
558
|
|
|
572
|
-
|
|
559
|
+
Serve weifuwu-ui.js and weifuwu-ui.css — zero-dependency frontend runtime (~5KB total).
|
|
560
|
+
Covers: AJAX loading, state binding, SSE streaming, WebSocket, theme/i18n/flash,
|
|
561
|
+
modal/collapse/tabs/dropdown/toast components.
|
|
573
562
|
|
|
574
563
|
```ts
|
|
575
|
-
import {
|
|
576
|
-
app.use(
|
|
577
|
-
app.use(cssRouter('./ui')) // serve /__wfw/style/:hash.css
|
|
564
|
+
import { wfuwAssets } from 'weifuwu'
|
|
565
|
+
app.use(wfuwAssets())
|
|
578
566
|
```
|
|
579
567
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
```ts
|
|
585
|
-
import { assetRouter, assetScripts } from 'weifuwu'
|
|
586
|
-
app.use(assetRouter())
|
|
587
|
-
// In layout: ${assetScripts()}
|
|
568
|
+
```html
|
|
569
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
570
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
588
571
|
```
|
|
589
572
|
|
|
590
573
|
### Standalone utilities
|
|
@@ -666,15 +649,15 @@ throw new HttpError('Not found', 404) // caught by serve(), returns 404
|
|
|
666
649
|
## CLI
|
|
667
650
|
|
|
668
651
|
```bash
|
|
669
|
-
npx weifuwu init my-app # Full-stack project (SSR +
|
|
652
|
+
npx weifuwu init my-app # Full-stack project (SSR + weifuwu-ui)
|
|
670
653
|
npx weifuwu init my-app --minimal # Minimal API-only project
|
|
671
654
|
npx weifuwu version # Print version
|
|
672
655
|
```
|
|
673
656
|
|
|
674
657
|
### Full-stack template (`init`)
|
|
675
658
|
|
|
676
|
-
Generates a complete project with SSR
|
|
677
|
-
|
|
659
|
+
Generates a complete project with SSR via `html()` tagged templates, weifuwu-ui frontend
|
|
660
|
+
runtime (zero external deps, ~5KB), theme switching, i18n, and flash messages.
|
|
678
661
|
|
|
679
662
|
```
|
|
680
663
|
my-app/
|
|
@@ -682,9 +665,9 @@ my-app/
|
|
|
682
665
|
app.ts — Router setup
|
|
683
666
|
ui/
|
|
684
667
|
app/
|
|
685
|
-
globals.css —
|
|
686
|
-
layout.ts — root layout (
|
|
687
|
-
page.ts — home page (theme/
|
|
668
|
+
globals.css — custom styles
|
|
669
|
+
layout.ts — root layout (weifuwu-ui + theme/i18n/flash)
|
|
670
|
+
page.ts — home page (wu-data/wu-theme/wu-lang demo)
|
|
688
671
|
locales/
|
|
689
672
|
en.json
|
|
690
673
|
zh-CN.json
|
|
@@ -709,10 +692,11 @@ Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
|
|
|
709
692
|
- `ws` — WebSocket
|
|
710
693
|
- `zod` — Schema validation
|
|
711
694
|
|
|
712
|
-
### Frontend
|
|
695
|
+
### Frontend
|
|
696
|
+
|
|
697
|
+
- weifuwu-ui.js (~5KB) — built-in, zero external dependencies
|
|
713
698
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
- `alpinejs` — Lightweight client interactivity
|
|
699
|
+
Covers: AJAX loading, state binding, SSE streaming, WebSocket, theme/i18n/flash,
|
|
700
|
+
modal/collapse/tabs/dropdown/toast components.
|
|
717
701
|
|
|
718
702
|
Zero build tools. Zero frontend framework compilation.
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# weifuwu-ui — Frontend Runtime for SSR
|
|
2
|
+
|
|
3
|
+
`weifuwu-ui` is a zero-dependency frontend runtime that ships with weifuwu.
|
|
4
|
+
It provides AJAX loading, state binding, SSE streaming, WebSocket, theme/i18n/flash integration,
|
|
5
|
+
and UI components — all via HTML attributes. No build step, no npm install.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
11
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
In weifuwu:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Router, wfuwAssets } from 'weifuwu'
|
|
18
|
+
|
|
19
|
+
const app = new Router()
|
|
20
|
+
app.use('/', wfuwAssets()) // serves /__wfw/js/weifuwu-ui.js and /__wfw/css/weifuwu-ui.css
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then in your `html()` layout:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
html`
|
|
27
|
+
<!DOCTYPE html>
|
|
28
|
+
<html data-theme="${ctx.theme?.value || 'light'}">
|
|
29
|
+
<head>
|
|
30
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
31
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<button class="wu-btn wu-btn-primary">Hello</button>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
`
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Attribute Reference
|
|
43
|
+
|
|
44
|
+
### 1. State & Binding (`wu-data`)
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<div wu-data="{ count: 0, open: false, name: 'World' }">
|
|
48
|
+
<button class="wu-btn" wu-on="click: count++">+1</button>
|
|
49
|
+
<span wu-text="count">0</span>
|
|
50
|
+
|
|
51
|
+
<button class="wu-btn" wu-on="click: open = !open">Toggle</button>
|
|
52
|
+
<div wu-show="open">This is visible when open is true</div>
|
|
53
|
+
<div wu-hide="open">This is hidden when open is true</div>
|
|
54
|
+
|
|
55
|
+
<input wu-model="name" />
|
|
56
|
+
<p>Hello, <span wu-text="name"></span>!</p>
|
|
57
|
+
</div>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Attribute | Description |
|
|
61
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
62
|
+
| `wu-data` | Define local reactive state (JSON object). |
|
|
63
|
+
| `wu-text` | Bind state value to element's `textContent`. |
|
|
64
|
+
| `wu-html` | Bind state value to element's `innerHTML` (escaped). |
|
|
65
|
+
| `wu-show` | Show element when state value is truthy (`display: ''`). |
|
|
66
|
+
| `wu-hide` | Hide element when state value is truthy (`display: none`). |
|
|
67
|
+
| `wu-class` | Conditional CSS class from expression: `wu-class="count > 0 ? 'has-items' : 'empty'"`. |
|
|
68
|
+
| `wu-model` | Two-way binding on `<input>`, `<select>`, `<textarea>`. |
|
|
69
|
+
| `wu-each` | Iterate over an array. Template uses `${this}` (item) and `${index}` (index). |
|
|
70
|
+
|
|
71
|
+
### 2. Events (`wu-on`)
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<button wu-on="click: count++">Increment</button>
|
|
75
|
+
<input wu-on="keyup: if(event.key === 'Enter') search()" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Format | Description |
|
|
79
|
+
| ------------- | -------------------------------------------------------------- |
|
|
80
|
+
| `click: expr` | Execute expression on click. |
|
|
81
|
+
| `keyup: expr` | Execute expression on keyup. Expression has access to `event`. |
|
|
82
|
+
|
|
83
|
+
Expressions execute in the scope of `wu-data` state variables. Any mutation goes through the Proxy and triggers binding updates.
|
|
84
|
+
|
|
85
|
+
### 3. AJAX Loading
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<!-- Load content on page load -->
|
|
89
|
+
<div wu-get="/partials/posts" wu-trigger="load">
|
|
90
|
+
<div class="wu-skeleton" style="height: 100px"></div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Click to load -->
|
|
94
|
+
<button class="wu-btn" wu-get="/users/1/edit" wu-target="#main">Edit</button>
|
|
95
|
+
|
|
96
|
+
<!-- POST form -->
|
|
97
|
+
<form wu-post="/users" wu-target="#user-list">
|
|
98
|
+
<input name="name" class="wu-input" />
|
|
99
|
+
<button class="wu-btn wu-btn-primary">Create</button>
|
|
100
|
+
</form>
|
|
101
|
+
|
|
102
|
+
<!-- DELETE with confirmation -->
|
|
103
|
+
<button
|
|
104
|
+
class="wu-btn wu-btn-danger"
|
|
105
|
+
wu-delete="/users/1"
|
|
106
|
+
wu-target="#user-1"
|
|
107
|
+
wu-confirm="Delete this user?"
|
|
108
|
+
>
|
|
109
|
+
Delete
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
<!-- Polling -->
|
|
113
|
+
<div wu-get="/notifications" wu-trigger="every:5s" wu-target="#notif-list"></div>
|
|
114
|
+
|
|
115
|
+
<!-- Infinite scroll -->
|
|
116
|
+
<div wu-get="/posts?page=2" wu-trigger="visible" wu-target="#posts" wu-swap="append"></div>
|
|
117
|
+
|
|
118
|
+
<!-- With loading indicator -->
|
|
119
|
+
<button class="wu-btn" wu-get="/slow" wu-target="#result" wu-loading="#spinner">Load</button>
|
|
120
|
+
<div id="spinner" class="wu-hidden">Loading...</div>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Attribute | Description |
|
|
124
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
125
|
+
| `wu-get` / `wu-post` / `wu-put` / `wu-patch` / `wu-delete` | HTTP method + URL |
|
|
126
|
+
| `wu-trigger` | `load` \| `click` \| `every:Ns` \| `visible` |
|
|
127
|
+
| `wu-target` | CSS selector for target element |
|
|
128
|
+
| `wu-swap` | `innerHTML` (default) \| `outerHTML` \| `before` \| `after` \| `prepend` \| `append` |
|
|
129
|
+
| `wu-confirm` | Confirmation dialog text |
|
|
130
|
+
| `wu-loading` | CSS selector for loading indicator (auto toggle `wu-hidden`) |
|
|
131
|
+
|
|
132
|
+
**Server response conventions:**
|
|
133
|
+
|
|
134
|
+
- Success (2xx): replace target with returned HTML
|
|
135
|
+
- Error (422) with `{ errors: { field: "msg" } }`: fill `[wu-error="field"]` elements
|
|
136
|
+
- `X-WFU-Redirect` header: client-side redirect
|
|
137
|
+
|
|
138
|
+
### 4. SSE Streaming
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<!-- Auto-connect SSE endpoint -->
|
|
142
|
+
<div wu-sse="/api/metrics" wu-on-sse-metric="cpu = data.cpu; memory = data.memory">
|
|
143
|
+
CPU: <span wu-text="cpu">0</span>% Memory: <span wu-text="memory">0</span>%
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Stream from POST response (AI chat, etc.) -->
|
|
147
|
+
<button
|
|
148
|
+
class="wu-btn wu-btn-primary"
|
|
149
|
+
wu-post="/api/chat"
|
|
150
|
+
wu-stream
|
|
151
|
+
wu-on-sse-text-delta="output += data.text"
|
|
152
|
+
wu-on-sse-finish="loading = false"
|
|
153
|
+
>
|
|
154
|
+
Send
|
|
155
|
+
</button>
|
|
156
|
+
<pre wu-text="output"></pre>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**`wu-stream`**: makes a POST/GET request and treats the response as SSE stream.
|
|
160
|
+
**`wu-sse`**: establishes a persistent EventSource connection.
|
|
161
|
+
**`wu-on-sse-{eventName}`**: handles SSE events by name. `data` variable holds the JSON-parsed event data.
|
|
162
|
+
|
|
163
|
+
**Programmatic API:**
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
// In inline <script> or wu-on expressions
|
|
167
|
+
wu.stream('POST', '/api/chat', {
|
|
168
|
+
body: JSON.stringify({ message: 'Hello' }),
|
|
169
|
+
onEvent: {
|
|
170
|
+
'text-delta': (data) => {
|
|
171
|
+
console.log(data.text)
|
|
172
|
+
},
|
|
173
|
+
finish: () => {
|
|
174
|
+
console.log('Done')
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
onDone: () => {
|
|
178
|
+
console.log('Stream closed')
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
wu.abort() // Cancel active stream
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 5. WebSocket
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<div
|
|
188
|
+
wu-ws="wss://server.com/chat"
|
|
189
|
+
wu-on-ws-open="connected = true"
|
|
190
|
+
wu-on-ws-close="connected = false"
|
|
191
|
+
wu-on-ws-message="messages.push(JSON.parse(data))"
|
|
192
|
+
>
|
|
193
|
+
Status: <span wu-text="connected ? 'Connected' : 'Disconnected'"></span>
|
|
194
|
+
</div>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Attribute | Description |
|
|
198
|
+
| ------------------ | ------------------------------------------------- |
|
|
199
|
+
| `wu-ws` | WebSocket URL (auto-connect on page load) |
|
|
200
|
+
| `wu-on-ws-open` | Called when connection opens |
|
|
201
|
+
| `wu-on-ws-close` | Called when connection closes |
|
|
202
|
+
| `wu-on-ws-message` | Called on each message. `data` is the raw string. |
|
|
203
|
+
|
|
204
|
+
**Programmatic API:**
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
wu.send({ message: 'Hello' }) // Send JSON through all active WS connections
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 6. Theme Switching
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<html data-theme="light">
|
|
214
|
+
<body>
|
|
215
|
+
<button wu-theme="dark">🌙 Dark</button>
|
|
216
|
+
<button wu-theme="light">☀️ Light</button>
|
|
217
|
+
<button wu-theme="system">🖥 System</button>
|
|
218
|
+
</body>
|
|
219
|
+
</html>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The `data-theme` attribute on `<html>` controls all CSS variables.
|
|
223
|
+
|
|
224
|
+
- `system` follows the OS preference (`prefers-color-scheme`).
|
|
225
|
+
- Changes are persisted via cookie and synced with server.
|
|
226
|
+
- CSS variables in `:root` and `[data-theme="dark"]` control all component colors.
|
|
227
|
+
|
|
228
|
+
### 7. Internationalization
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<body data-locale="zh-CN">
|
|
232
|
+
<button wu-lang="zh-CN">中文</button>
|
|
233
|
+
<button wu-lang="en">English</button>
|
|
234
|
+
|
|
235
|
+
<span wu-text-key="greeting">你好</span>
|
|
236
|
+
<span wu-text-key="nav.home">首页</span>
|
|
237
|
+
</body>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Server setup:**
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// In your layout, inject translation messages
|
|
244
|
+
html`
|
|
245
|
+
<script id="__wfw-i18n" type="application/json">
|
|
246
|
+
${raw(JSON.stringify(ctx.i18n.messages))}
|
|
247
|
+
</script>
|
|
248
|
+
`
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- `wu-lang`: click to switch locale. Sends JSON request to `/__lang/:locale`, updates all `[wu-text-key]` elements.
|
|
252
|
+
- `wu-text-key`: language key bound element. Server renders initial value. On switch, weifuwu-ui updates text without refresh.
|
|
253
|
+
- `wu.t(key)`: programmatic translation access (useful in scripts).
|
|
254
|
+
|
|
255
|
+
### 8. Flash Messages
|
|
256
|
+
|
|
257
|
+
```html
|
|
258
|
+
<div wu-flash></div>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Server setup:**
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// In your page
|
|
265
|
+
html`
|
|
266
|
+
${ctx.flash.value &&
|
|
267
|
+
html`
|
|
268
|
+
<script id="__wfw-flash" type="application/json">
|
|
269
|
+
${raw(JSON.stringify(ctx.flash.value))}
|
|
270
|
+
</script>
|
|
271
|
+
`}
|
|
272
|
+
<div wu-flash></div>
|
|
273
|
+
`
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
| Flash data format | Result |
|
|
277
|
+
| ----------------------------------------- | ---------------------------- |
|
|
278
|
+
| `{ type: "success", message: "Saved!" }` | Green flash, auto-dismiss 3s |
|
|
279
|
+
| `{ type: "error", message: "Failed" }` | Red flash |
|
|
280
|
+
| `{ type: "info", message: "Processing" }` | Blue flash |
|
|
281
|
+
|
|
282
|
+
### 9. Form Validation Errors
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<form wu-post="/users" wu-target="#user-list">
|
|
286
|
+
<label class="wu-label">Name</label>
|
|
287
|
+
<input name="name" class="wu-input" />
|
|
288
|
+
<span wu-error="name" class="wu-error"></span>
|
|
289
|
+
|
|
290
|
+
<button class="wu-btn wu-btn-primary">Submit</button>
|
|
291
|
+
</form>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Server returns 422 with JSON body `{ errors: { name: "Required", email: "Invalid" } }`.
|
|
295
|
+
weifuwu-ui automatically fills the corresponding `[wu-error="field"]` elements.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## UI Components
|
|
300
|
+
|
|
301
|
+
### Modal
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
<button class="wu-btn" wu-target="#my-modal" wu-toggle>Open Modal</button>
|
|
305
|
+
|
|
306
|
+
<div id="my-modal" wu-modal>
|
|
307
|
+
<div class="wu-modal-content">
|
|
308
|
+
<h3 class="wu-modal-title">Title</h3>
|
|
309
|
+
<p>Modal content here...</p>
|
|
310
|
+
<button class="wu-btn" wu-close>Close</button>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Features: ESC to close, click outside to close, `wu-toggle` / `wu-close` attributes.
|
|
316
|
+
|
|
317
|
+
### Collapse
|
|
318
|
+
|
|
319
|
+
```html
|
|
320
|
+
<div wu-collapse>
|
|
321
|
+
<button wu-toggle>Section Title</button>
|
|
322
|
+
<div wu-body>Collapsible content here.</div>
|
|
323
|
+
</div>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Tabs
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<div wu-tabs>
|
|
330
|
+
<nav>
|
|
331
|
+
<button wu-tab="tab1" class="wu-active">Tab 1</button>
|
|
332
|
+
<button wu-tab="tab2">Tab 2</button>
|
|
333
|
+
</nav>
|
|
334
|
+
<div wu-panel="tab1" class="wu-active">Content 1</div>
|
|
335
|
+
<div wu-panel="tab2">Content 2</div>
|
|
336
|
+
</div>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Dropdown
|
|
340
|
+
|
|
341
|
+
```html
|
|
342
|
+
<div wu-dropdown>
|
|
343
|
+
<button wu-toggle class="wu-btn">Menu ▾</button>
|
|
344
|
+
<div wu-menu>
|
|
345
|
+
<a href="/profile">Profile</a>
|
|
346
|
+
<a href="/settings">Settings</a>
|
|
347
|
+
<hr />
|
|
348
|
+
<a href="/logout">Logout</a>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Toast Notification
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
// Programmatic
|
|
357
|
+
wu.toast('Saved successfully!', 'success')
|
|
358
|
+
wu.toast('Something went wrong', 'error')
|
|
359
|
+
wu.toast('New message received', 'info')
|
|
360
|
+
wu.toast('Warning: disk space low', 'warning')
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## CSS Customization
|
|
366
|
+
|
|
367
|
+
Override CSS variables in your stylesheet:
|
|
368
|
+
|
|
369
|
+
```css
|
|
370
|
+
:root {
|
|
371
|
+
--wu-primary: #7c3aed; /* Change primary color to purple */
|
|
372
|
+
--wu-radius: 8px; /* Larger border radius */
|
|
373
|
+
--wu-bg: #faf5ff; /* Custom background */
|
|
374
|
+
--wu-text: #1a1a2e; /* Custom text color */
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The theme is controlled by `data-theme` on `<html>`:
|
|
379
|
+
|
|
380
|
+
```html
|
|
381
|
+
<html data-theme="dark">
|
|
382
|
+
<!-- Dark mode active -->
|
|
383
|
+
<html data-theme="light">
|
|
384
|
+
<!-- Light mode active -->
|
|
385
|
+
</html>
|
|
386
|
+
</html>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## CSS Class Reference
|
|
392
|
+
|
|
393
|
+
| Class | Description |
|
|
394
|
+
| ------------------------------------------------------------- | -------------------------- |
|
|
395
|
+
| `.wu-btn` | Base button |
|
|
396
|
+
| `.wu-btn-primary` | Primary action button |
|
|
397
|
+
| `.wu-btn-danger` | Destructive button |
|
|
398
|
+
| `.wu-btn-sm` / `.wu-btn-lg` | Button sizes |
|
|
399
|
+
| `.wu-input` / `.wu-select` / `.wu-textarea` | Form inputs |
|
|
400
|
+
| `.wu-label` | Form label |
|
|
401
|
+
| `.wu-error` | Error text |
|
|
402
|
+
| `.wu-card` | Card container |
|
|
403
|
+
| `.wu-modal-content` | Modal content |
|
|
404
|
+
| `.wu-toast` / `.wu-toast-success` / `.wu-toast-error` | Toast styles |
|
|
405
|
+
| `.wu-skeleton` | Loading skeleton animation |
|
|
406
|
+
| `.wu-flash-*` | Flash message styles |
|
|
407
|
+
| `.wu-hidden` | Utility: `display: none` |
|
|
408
|
+
| `.wu-flex` / `.wu-grid` / `.wu-gap-*` / `.wu-p-*` / `.wu-m-*` | Layout utilities |
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Integration with weifuwu Backend
|
|
413
|
+
|
|
414
|
+
| Backend Module | Frontend Attribute |
|
|
415
|
+
| -------------- | ------------------------------------ |
|
|
416
|
+
| `theme()` | `wu-theme` |
|
|
417
|
+
| `i18n()` | `wu-lang` / `wu-text-key` / `wu.t()` |
|
|
418
|
+
| `flash()` | `wu-flash` |
|
|
419
|
+
|
|
420
|
+
### Full Example
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { Router, html, raw, theme, i18n, flash, wfuwAssets } from 'weifuwu'
|
|
424
|
+
|
|
425
|
+
const app = new Router()
|
|
426
|
+
app.use(theme())
|
|
427
|
+
app.use(i18n({ dir: './locales' }))
|
|
428
|
+
app.use(flash())
|
|
429
|
+
app.use('/', wfuwAssets())
|
|
430
|
+
|
|
431
|
+
app.get(
|
|
432
|
+
'/',
|
|
433
|
+
(req, ctx) => html`
|
|
434
|
+
<!DOCTYPE html>
|
|
435
|
+
<html data-theme="${ctx.theme.value}">
|
|
436
|
+
<head>
|
|
437
|
+
<meta charset="utf-8" />
|
|
438
|
+
<title>${ctx.i18n.t('app.title')}</title>
|
|
439
|
+
<link rel="stylesheet" href="/__wfw/css/weifuwu-ui.css" />
|
|
440
|
+
<script src="/__wfw/js/weifuwu-ui.js"></script>
|
|
441
|
+
<script id="__wfw-i18n" type="application/json">
|
|
442
|
+
${raw(JSON.stringify(ctx.i18n.messages))}
|
|
443
|
+
</script>
|
|
444
|
+
${ctx.flash.value &&
|
|
445
|
+
raw(
|
|
446
|
+
`<script id="__wfw-flash" type="application/json">${JSON.stringify(ctx.flash.value)}</script>`,
|
|
447
|
+
)}
|
|
448
|
+
</head>
|
|
449
|
+
<body data-locale="${ctx.i18n.locale}">
|
|
450
|
+
<nav class="wu-flex wu-items-center wu-justify-between wu-p-4 wu-border-bottom">
|
|
451
|
+
<strong>My App</strong>
|
|
452
|
+
<div class="wu-flex wu-gap-sm">
|
|
453
|
+
<button wu-theme="dark">🌙</button>
|
|
454
|
+
<button wu-theme="light">☀️</button>
|
|
455
|
+
<button wu-lang="zh-CN">中文</button>
|
|
456
|
+
<button wu-lang="en">EN</button>
|
|
457
|
+
</div>
|
|
458
|
+
</nav>
|
|
459
|
+
|
|
460
|
+
<div wu-flash></div>
|
|
461
|
+
|
|
462
|
+
<main class="wu-p-4">
|
|
463
|
+
<h1 class="wu-text-2xl">${ctx.i18n.t('dashboard.title')}</h1>
|
|
464
|
+
<div wu-get="/partials/stats" wu-trigger="load" class="wu-mt-4">
|
|
465
|
+
<div class="wu-skeleton" style="height:100px"></div>
|
|
466
|
+
</div>
|
|
467
|
+
</main>
|
|
468
|
+
</body>
|
|
469
|
+
</html>
|
|
470
|
+
`,
|
|
471
|
+
)
|
|
472
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -66,3 +66,4 @@ export { loadModule, clearModuleCache } from './ssr/compile.ts';
|
|
|
66
66
|
export { cssContext, cssRouter, clearCSSCache } from './ssr/css.ts';
|
|
67
67
|
export type { CssAsset } from './ssr/css.ts';
|
|
68
68
|
export { assetRouter, assetScripts } from './ssr/assets.ts';
|
|
69
|
+
export { wfuwAssets } from './ssr/ui/assets.ts';
|