@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.
- package/dist/bin.js +84 -0
- package/dist/index.js +2 -0
- package/dist/skills/create-form/SKILL.md +166 -0
- package/dist/skills/data-table/SKILL.md +178 -0
- package/dist/skills/dialog-modal/SKILL.md +131 -0
- package/dist/skills/i18n-usage/SKILL.md +87 -0
- package/dist/skills/layout-navigation/SKILL.md +63 -0
- package/dist/skills/wangs-ui-components/SKILL.md +89 -0
- package/dist/src-C22mKmnQ.js +210 -0
- package/package.json +58 -0
- package/skills/create-form/SKILL.md +166 -0
- package/skills/data-table/SKILL.md +178 -0
- package/skills/dialog-modal/SKILL.md +131 -0
- package/skills/i18n-usage/SKILL.md +87 -0
- package/skills/layout-navigation/SKILL.md +63 -0
- package/skills/wangs-ui-components/SKILL.md +89 -0
|
@@ -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.
|