@wangs-ui/skills 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: layout-navigation
3
+ description: Guidelines and patterns for page layout, sidebar navigation, breadcrumbs, and tabs using Wangs UI layout blocks.
4
+ ---
5
+
6
+ # Skill: Layout & Navigation Structure
7
+
8
+ Use this skill when constructing application shells, multi-level sidebars, page headers, breadcrumbs, or tabbed views with `@wangs-ui/react-core`.
9
+
10
+ ---
11
+
12
+ ## 1. App Shell Pattern
13
+
14
+ ```tsx
15
+ import React from 'react';
16
+ import AppLayout from '@wangs-ui/react-core/blocks/applayout';
17
+ import Sidebar from '@wangs-ui/react-core/blocks/sidebar';
18
+ import Breadcrumb from '@wangs-ui/react-core/primitive/breadcrumb';
19
+ import { HomeLine, UserLine, SettingsLine } from '@wangs-ui/react-icons';
20
+ import { useI18n } from '@wangs-ui/react-i18n';
21
+
22
+ export default function MainAppShell({ children }: { children: React.ReactNode }) {
23
+ const { t } = useI18n();
24
+
25
+ const navigationItems = [
26
+ { label: t('Dashboard'), icon: <HomeLine />, href: '/dashboard' },
27
+ { label: t('Users'), icon: <UserLine />, href: '/users' },
28
+ { label: t('Settings'), icon: <SettingsLine />, href: '/settings' },
29
+ ];
30
+
31
+ return (
32
+ <AppLayout
33
+ sidebar={<Sidebar items={navigationItems} />}
34
+ header={
35
+ <header className="flex h-14 items-center justify-between border-b border-secondary-200 px-6">
36
+ <Breadcrumb model={[{ label: t('Home') }, { label: t('Dashboard') }]} />
37
+ </header>
38
+ }
39
+ >
40
+ <main className="p-6">{children}</main>
41
+ </AppLayout>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 2. Best Practices
49
+
50
+ 1. **Page Title & Breadcrumb Alignment**:
51
+ Every page view inside the layout should provide clear `.heading-1` hierarchy and synchronized breadcrumbs.
52
+ 2. **Spacing Grid Consistency**:
53
+ Use consistent outer container padding (`p-6` / `p-xxl`) across views.
54
+ 3. **Tabbed Subviews**:
55
+ When separating complex forms or detail views, use `<Tabs>` component with controlled tab index.
56
+
57
+ ---
58
+
59
+ ## 3. MCP Navigation & Block Inspection
60
+
61
+ To inspect complete navigation options, badge counters, collapsible sidebars, or responsive header controls:
62
+
63
+ - Call MCP tool `get-documentation({ id: "sidebar" })` or `get-documentation({ id: "breadcrumb" })` to view full configuration options and live stories.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: wangs-ui-components
3
+ description: Foundational rules, subpath imports, design tokens, and the MCP Discovery Protocol for building React apps with Wangs UI.
4
+ ---
5
+
6
+ # Skill: Wangs UI Component Fundamentals & MCP Protocol
7
+
8
+ Use this skill whenever you write or modify UI components using Wangs UI (`@wangs-ui/react-core`, `@wangs-ui/react-icons`, `@wangs-ui/react-presets`).
9
+
10
+ ---
11
+
12
+ ## 1. The MCP Discovery Protocol (Mandatory Before Writing Code)
13
+
14
+ Do **NOT** guess component props, Pass-Through (`pt`) slots, or event names. Follow this discovery protocol:
15
+
16
+ ```mermaid
17
+ graph TD
18
+ A[Identify Component Needed] --> B[Call get-documentation id]
19
+ B --> C{Need live story / variant?}
20
+ C -->|Yes| D[Call get-documentation-for-story]
21
+ C -->|No| E[Check Graphify: query_graph]
22
+ D --> E
23
+ E --> F[Implement Component with Subpath Imports]
24
+ ```
25
+
26
+ 1. **Step 1: Inspect Props & Types**:
27
+ Call `get-documentation({ id: "<component-name>" })` (e.g. `button`, `inputtext`, `datatable`) to get the exact prop interfaces, severity variants, and sizes.
28
+ 2. **Step 2: Inspect Live Usage & Slots**:
29
+ Call `get-documentation-for-story({ id: "<component-name>", storyName: "<variant>" })` to view how props, icons, and pass-through (`pt`) classes are composed in real code.
30
+ 3. **Step 3: Inspect Codebase Relationships**:
31
+ Call `query_graph({ query: "<ComponentName>" })` to see how other parts of the monorepo compose this component.
32
+
33
+ ---
34
+
35
+ ## 2. Subpath Modular Imports (Mandatory)
36
+
37
+ Always import via specific subpaths to guarantee tree-shaking and avoid bundling entire packages:
38
+
39
+ ```tsx
40
+ // Primitives
41
+ import Button from '@wangs-ui/react-core/primitive/button';
42
+ import Card from '@wangs-ui/react-core/primitive/card';
43
+ import InputText from '@wangs-ui/react-core/primitive/inputtext';
44
+ import Select from '@wangs-ui/react-core/primitive/select';
45
+ import Tag from '@wangs-ui/react-core/primitive/tag';
46
+
47
+ // Providers & Hooks
48
+ import { WangsUiProvider } from '@wangs-ui/react-core/api';
49
+ import { useI18n } from '@wangs-ui/react-i18n';
50
+
51
+ // Icons
52
+ import { SearchLine, AddLine, DeleteBin6Line, CheckLine } from '@wangs-ui/react-icons';
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 3. Strict Primitive Substitution Rule
58
+
59
+ Never write raw HTML when a Wangs UI primitive exists:
60
+
61
+ | Forbidden Raw HTML | Mandatory Wangs UI Component | Subpath Import |
62
+ | :------------------------ | :--------------------------- | :------------------------------------------- |
63
+ | `<button>` | `Button` | `@wangs-ui/react-core/primitive/button` |
64
+ | `<input type="text">` | `InputText` | `@wangs-ui/react-core/primitive/inputtext` |
65
+ | `<input type="number">` | `InputNumber` | `@wangs-ui/react-core/primitive/inputnumber` |
66
+ | `<input type="checkbox">` | `Checkbox` | `@wangs-ui/react-core/primitive/checkbox` |
67
+ | `<select>` | `Select` | `@wangs-ui/react-core/primitive/select` |
68
+ | `<dialog>` / modal | `Dialog` / `Modal` | `@wangs-ui/react-core/primitive/dialog` |
69
+ | `<table>` | `DataTable` | `@wangs-ui/react-core/primitive/datatable` |
70
+ | Container box | `Card` | `@wangs-ui/react-core/primitive/card` |
71
+ | Pill badge | `Tag` / `Badge` | `@wangs-ui/react-core/primitive/tag` |
72
+
73
+ ---
74
+
75
+ ## 4. Typography Scale & 4px Spacing Tokens
76
+
77
+ ### Typography Helper Classes
78
+
79
+ - `.heading-1` — Page title (22px, 600)
80
+ - `.heading-2` — Section / Card title (18px, 600)
81
+ - `.heading-3` — Sub-header (16px, 500)
82
+ - `.heading-4` — Field label (14px, 500)
83
+ - `.heading-5` — Small group header (12px, 600)
84
+ - `.p` — Body copy (12px, 500)
85
+
86
+ ### 4px Spacing Tokens
87
+
88
+ - Gap: `gap-xs` (4px), `gap-s` (6px), `gap-md` (8px), `gap-m` (12px), `gap-l` (16px), `gap-xl` (20px), `gap-xxl` (24px)
89
+ - Padding: `p-xs`, `p-s`, `p-md`, `p-m`, `p-l`, `p-xl`, `p-xxl`
@@ -0,0 +1,210 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { cancel, intro, isCancel, multiselect, outro } from "@clack/prompts";
5
+ //#region src/registry.ts
6
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ function getSkillsDir() {
8
+ const candidates = [
9
+ path.resolve(__dirname, "skills"),
10
+ path.resolve(__dirname, "../skills"),
11
+ path.resolve(__dirname, "../../skills")
12
+ ];
13
+ for (const c of candidates) if (fs.existsSync(c)) return c;
14
+ return path.resolve(__dirname, "skills");
15
+ }
16
+ function loadAllSkills() {
17
+ const skillsDir = getSkillsDir();
18
+ if (!fs.existsSync(skillsDir)) return [];
19
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
20
+ const skills = [];
21
+ for (const entry of entries) if (entry.isDirectory()) {
22
+ const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
23
+ if (fs.existsSync(skillMdPath)) {
24
+ const content = fs.readFileSync(skillMdPath, "utf-8");
25
+ const { name: entryName } = entry;
26
+ let name = entryName;
27
+ let description = "Wangs UI consumer skill";
28
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
29
+ if (frontmatterMatch) {
30
+ const fm = frontmatterMatch[1];
31
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
32
+ const descMatch = fm.match(/^description:\s*(.+)$/m);
33
+ if (nameMatch) name = nameMatch[1].trim();
34
+ if (descMatch) description = descMatch[1].trim();
35
+ }
36
+ skills.push({
37
+ id: entry.name,
38
+ name,
39
+ description,
40
+ content
41
+ });
42
+ }
43
+ }
44
+ return skills;
45
+ }
46
+ function getSkill(id) {
47
+ return loadAllSkills().find((s) => s.id === id || s.name === id);
48
+ }
49
+ //#endregion
50
+ //#region src/detector.ts
51
+ function getAgentSkillDirs(baseDir = process.cwd()) {
52
+ const dirs = [];
53
+ const candidates = [
54
+ path.join(baseDir, ".agents", "skills"),
55
+ path.join(baseDir, ".claude", "skills"),
56
+ path.join(baseDir, ".opencode", "skills"),
57
+ path.join(baseDir, ".kilo", "skills")
58
+ ];
59
+ for (const c of candidates) if (fs.existsSync(c) || fs.existsSync(path.dirname(c))) dirs.push(c);
60
+ if (dirs.length === 0) dirs.push(path.join(baseDir, ".agents", "skills"));
61
+ return dirs;
62
+ }
63
+ function isSkillInstalled(skillId, baseDir = process.cwd()) {
64
+ const dirs = getAgentSkillDirs(baseDir);
65
+ for (const d of dirs) {
66
+ const skillPath = path.join(d, skillId, "SKILL.md");
67
+ if (fs.existsSync(skillPath)) return true;
68
+ }
69
+ return false;
70
+ }
71
+ function getInstalledSkills(baseDir = process.cwd()) {
72
+ const dirs = getAgentSkillDirs(baseDir);
73
+ const installed = /* @__PURE__ */ new Set();
74
+ for (const d of dirs) if (fs.existsSync(d)) {
75
+ const entries = fs.readdirSync(d, { withFileTypes: true });
76
+ for (const entry of entries) if (entry.isDirectory()) {
77
+ const skillMd = path.join(d, entry.name, "SKILL.md");
78
+ if (fs.existsSync(skillMd)) installed.add(entry.name);
79
+ }
80
+ }
81
+ return Array.from(installed);
82
+ }
83
+ function installSkill(skill, baseDir = process.cwd()) {
84
+ const targetDirs = getAgentSkillDirs(baseDir);
85
+ const writtenPaths = [];
86
+ for (const baseSkillDir of targetDirs) {
87
+ const destDir = path.join(baseSkillDir, skill.id);
88
+ fs.mkdirSync(destDir, { recursive: true });
89
+ const destFile = path.join(destDir, "SKILL.md");
90
+ fs.writeFileSync(destFile, skill.content, "utf-8");
91
+ writtenPaths.push(destFile);
92
+ }
93
+ return writtenPaths;
94
+ }
95
+ function removeSkill(skillId, baseDir = process.cwd()) {
96
+ const targetDirs = getAgentSkillDirs(baseDir);
97
+ const removedPaths = [];
98
+ for (const baseSkillDir of targetDirs) {
99
+ const destDir = path.join(baseSkillDir, skillId);
100
+ if (fs.existsSync(destDir)) {
101
+ fs.rmSync(destDir, {
102
+ recursive: true,
103
+ force: true
104
+ });
105
+ removedPaths.push(destDir);
106
+ }
107
+ }
108
+ return removedPaths;
109
+ }
110
+ //#endregion
111
+ //#region src/commands/list.ts
112
+ function listSkills(baseDir = process.cwd()) {
113
+ intro("\x1B[1m\x1B[36m📦 Wangs UI Consumer Skills Registry\x1B[0m");
114
+ const allSkills = loadAllSkills();
115
+ const targetDirs = getAgentSkillDirs(baseDir);
116
+ if (allSkills.length === 0) {
117
+ console.log("No skills found in registry.");
118
+ outro("Done.");
119
+ return;
120
+ }
121
+ console.log(`\nAgent directories: \x1b[2m${targetDirs.join(", ")}\x1b[0m\n`);
122
+ for (const skill of allSkills) {
123
+ const statusBadge = isSkillInstalled(skill.id, baseDir) ? "\x1B[32m[Installed]\x1B[0m" : "\x1B[90m[Available]\x1B[0m";
124
+ console.log(` ${statusBadge} \x1b[1m${skill.id}\x1b[0m`);
125
+ console.log(` \x1b[90m${skill.description}\x1b[0m\n`);
126
+ }
127
+ outro(`Total skills: ${allSkills.length} | Run \x1b[36mnpx @wangs-ui/skills add <skill-name>\x1b[0m to install.`);
128
+ }
129
+ //#endregion
130
+ //#region src/commands/add.ts
131
+ async function addSkills(skillIds, baseDir = process.cwd()) {
132
+ intro("\x1B[1m\x1B[36m➕ Install Wangs UI Consumer Skills\x1B[0m");
133
+ const allSkills = loadAllSkills();
134
+ let targetIds = skillIds.filter(Boolean);
135
+ if (targetIds.length === 0) {
136
+ const selected = await multiselect({
137
+ message: "Select Wangs UI skills to install into your agent environment:",
138
+ options: allSkills.map((s) => ({
139
+ value: s.id,
140
+ label: s.id,
141
+ hint: s.description
142
+ })),
143
+ required: true
144
+ });
145
+ if (isCancel(selected)) {
146
+ cancel("Operation cancelled.");
147
+ return;
148
+ }
149
+ targetIds = selected;
150
+ }
151
+ const installedList = [];
152
+ for (const id of targetIds) {
153
+ const skill = getSkill(id);
154
+ if (!skill) {
155
+ console.log(`\x1b[33m⚠️ Skill "${id}" not found in registry. Run "list" to view available skills.\x1b[0m`);
156
+ continue;
157
+ }
158
+ const paths = installSkill(skill, baseDir);
159
+ installedList.push(skill.id);
160
+ for (const p of paths) console.log(` \x1b[32m✔\x1b[0m Installed \x1b[1m${skill.id}\x1b[0m -> \x1b[2m${p}\x1b[0m`);
161
+ }
162
+ if (installedList.length > 0) outro(`\x1b[32mSuccessfully installed ${installedList.length} skill(s)!\x1b[0m`);
163
+ else outro("No skills were installed.");
164
+ }
165
+ //#endregion
166
+ //#region src/commands/update.ts
167
+ function updateSkills(skillIds = [], baseDir = process.cwd()) {
168
+ intro("\x1B[1m\x1B[36m🔄 Update Wangs UI Consumer Skills\x1B[0m");
169
+ let targets = skillIds.filter(Boolean);
170
+ if (targets.length === 0) targets = getInstalledSkills(baseDir);
171
+ if (targets.length === 0) {
172
+ console.log("No installed skills found in project to update.");
173
+ outro("Done.");
174
+ return;
175
+ }
176
+ const updatedList = [];
177
+ for (const id of targets) {
178
+ const skill = getSkill(id);
179
+ if (!skill) {
180
+ console.log(`\x1b[33m⚠️ Skill "${id}" not found in current registry.\x1b[0m`);
181
+ continue;
182
+ }
183
+ const paths = installSkill(skill, baseDir);
184
+ updatedList.push(skill.id);
185
+ for (const p of paths) console.log(` \x1b[32m✔\x1b[0m Updated \x1b[1m${skill.id}\x1b[0m -> \x1b[2m${p}\x1b[0m`);
186
+ }
187
+ outro(`\x1b[32mSuccessfully updated ${updatedList.length} skill(s)!\x1b[0m`);
188
+ }
189
+ //#endregion
190
+ //#region src/commands/remove.ts
191
+ function removeSkills(skillIds, baseDir = process.cwd()) {
192
+ intro("\x1B[1m\x1B[31m🗑️ Remove Wangs UI Consumer Skills\x1B[0m");
193
+ const targets = skillIds.filter(Boolean);
194
+ if (targets.length === 0) {
195
+ console.log("Please specify skill name(s) to remove.");
196
+ outro("Aborted.");
197
+ return;
198
+ }
199
+ const removedList = [];
200
+ for (const id of targets) {
201
+ const paths = removeSkill(id, baseDir);
202
+ if (paths.length > 0) {
203
+ removedList.push(id);
204
+ for (const p of paths) console.log(` \x1b[31m✔\x1b[0m Removed \x1b[1m${id}\x1b[0m from \x1b[2m${p}\x1b[0m`);
205
+ } else console.log(` \x1b[90m- Skill "${id}" was not installed.\x1b[0m`);
206
+ }
207
+ outro(`\x1b[32mCompleted. Removed ${removedList.length} skill(s).\x1b[0m`);
208
+ }
209
+ //#endregion
210
+ export { getAgentSkillDirs as a, isSkillInstalled as c, getSkillsDir as d, loadAllSkills as f, listSkills as i, removeSkill as l, updateSkills as n, getInstalledSkills as o, addSkills as r, installSkill as s, removeSkills as t, getSkill as u };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@wangs-ui/skills",
3
+ "version": "1.0.36",
4
+ "description": "CLI to install, update, and manage modular AI agent skills for Wangs UI React applications",
5
+ "keywords": [
6
+ "agents",
7
+ "ai",
8
+ "antigravity",
9
+ "claude-code",
10
+ "cli",
11
+ "cursor",
12
+ "kilo",
13
+ "mcp",
14
+ "opencode",
15
+ "skills",
16
+ "wangs-ui"
17
+ ],
18
+ "homepage": "https://github.com/fewangsit/wangs-ui-react",
19
+ "license": "MIT",
20
+ "author": "Wangsit FE Developer",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/fewangsit/wangs-ui-react.git",
24
+ "directory": "packages/skills"
25
+ },
26
+ "bin": {
27
+ "skills": "dist/bin.js",
28
+ "wangs-ui-skills": "dist/bin.js"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "sideEffects": false,
35
+ "files": [
36
+ "dist",
37
+ "skills"
38
+ ],
39
+ "type": "module",
40
+ "main": "./dist/index.js",
41
+ "module": "./dist/index.js",
42
+ "exports": {
43
+ ".": {
44
+ "import": "./dist/index.js",
45
+ "default": "./dist/index.js"
46
+ },
47
+ "./package.json": "./package.json"
48
+ },
49
+ "dependencies": {
50
+ "@clack/prompts": "^1.7.0"
51
+ },
52
+ "devDependencies": {},
53
+ "scripts": {
54
+ "build": "vite build",
55
+ "check:publint": "publint",
56
+ "check:attw": "attw --pack ."
57
+ }
58
+ }
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: create-form
3
+ description: Real-world patterns for building strongly-typed forms, multipart file uploads, server validation mapping, and dirty tracking with @wangs-ui/form.
4
+ ---
5
+
6
+ # Skill: Real-World Form Workflows with `@wangs-ui/form`
7
+
8
+ Use this skill when building CRUD forms, data entry dialogs, multi-field settings pages, or multipart forms in Wangs UI applications.
9
+
10
+ ---
11
+
12
+ ## 1. MCP Inspection Step (Before Building Fields)
13
+
14
+ Before implementing specific input controls, query the MCP server to inspect their exact prop signatures:
15
+
16
+ - `get-documentation({ id: "select" })` — Check option formats (`{ label, value }` vs object).
17
+ - `get-documentation({ id: "datepicker" })` — Check `selectionMode` (`single`, `range`, `multiple`) and date format props.
18
+ - `get-documentation({ id: "multiselect" })` — Check chips display and filter behavior.
19
+ - `get-documentation({ id: "fileupload" })` — Check accepted mime types and upload handlers.
20
+
21
+ ---
22
+
23
+ ## 2. Recipe 1: Standard CRUD Form with Server Validation Mapping
24
+
25
+ Real-world forms must handle async submission, loading state, and map backend validation errors back into `<Field>` errors:
26
+
27
+ ```tsx
28
+ import React, { useState } from 'react';
29
+ import Card from '@wangs-ui/react-core/primitive/card';
30
+ import Button from '@wangs-ui/react-core/primitive/button';
31
+ import InputText from '@wangs-ui/react-core/primitive/inputtext';
32
+ import Select from '@wangs-ui/react-core/primitive/select';
33
+ import { Form, Field } from '@wangs-ui/react-core';
34
+ import { useFormControl } from '@wangs-ui/form';
35
+ import { useI18n } from '@wangs-ui/react-i18n';
36
+
37
+ interface UserFormValues {
38
+ fullName: string;
39
+ email: string;
40
+ role: string;
41
+ }
42
+
43
+ export default function UserForm({ onSuccess }: { onSuccess?: () => void }) {
44
+ const { t } = useI18n();
45
+ const formControl = useFormControl<UserFormValues>({ type: 'json' });
46
+ const [isSubmitting, setIsSubmitting] = useState(false);
47
+
48
+ const roleOptions = [
49
+ { label: t('Administrator'), value: 'admin' },
50
+ { label: t('Operator'), value: 'operator' },
51
+ { label: t('Viewer'), value: 'viewer' },
52
+ ];
53
+
54
+ const handleSubmit = async (values: UserFormValues) => {
55
+ setIsSubmitting(true);
56
+ try {
57
+ // Execute API call: await api.createUser(values);
58
+ onSuccess?.();
59
+ } catch (err: any) {
60
+ // Map server validation error directly to field
61
+ if (err?.fieldErrors?.email) {
62
+ formControl.setError('email', {
63
+ type: 'server',
64
+ message: err.fieldErrors.email,
65
+ });
66
+ }
67
+ } finally {
68
+ setIsSubmitting(false);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <Card className="p-6">
74
+ <Form control={formControl} onSubmit={handleSubmit} className="flex flex-col gap-m">
75
+ <h2 className="heading-2">{t('User Information')}</h2>
76
+
77
+ {/* Text Field */}
78
+ <Field<string>
79
+ name="fullName"
80
+ label={t('Full Name')}
81
+ required
82
+ rules={{ required: t('Full name is required') }}
83
+ >
84
+ {(field) => (
85
+ <InputText {...field} placeholder={t('e.g. Jane Doe')} value={field.value || ''} />
86
+ )}
87
+ </Field>
88
+
89
+ {/* Email Field with Regex Validation */}
90
+ <Field<string>
91
+ name="email"
92
+ label={t('Email Address')}
93
+ required
94
+ rules={{
95
+ required: t('Email is required'),
96
+ pattern: {
97
+ value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
98
+ message: t('Enter a valid email address'),
99
+ },
100
+ }}
101
+ >
102
+ {(field) => (
103
+ <InputText {...field} placeholder={t('name@company.com')} value={field.value || ''} />
104
+ )}
105
+ </Field>
106
+
107
+ {/* Select Dropdown Field */}
108
+ <Field<string>
109
+ name="role"
110
+ label={t('Access Role')}
111
+ required
112
+ rules={{ required: t('Role selection is required') }}
113
+ >
114
+ {(field) => (
115
+ <Select
116
+ {...field}
117
+ options={roleOptions}
118
+ placeholder={t('Select role')}
119
+ value={field.value}
120
+ onChange={(e) => field.onChange(e.value)}
121
+ />
122
+ )}
123
+ </Field>
124
+
125
+ {/* Form Actions */}
126
+ <div className="flex justify-end gap-s pt-s">
127
+ <Button
128
+ type="button"
129
+ variant="text"
130
+ label={t('Reset')}
131
+ onClick={() => formControl.reset()}
132
+ disabled={isSubmitting}
133
+ />
134
+ <Button type="submit" label={t('Save User')} severity="primary" loading={isSubmitting} />
135
+ </div>
136
+ </Form>
137
+ </Card>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 3. Recipe 2: Multipart Form (File Upload + Metadata)
145
+
146
+ For file uploads (avatar, attachment, documents), set `type: 'formdata'`:
147
+
148
+ ```tsx
149
+ import { useFormControl } from '@wangs-ui/form';
150
+
151
+ interface ProfileUploadValues {
152
+ displayName: string;
153
+ avatar: File | null;
154
+ }
155
+
156
+ // Generates FormData under the hood
157
+ const formControl = useFormControl<ProfileUploadValues>({ type: 'formdata' });
158
+ ```
159
+
160
+ ---
161
+
162
+ ## 4. Key Implementation Rules
163
+
164
+ 1. **Always Use `<Form>` & `<Field>`**: Never bind raw uncontrolled inputs.
165
+ 2. **Translate All Labels & Error Messages**: Wrap text in `t('...')`.
166
+ 3. **Handle Loading State**: Disable reset buttons and set `loading={isSubmitting}` on submit buttons.