@useworkapp/construct-skill 1.0.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 ADDED
@@ -0,0 +1,181 @@
1
+ # @useworkapp/construct-skill
2
+
3
+ AI coding assistant skill for building [Work](https://usework.app) platform Constructs. Compatible with **Claude Code** and **OpenCode**.
4
+
5
+ ## What is a Construct?
6
+
7
+ A Construct is a self-contained React micro-app that runs on the Work streaming platform. Think of it like a song on Spotify - users stream Constructs to solve business problems, and Runners (developers) earn from every stream.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install -g @useworkapp/construct-skill
13
+ ```
14
+
15
+ The skill will auto-detect your AI coding tools and install automatically.
16
+
17
+ ### Manual Installation
18
+
19
+ ```bash
20
+ # Install for Claude Code
21
+ construct-skill install claude-code
22
+
23
+ # Install for OpenCode
24
+ construct-skill install opencode
25
+
26
+ # Install for both
27
+ construct-skill install all
28
+
29
+ # Install to current project instead of global
30
+ construct-skill install claude-code --project
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### With Claude Code
36
+
37
+ The skill is automatically invoked when you ask Claude to:
38
+ - "Build me an expense tracker Construct"
39
+ - "Review this component for Work standards"
40
+ - "Help me create a Construct for inventory management"
41
+
42
+ ### With OpenCode
43
+
44
+ Use the `/construct` command:
45
+
46
+ ```
47
+ /construct Build a todo list app
48
+ /construct Review this code for Work standards
49
+ ```
50
+
51
+ ### Validate Your Code
52
+
53
+ Check if your Construct follows Work standards:
54
+
55
+ ```bash
56
+ npx @useworkapp/construct-skill validate ./App.tsx
57
+ ```
58
+
59
+ Output:
60
+ ```
61
+ ╔══════════════════════════════════════════════════════════════╗
62
+ ║ WORK CONSTRUCT VALIDATOR ║
63
+ ╚══════════════════════════════════════════════════════════════╝
64
+
65
+ File: ./App.tsx
66
+
67
+ WARNINGS:
68
+ ⚠ Consider adding responsive breakpoints
69
+
70
+ ✓ Construct passes required validations (with warnings)
71
+
72
+ ────────────────────────────────────────────────────────────
73
+ Status: PASS
74
+ Errors: 0 | Warnings: 1
75
+ ────────────────────────────────────────────────────────────
76
+ ```
77
+
78
+ ## Construct Requirements
79
+
80
+ All Constructs must follow these standards:
81
+
82
+ | Requirement | Description |
83
+ |------------|-------------|
84
+ | **Single File** | One `App.tsx` with default export |
85
+ | **shadcn/ui** | Only use approved components |
86
+ | **localStorage** | Persist data with `work-construct-` prefix |
87
+ | **Tailwind CSS** | No inline styles or CSS modules |
88
+ | **Responsive** | Use `sm:`, `md:`, `lg:` breakpoints |
89
+ | **Dark Mode** | Support via CSS variables |
90
+ | **Accessible** | aria-labels, semantic HTML, labels |
91
+ | **No API Calls** | No fetch, axios, or external requests |
92
+
93
+ ## Available Components
94
+
95
+ ```tsx
96
+ // Layout
97
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
98
+
99
+ // Forms
100
+ import { Button, Input, Textarea, Label } from "@/components/ui/...";
101
+ import { Select, Checkbox, Switch } from "@/components/ui/...";
102
+
103
+ // Feedback
104
+ import { Badge, Alert, Progress } from "@/components/ui/...";
105
+
106
+ // Data Display
107
+ import { Table, Tabs } from "@/components/ui/...";
108
+
109
+ // Overlay
110
+ import { Dialog, DropdownMenu } from "@/components/ui/...";
111
+
112
+ // Icons (Lucide only)
113
+ import { Plus, Trash2, Edit } from "lucide-react";
114
+ ```
115
+
116
+ ## Example Construct
117
+
118
+ ```tsx
119
+ "use client";
120
+
121
+ import { useState, useEffect } from "react";
122
+ import { Button } from "@/components/ui/button";
123
+ import { Input } from "@/components/ui/input";
124
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
125
+ import { Plus } from "lucide-react";
126
+
127
+ const STORAGE_KEY = "work-construct-my-app";
128
+
129
+ export default function App() {
130
+ const [items, setItems] = useState<string[]>(() => {
131
+ if (typeof window === "undefined") return [];
132
+ const saved = localStorage.getItem(STORAGE_KEY);
133
+ return saved ? JSON.parse(saved) : [];
134
+ });
135
+
136
+ useEffect(() => {
137
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
138
+ }, [items]);
139
+
140
+ return (
141
+ <div className="min-h-screen bg-background p-4 sm:p-6">
142
+ <Card className="mx-auto max-w-md">
143
+ <CardHeader>
144
+ <CardTitle>My Construct</CardTitle>
145
+ </CardHeader>
146
+ <CardContent>
147
+ {/* Your app here */}
148
+ </CardContent>
149
+ </Card>
150
+ </div>
151
+ );
152
+ }
153
+ ```
154
+
155
+ ## Commands
156
+
157
+ | Command | Description |
158
+ |---------|-------------|
159
+ | `construct-skill install <platform>` | Install skill for Claude Code or OpenCode |
160
+ | `construct-skill uninstall <platform>` | Remove skill |
161
+ | `construct-skill validate <file>` | Validate a Construct file |
162
+ | `construct-skill detect` | Show detected AI coding tools |
163
+ | `construct-skill help` | Show help |
164
+
165
+ ## Become a Runner
166
+
167
+ Runners are developers who build Constructs and earn from every stream.
168
+
169
+ - **60% revenue share** (70% for Founders Protocol members)
170
+ - Build once, earn forever
171
+ - Join at [usework.app/runner](https://usework.app/runner)
172
+
173
+ ## Links
174
+
175
+ - [Work Platform](https://usework.app)
176
+ - [Runner Documentation](https://usework.app/docs/runners)
177
+ - [Construct Guidelines](https://usework.app/docs/runners/constructs)
178
+
179
+ ## License
180
+
181
+ MIT
package/bin/install.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const PACKAGE_ROOT = path.join(__dirname, "..");
8
+
9
+ const TARGETS = {
10
+ "claude-code": {
11
+ name: "Claude Code",
12
+ globalDir: path.join(os.homedir(), ".claude", "skills"),
13
+ projectDir: ".claude/skills",
14
+ sourceDir: path.join(PACKAGE_ROOT, "claude-code"),
15
+ },
16
+ opencode: {
17
+ name: "OpenCode",
18
+ globalDir: path.join(os.homedir(), ".config", "opencode", "commands"),
19
+ projectDir: ".opencode/commands",
20
+ sourceDir: path.join(PACKAGE_ROOT, "opencode"),
21
+ },
22
+ };
23
+
24
+ function copyRecursive(src, dest) {
25
+ if (!fs.existsSync(src)) {
26
+ return false;
27
+ }
28
+
29
+ const stats = fs.statSync(src);
30
+
31
+ if (stats.isDirectory()) {
32
+ fs.mkdirSync(dest, { recursive: true });
33
+ const files = fs.readdirSync(src);
34
+ for (const file of files) {
35
+ copyRecursive(path.join(src, file), path.join(dest, file));
36
+ }
37
+ } else {
38
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
39
+ fs.copyFileSync(src, dest);
40
+ }
41
+
42
+ return true;
43
+ }
44
+
45
+ function detectPlatforms() {
46
+ const detected = [];
47
+
48
+ // Check for Claude Code
49
+ const claudeConfig = path.join(os.homedir(), ".claude");
50
+ if (fs.existsSync(claudeConfig)) {
51
+ detected.push("claude-code");
52
+ }
53
+
54
+ // Check for OpenCode
55
+ const opencodeConfig = path.join(os.homedir(), ".config", "opencode");
56
+ if (fs.existsSync(opencodeConfig)) {
57
+ detected.push("opencode");
58
+ }
59
+
60
+ return detected;
61
+ }
62
+
63
+ function install(platform, scope = "global") {
64
+ const target = TARGETS[platform];
65
+ if (!target) {
66
+ console.error(`Unknown platform: ${platform}`);
67
+ console.error(`Available platforms: ${Object.keys(TARGETS).join(", ")}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const destDir = scope === "project" ? target.projectDir : target.globalDir;
72
+
73
+ console.log(`Installing ${target.name} skill...`);
74
+ console.log(` Source: ${target.sourceDir}`);
75
+ console.log(` Destination: ${destDir}`);
76
+
77
+ if (copyRecursive(target.sourceDir, destDir)) {
78
+ console.log(` ✓ Installed successfully!\n`);
79
+ return true;
80
+ } else {
81
+ console.error(` ✗ Source directory not found\n`);
82
+ return false;
83
+ }
84
+ }
85
+
86
+ function uninstall(platform, scope = "global") {
87
+ const target = TARGETS[platform];
88
+ if (!target) {
89
+ console.error(`Unknown platform: ${platform}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ const destDir = scope === "project" ? target.projectDir : target.globalDir;
94
+
95
+ // For Claude Code, remove the specific skill directory
96
+ const skillDir =
97
+ platform === "claude-code"
98
+ ? path.join(destDir, "construct-builder")
99
+ : path.join(destDir, "construct.md");
100
+
101
+ if (fs.existsSync(skillDir)) {
102
+ if (fs.statSync(skillDir).isDirectory()) {
103
+ fs.rmSync(skillDir, { recursive: true });
104
+ } else {
105
+ fs.unlinkSync(skillDir);
106
+ }
107
+ console.log(`✓ Uninstalled ${target.name} skill from ${destDir}`);
108
+ return true;
109
+ } else {
110
+ console.log(`Skill not found at ${skillDir}`);
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function printUsage() {
116
+ console.log(`
117
+ @usework/construct-skill - AI skill for building Work Constructs
118
+
119
+ USAGE:
120
+ construct-skill [command] [options]
121
+
122
+ COMMANDS:
123
+ install [platform] Install skill for specified platform
124
+ uninstall [platform] Remove skill for specified platform
125
+ validate <file> Validate a Construct file
126
+ detect Show detected AI coding tools
127
+ help Show this help message
128
+
129
+ PLATFORMS:
130
+ claude-code Install for Claude Code (~/.claude/skills/)
131
+ opencode Install for OpenCode (~/.config/opencode/commands/)
132
+ all Install for all detected platforms
133
+
134
+ OPTIONS:
135
+ --global Install to user home directory (default)
136
+ --project Install to current project directory
137
+ --auto Auto-detect and install (used by postinstall)
138
+
139
+ EXAMPLES:
140
+ construct-skill install claude-code
141
+ construct-skill install opencode --project
142
+ construct-skill install all
143
+ construct-skill uninstall claude-code
144
+ `);
145
+ }
146
+
147
+ function main() {
148
+ const args = process.argv.slice(2);
149
+
150
+ if (args.length === 0 || args.includes("help") || args.includes("--help")) {
151
+ printUsage();
152
+ process.exit(0);
153
+ }
154
+
155
+ const command = args[0];
156
+
157
+ // Route to validate command
158
+ if (command === "validate") {
159
+ require("./validate.js");
160
+ return;
161
+ }
162
+ const platform = args[1];
163
+ const scope = args.includes("--project") ? "project" : "global";
164
+ const auto = args.includes("--auto");
165
+
166
+ switch (command) {
167
+ case "detect":
168
+ const detected = detectPlatforms();
169
+ console.log("\nDetected AI coding tools:");
170
+ if (detected.length === 0) {
171
+ console.log(" None found. Install Claude Code or OpenCode first.\n");
172
+ } else {
173
+ detected.forEach((p) => console.log(` ✓ ${TARGETS[p].name}`));
174
+ console.log();
175
+ }
176
+ break;
177
+
178
+ case "install":
179
+ if (auto) {
180
+ // Auto-install mode (postinstall script)
181
+ const platforms = detectPlatforms();
182
+ if (platforms.length === 0) {
183
+ console.log(
184
+ "\n@usework/construct-skill installed but no AI coding tools detected."
185
+ );
186
+ console.log("Run 'construct-skill install <platform>' after installing Claude Code or OpenCode.\n");
187
+ } else {
188
+ console.log("\n@usework/construct-skill - Auto-installing...\n");
189
+ platforms.forEach((p) => install(p, scope));
190
+ }
191
+ break;
192
+ }
193
+
194
+ if (!platform) {
195
+ console.error("Please specify a platform: claude-code, opencode, or all");
196
+ process.exit(1);
197
+ }
198
+
199
+ if (platform === "all") {
200
+ Object.keys(TARGETS).forEach((p) => install(p, scope));
201
+ } else {
202
+ install(platform, scope);
203
+ }
204
+ break;
205
+
206
+ case "uninstall":
207
+ if (!platform) {
208
+ console.error("Please specify a platform: claude-code, opencode, or all");
209
+ process.exit(1);
210
+ }
211
+
212
+ if (platform === "all") {
213
+ Object.keys(TARGETS).forEach((p) => uninstall(p, scope));
214
+ } else {
215
+ uninstall(platform, scope);
216
+ }
217
+ break;
218
+
219
+ default:
220
+ console.error(`Unknown command: ${command}`);
221
+ printUsage();
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ main();
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Construct Validator
5
+ * Validates that a React component follows Work platform Construct standards
6
+ *
7
+ * Usage: npx @useworkapp/construct-skill validate <path-to-file>
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ const ALLOWED_SHADCN_COMPONENTS = [
14
+ // Layout
15
+ "Card", "CardContent", "CardHeader", "CardTitle", "CardDescription", "CardFooter",
16
+ // Forms
17
+ "Button", "Input", "Textarea", "Label",
18
+ "Select", "SelectContent", "SelectItem", "SelectTrigger", "SelectValue",
19
+ "Checkbox", "RadioGroup", "RadioGroupItem", "Switch",
20
+ // Feedback
21
+ "Badge", "Alert", "AlertDescription", "AlertTitle", "Progress",
22
+ // Data Display
23
+ "Table", "TableBody", "TableCell", "TableHead", "TableHeader", "TableRow",
24
+ "Tabs", "TabsContent", "TabsList", "TabsTrigger",
25
+ // Overlay
26
+ "Dialog", "DialogContent", "DialogDescription", "DialogFooter",
27
+ "DialogHeader", "DialogTitle", "DialogTrigger",
28
+ "DropdownMenu", "DropdownMenuContent", "DropdownMenuItem", "DropdownMenuTrigger",
29
+ // Other
30
+ "Separator", "ScrollArea", "Tooltip", "TooltipContent", "TooltipProvider", "TooltipTrigger",
31
+ ];
32
+
33
+ const FORBIDDEN_PATTERNS = [
34
+ { pattern: /fetch\s*\(/, message: "External API calls (fetch) are not allowed", error: true },
35
+ { pattern: /axios/, message: "External API calls (axios) are not allowed", error: true },
36
+ { pattern: /XMLHttpRequest/, message: "External API calls (XMLHttpRequest) are not allowed", error: true },
37
+ { pattern: /style\s*=\s*\{/, message: "Inline styles detected - use Tailwind classes instead", error: false },
38
+ { pattern: /\.module\.css/, message: "CSS modules are not supported", error: true },
39
+ { pattern: /import.*['"].*\.css['"]/, message: "External CSS imports are not supported", error: true },
40
+ ];
41
+
42
+ const REQUIRED_PATTERNS = [
43
+ { pattern: /export\s+default/, message: "Must have a default export", error: true },
44
+ ];
45
+
46
+ const RECOMMENDED_PATTERNS = [
47
+ { pattern: /localStorage/, message: "Consider using localStorage for data persistence" },
48
+ { pattern: /(sm:|md:|lg:)/, message: "Consider adding responsive breakpoints" },
49
+ { pattern: /(dark:|bg-background|text-foreground)/, message: "Consider adding dark mode support" },
50
+ ];
51
+
52
+ function validateConstruct(filePath) {
53
+ const errors = [];
54
+ const warnings = [];
55
+
56
+ // Check file exists
57
+ if (!fs.existsSync(filePath)) {
58
+ return { valid: false, errors: [`File not found: ${filePath}`], warnings: [] };
59
+ }
60
+
61
+ // Check file extension
62
+ const ext = path.extname(filePath);
63
+ if (ext !== ".tsx" && ext !== ".ts" && ext !== ".jsx" && ext !== ".js") {
64
+ errors.push(`File must be .tsx, .ts, .jsx, or .js - got ${ext}`);
65
+ }
66
+
67
+ const content = fs.readFileSync(filePath, "utf-8");
68
+
69
+ // Check for forbidden patterns
70
+ for (const { pattern, message, error } of FORBIDDEN_PATTERNS) {
71
+ if (pattern.test(content)) {
72
+ if (error) {
73
+ errors.push(message);
74
+ } else {
75
+ warnings.push(message);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Check for required patterns
81
+ for (const { pattern, message } of REQUIRED_PATTERNS) {
82
+ if (!pattern.test(content)) {
83
+ errors.push(message);
84
+ }
85
+ }
86
+
87
+ // Check for recommended patterns
88
+ for (const { pattern, message } of RECOMMENDED_PATTERNS) {
89
+ if (!pattern.test(content)) {
90
+ warnings.push(message);
91
+ }
92
+ }
93
+
94
+ // Check for non-standard component usage
95
+ const jsxComponentRegex = /<([A-Z][a-zA-Z]*)/g;
96
+ const usedComponents = new Set();
97
+ let match;
98
+
99
+ while ((match = jsxComponentRegex.exec(content)) !== null) {
100
+ usedComponents.add(match[1]);
101
+ }
102
+
103
+ // Find locally defined components
104
+ const localComponents = new Set();
105
+ const functionComponentRegex = /(?:function|const)\s+([A-Z][a-zA-Z]*)/g;
106
+ while ((match = functionComponentRegex.exec(content)) !== null) {
107
+ localComponents.add(match[1]);
108
+ }
109
+
110
+ // Validate components are from allowed list or defined locally
111
+ for (const comp of usedComponents) {
112
+ if (
113
+ !ALLOWED_SHADCN_COMPONENTS.includes(comp) &&
114
+ !localComponents.has(comp) &&
115
+ comp !== "App"
116
+ ) {
117
+ warnings.push(`Unknown component: <${comp}> - ensure it's defined locally or is a valid shadcn/ui component`);
118
+ }
119
+ }
120
+
121
+ // Check for accessibility - icon buttons without aria-label
122
+ const iconButtonMatches = content.match(/<Button[^>]*size\s*=\s*["']icon["'][^>]*>/g) || [];
123
+ for (const buttonMatch of iconButtonMatches) {
124
+ if (!buttonMatch.includes("aria-label")) {
125
+ warnings.push("Icon button found without aria-label - add aria-label for accessibility");
126
+ break;
127
+ }
128
+ }
129
+
130
+ // Check for form inputs without labels
131
+ const inputMatches = content.match(/<Input[^>]*>/g) || [];
132
+ const labelMatches = content.match(/<Label[^>]*>/g) || [];
133
+ if (inputMatches.length > labelMatches.length) {
134
+ warnings.push("Some Input components may be missing associated Label components");
135
+ }
136
+
137
+ // Check localStorage key format
138
+ const storageKeyRegex = /const\s+STORAGE_KEY\s*=\s*["']([^"']+)["']/;
139
+ const keyMatch = content.match(storageKeyRegex);
140
+ if (keyMatch && !keyMatch[1].startsWith("work-construct-")) {
141
+ warnings.push(`localStorage key should start with "work-construct-" (found "${keyMatch[1]}")`);
142
+ }
143
+
144
+ return {
145
+ valid: errors.length === 0,
146
+ errors,
147
+ warnings,
148
+ };
149
+ }
150
+
151
+ function printResult(result, filePath) {
152
+ console.log("\n╔══════════════════════════════════════════════════════════════╗");
153
+ console.log("║ WORK CONSTRUCT VALIDATOR ║");
154
+ console.log("╚══════════════════════════════════════════════════════════════╝\n");
155
+ console.log(`File: ${filePath}\n`);
156
+
157
+ if (result.errors.length > 0) {
158
+ console.log("ERRORS:");
159
+ result.errors.forEach((e) => console.log(` ✗ ${e}`));
160
+ console.log();
161
+ }
162
+
163
+ if (result.warnings.length > 0) {
164
+ console.log("WARNINGS:");
165
+ result.warnings.forEach((w) => console.log(` ⚠ ${w}`));
166
+ console.log();
167
+ }
168
+
169
+ if (result.valid && result.warnings.length === 0) {
170
+ console.log("✓ Construct passes all validations!\n");
171
+ } else if (result.valid) {
172
+ console.log("✓ Construct passes required validations (with warnings)\n");
173
+ } else {
174
+ console.log("✗ Construct has validation errors that must be fixed.\n");
175
+ }
176
+
177
+ console.log("─".repeat(60));
178
+ console.log(`Status: ${result.valid ? "PASS" : "FAIL"}`);
179
+ console.log(`Errors: ${result.errors.length} | Warnings: ${result.warnings.length}`);
180
+ console.log("─".repeat(60) + "\n");
181
+ }
182
+
183
+ // CLI execution
184
+ if (require.main === module) {
185
+ const args = process.argv.slice(2);
186
+
187
+ // Handle validate subcommand from install.js routing
188
+ if (args[0] === "validate") {
189
+ args.shift();
190
+ }
191
+
192
+ if (args.length === 0) {
193
+ console.log("Usage: npx @useworkapp/construct-skill validate <path-to-file>");
194
+ console.log("\nExample:");
195
+ console.log(" npx @useworkapp/construct-skill validate ./App.tsx");
196
+ process.exit(1);
197
+ }
198
+
199
+ const filePath = path.resolve(args[0]);
200
+ const result = validateConstruct(filePath);
201
+
202
+ printResult(result, filePath);
203
+
204
+ process.exit(result.valid ? 0 : 1);
205
+ }
206
+
207
+ module.exports = { validateConstruct };
@@ -0,0 +1,209 @@
1
+ ---
2
+ name: construct-builder
3
+ description: Build and validate Constructs for the Work streaming platform. Use when creating React micro-apps, reviewing component code for Work standards, or helping Runners build Constructs. Enforces shadcn/ui patterns, Tailwind CSS, localStorage persistence, and accessibility requirements.
4
+ allowed-tools:
5
+ - Read
6
+ - Grep
7
+ - Glob
8
+ - Write
9
+ - Edit
10
+ - Bash(npm:*)
11
+ ---
12
+
13
+ # Work Construct Builder
14
+
15
+ Build production-ready Constructs for the Work app streaming platform.
16
+
17
+ ## What is a Construct?
18
+
19
+ A Construct is a self-contained React micro-app that:
20
+ - Solves a specific business need (tracker, calculator, dashboard, form, etc.)
21
+ - Runs in Work's sandboxed iframe environment
22
+ - Uses localStorage for data persistence
23
+ - Follows strict UI/UX standards
24
+
25
+ ## Construct Requirements
26
+
27
+ ### 1. Single-File Architecture
28
+
29
+ Constructs must be a single `App.tsx` file with:
30
+ - Default export of main component
31
+ - All sub-components defined in the same file
32
+ - No external API calls (localStorage only)
33
+
34
+ ```tsx
35
+ // Correct structure
36
+ import { useState } from "react";
37
+ import { Button } from "@/components/ui/button";
38
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
39
+
40
+ export default function App() {
41
+ // Component logic
42
+ return <div>...</div>;
43
+ }
44
+ ```
45
+
46
+ ### 2. Required UI Components
47
+
48
+ Use ONLY these shadcn/ui components (available in Work runtime):
49
+
50
+ **Layout:**
51
+ - `Card`, `CardContent`, `CardHeader`, `CardTitle`, `CardDescription`, `CardFooter`
52
+
53
+ **Forms:**
54
+ - `Button`, `Input`, `Textarea`, `Label`
55
+ - `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue`
56
+ - `Checkbox`, `RadioGroup`, `RadioGroupItem`
57
+ - `Switch`
58
+
59
+ **Feedback:**
60
+ - `Badge`, `Alert`, `AlertDescription`, `AlertTitle`
61
+ - `Progress`
62
+
63
+ **Data Display:**
64
+ - `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow`
65
+ - `Tabs`, `TabsContent`, `TabsList`, `TabsTrigger`
66
+
67
+ **Overlay:**
68
+ - `Dialog`, `DialogContent`, `DialogDescription`, `DialogFooter`, `DialogHeader`, `DialogTitle`, `DialogTrigger`
69
+ - `DropdownMenu`, `DropdownMenuContent`, `DropdownMenuItem`, `DropdownMenuTrigger`
70
+
71
+ ### 3. Data Persistence Pattern
72
+
73
+ Always use localStorage with a unique key prefix:
74
+
75
+ ```tsx
76
+ const STORAGE_KEY = "work-construct-{construct-name}";
77
+
78
+ // Load data
79
+ const [data, setData] = useState(() => {
80
+ if (typeof window === "undefined") return initialState;
81
+ const saved = localStorage.getItem(STORAGE_KEY);
82
+ return saved ? JSON.parse(saved) : initialState;
83
+ });
84
+
85
+ // Save data (in useEffect)
86
+ useEffect(() => {
87
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
88
+ }, [data]);
89
+ ```
90
+
91
+ ### 4. Styling Requirements
92
+
93
+ **DO:**
94
+ - Use Tailwind CSS utility classes
95
+ - Use responsive classes (`sm:`, `md:`, `lg:`)
96
+ - Use CSS variables for theming (`text-foreground`, `bg-background`, `border-border`)
97
+ - Add `dark:` variants for dark mode support
98
+
99
+ **DON'T:**
100
+ - Use inline styles
101
+ - Use CSS modules or external stylesheets
102
+ - Use fixed pixel widths (use responsive units)
103
+ - Override shadcn component styles
104
+
105
+ ```tsx
106
+ // Correct
107
+ <div className="flex flex-col gap-4 p-4 sm:p-6">
108
+ <Card className="w-full">
109
+ <CardHeader>
110
+ <CardTitle className="text-lg sm:text-xl">Title</CardTitle>
111
+ </CardHeader>
112
+ </Card>
113
+ </div>
114
+
115
+ // Incorrect
116
+ <div style={{ padding: "20px" }}>
117
+ <Card style={{ width: "500px" }}>
118
+ ```
119
+
120
+ ### 5. Accessibility Requirements
121
+
122
+ **Mandatory:**
123
+ - All interactive elements must be keyboard accessible
124
+ - Use semantic HTML (`button`, `nav`, `main`, `section`, `article`)
125
+ - Add `aria-label` to icon-only buttons
126
+ - Ensure color contrast meets WCAG AA (4.5:1 for text)
127
+ - Form inputs must have associated labels
128
+
129
+ ```tsx
130
+ // Correct
131
+ <Button variant="ghost" size="icon" aria-label="Delete item">
132
+ <Trash2 className="h-4 w-4" />
133
+ </Button>
134
+
135
+ <div className="space-y-2">
136
+ <Label htmlFor="email">Email</Label>
137
+ <Input id="email" type="email" />
138
+ </div>
139
+
140
+ // Incorrect
141
+ <Button variant="ghost" size="icon">
142
+ <Trash2 className="h-4 w-4" />
143
+ </Button>
144
+ ```
145
+
146
+ ### 6. Icon Usage
147
+
148
+ Use Lucide React icons only:
149
+
150
+ ```tsx
151
+ import { Plus, Trash2, Edit, Check, X, Search, Filter } from "lucide-react";
152
+
153
+ <Plus className="h-4 w-4" />
154
+ ```
155
+
156
+ ### 7. State Management
157
+
158
+ For complex state, use `useReducer`:
159
+
160
+ ```tsx
161
+ type State = { items: Item[]; filter: string };
162
+ type Action =
163
+ | { type: "ADD_ITEM"; payload: Item }
164
+ | { type: "DELETE_ITEM"; payload: string }
165
+ | { type: "SET_FILTER"; payload: string };
166
+
167
+ function reducer(state: State, action: Action): State {
168
+ switch (action.type) {
169
+ case "ADD_ITEM":
170
+ return { ...state, items: [...state.items, action.payload] };
171
+ // ...
172
+ }
173
+ }
174
+ ```
175
+
176
+ ## Validation Checklist
177
+
178
+ Before submitting a Construct, verify:
179
+
180
+ - [ ] Single `App.tsx` file with default export
181
+ - [ ] Uses only approved shadcn/ui components
182
+ - [ ] localStorage persistence with unique key
183
+ - [ ] Responsive design (works on mobile)
184
+ - [ ] Dark mode support
185
+ - [ ] All buttons/inputs have proper labels
186
+ - [ ] No external API calls
187
+ - [ ] No TypeScript errors
188
+ - [ ] Meaningful title and description
189
+
190
+ ## Example Construct
191
+
192
+ See [example.tsx](example.tsx) for a complete working example.
193
+
194
+ ## Validation
195
+
196
+ Run the validator to check your Construct:
197
+
198
+ ```bash
199
+ npx @useworkapp/construct-skill validate path/to/App.tsx
200
+ ```
201
+
202
+ ## Commands
203
+
204
+ When asked to build or review a Construct:
205
+
206
+ 1. **Build**: Generate complete `App.tsx` following all requirements
207
+ 2. **Review**: Check existing code against the validation checklist
208
+ 3. **Fix**: Identify and correct any violations
209
+ 4. **Optimize**: Suggest performance and UX improvements
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Example Construct: Todo List
5
+ * A complete, production-ready example following all Work platform standards.
6
+ */
7
+
8
+ import { useState, useEffect } from "react";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
12
+ import { Checkbox } from "@/components/ui/checkbox";
13
+ import { Label } from "@/components/ui/label";
14
+ import { Badge } from "@/components/ui/badge";
15
+ import { Plus, Trash2 } from "lucide-react";
16
+
17
+ // Types
18
+ interface Todo {
19
+ id: string;
20
+ text: string;
21
+ completed: boolean;
22
+ createdAt: number;
23
+ }
24
+
25
+ // Storage key with work-construct- prefix
26
+ const STORAGE_KEY = "work-construct-todo-list";
27
+
28
+ export default function App() {
29
+ // Initialize state from localStorage
30
+ const [todos, setTodos] = useState<Todo[]>(() => {
31
+ if (typeof window === "undefined") return [];
32
+ const saved = localStorage.getItem(STORAGE_KEY);
33
+ return saved ? JSON.parse(saved) : [];
34
+ });
35
+ const [newTodo, setNewTodo] = useState("");
36
+
37
+ // Persist to localStorage on change
38
+ useEffect(() => {
39
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
40
+ }, [todos]);
41
+
42
+ // Handlers
43
+ const addTodo = () => {
44
+ if (!newTodo.trim()) return;
45
+ setTodos([
46
+ ...todos,
47
+ {
48
+ id: crypto.randomUUID(),
49
+ text: newTodo.trim(),
50
+ completed: false,
51
+ createdAt: Date.now(),
52
+ },
53
+ ]);
54
+ setNewTodo("");
55
+ };
56
+
57
+ const toggleTodo = (id: string) => {
58
+ setTodos(
59
+ todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
60
+ );
61
+ };
62
+
63
+ const deleteTodo = (id: string) => {
64
+ setTodos(todos.filter((t) => t.id !== id));
65
+ };
66
+
67
+ // Computed values
68
+ const completedCount = todos.filter((t) => t.completed).length;
69
+ const totalCount = todos.length;
70
+
71
+ return (
72
+ <div className="min-h-screen bg-background p-4 sm:p-6">
73
+ <Card className="mx-auto max-w-md">
74
+ <CardHeader>
75
+ <div className="flex items-center justify-between">
76
+ <CardTitle>Todo List</CardTitle>
77
+ {totalCount > 0 && (
78
+ <Badge variant="secondary">
79
+ {completedCount}/{totalCount}
80
+ </Badge>
81
+ )}
82
+ </div>
83
+ </CardHeader>
84
+ <CardContent className="space-y-4">
85
+ {/* Add Todo Form */}
86
+ <div className="flex gap-2">
87
+ <Input
88
+ placeholder="Add a task..."
89
+ value={newTodo}
90
+ onChange={(e) => setNewTodo(e.target.value)}
91
+ onKeyDown={(e) => e.key === "Enter" && addTodo()}
92
+ aria-label="New task input"
93
+ />
94
+ <Button onClick={addTodo} aria-label="Add task">
95
+ <Plus className="h-4 w-4" />
96
+ </Button>
97
+ </div>
98
+
99
+ {/* Todo List */}
100
+ <div className="space-y-2">
101
+ {todos.map((todo) => (
102
+ <div
103
+ key={todo.id}
104
+ className="flex items-center gap-3 rounded-lg border bg-card p-3 transition-colors hover:bg-accent/50"
105
+ >
106
+ <Checkbox
107
+ id={todo.id}
108
+ checked={todo.completed}
109
+ onCheckedChange={() => toggleTodo(todo.id)}
110
+ aria-label={`Mark "${todo.text}" as ${todo.completed ? "incomplete" : "complete"}`}
111
+ />
112
+ <Label
113
+ htmlFor={todo.id}
114
+ className={`flex-1 cursor-pointer ${
115
+ todo.completed
116
+ ? "text-muted-foreground line-through"
117
+ : "text-foreground"
118
+ }`}
119
+ >
120
+ {todo.text}
121
+ </Label>
122
+ <Button
123
+ variant="ghost"
124
+ size="icon"
125
+ onClick={() => deleteTodo(todo.id)}
126
+ aria-label={`Delete "${todo.text}"`}
127
+ className="h-8 w-8 text-muted-foreground hover:text-destructive"
128
+ >
129
+ <Trash2 className="h-4 w-4" />
130
+ </Button>
131
+ </div>
132
+ ))}
133
+ </div>
134
+
135
+ {/* Empty State */}
136
+ {todos.length === 0 && (
137
+ <p className="py-8 text-center text-sm text-muted-foreground">
138
+ No tasks yet. Add one above!
139
+ </p>
140
+ )}
141
+
142
+ {/* Clear Completed */}
143
+ {completedCount > 0 && (
144
+ <Button
145
+ variant="outline"
146
+ size="sm"
147
+ className="w-full"
148
+ onClick={() => setTodos(todos.filter((t) => !t.completed))}
149
+ >
150
+ Clear {completedCount} completed
151
+ </Button>
152
+ )}
153
+ </CardContent>
154
+ </Card>
155
+ </div>
156
+ );
157
+ }
@@ -0,0 +1,319 @@
1
+ ---
2
+ description: Build and validate Constructs for the Work streaming platform. Creates React micro-apps following Work standards with shadcn/ui, Tailwind CSS, and localStorage persistence.
3
+ agent: build
4
+ ---
5
+
6
+ # Work Construct Builder
7
+
8
+ Build production-ready Constructs for the Work app streaming platform.
9
+
10
+ $ARGUMENTS
11
+
12
+ ## What is a Construct?
13
+
14
+ A Construct is a self-contained React micro-app that:
15
+ - Solves a specific business need (tracker, calculator, dashboard, form, etc.)
16
+ - Runs in Work's sandboxed iframe environment
17
+ - Uses localStorage for data persistence
18
+ - Follows strict UI/UX standards
19
+
20
+ ## Construct Requirements
21
+
22
+ ### 1. Single-File Architecture
23
+
24
+ Constructs must be a single `App.tsx` file with:
25
+ - Default export of main component
26
+ - All sub-components defined in the same file
27
+ - No external API calls (localStorage only)
28
+
29
+ ```tsx
30
+ // Correct structure
31
+ import { useState } from "react";
32
+ import { Button } from "@/components/ui/button";
33
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
34
+
35
+ export default function App() {
36
+ // Component logic
37
+ return <div>...</div>;
38
+ }
39
+ ```
40
+
41
+ ### 2. Required UI Components
42
+
43
+ Use ONLY these shadcn/ui components (available in Work runtime):
44
+
45
+ **Layout:**
46
+ - `Card`, `CardContent`, `CardHeader`, `CardTitle`, `CardDescription`, `CardFooter`
47
+
48
+ **Forms:**
49
+ - `Button`, `Input`, `Textarea`, `Label`
50
+ - `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue`
51
+ - `Checkbox`, `RadioGroup`, `RadioGroupItem`
52
+ - `Switch`
53
+
54
+ **Feedback:**
55
+ - `Badge`, `Alert`, `AlertDescription`, `AlertTitle`
56
+ - `Progress`
57
+
58
+ **Data Display:**
59
+ - `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow`
60
+ - `Tabs`, `TabsContent`, `TabsList`, `TabsTrigger`
61
+
62
+ **Overlay:**
63
+ - `Dialog`, `DialogContent`, `DialogDescription`, `DialogFooter`, `DialogHeader`, `DialogTitle`, `DialogTrigger`
64
+ - `DropdownMenu`, `DropdownMenuContent`, `DropdownMenuItem`, `DropdownMenuTrigger`
65
+
66
+ ### 3. Data Persistence Pattern
67
+
68
+ Always use localStorage with a unique key prefix:
69
+
70
+ ```tsx
71
+ const STORAGE_KEY = "work-construct-{construct-name}";
72
+
73
+ // Load data
74
+ const [data, setData] = useState(() => {
75
+ if (typeof window === "undefined") return initialState;
76
+ const saved = localStorage.getItem(STORAGE_KEY);
77
+ return saved ? JSON.parse(saved) : initialState;
78
+ });
79
+
80
+ // Save data (in useEffect)
81
+ useEffect(() => {
82
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
83
+ }, [data]);
84
+ ```
85
+
86
+ ### 4. Styling Requirements
87
+
88
+ **DO:**
89
+ - Use Tailwind CSS utility classes
90
+ - Use responsive classes (`sm:`, `md:`, `lg:`)
91
+ - Use CSS variables for theming (`text-foreground`, `bg-background`, `border-border`)
92
+ - Add `dark:` variants for dark mode support
93
+
94
+ **DON'T:**
95
+ - Use inline styles
96
+ - Use CSS modules or external stylesheets
97
+ - Use fixed pixel widths (use responsive units)
98
+ - Override shadcn component styles
99
+
100
+ ```tsx
101
+ // Correct
102
+ <div className="flex flex-col gap-4 p-4 sm:p-6">
103
+ <Card className="w-full">
104
+ <CardHeader>
105
+ <CardTitle className="text-lg sm:text-xl">Title</CardTitle>
106
+ </CardHeader>
107
+ </Card>
108
+ </div>
109
+
110
+ // Incorrect
111
+ <div style={{ padding: "20px" }}>
112
+ <Card style={{ width: "500px" }}>
113
+ ```
114
+
115
+ ### 5. Accessibility Requirements
116
+
117
+ **Mandatory:**
118
+ - All interactive elements must be keyboard accessible
119
+ - Use semantic HTML (`button`, `nav`, `main`, `section`, `article`)
120
+ - Add `aria-label` to icon-only buttons
121
+ - Ensure color contrast meets WCAG AA (4.5:1 for text)
122
+ - Form inputs must have associated labels
123
+
124
+ ```tsx
125
+ // Correct
126
+ <Button variant="ghost" size="icon" aria-label="Delete item">
127
+ <Trash2 className="h-4 w-4" />
128
+ </Button>
129
+
130
+ <div className="space-y-2">
131
+ <Label htmlFor="email">Email</Label>
132
+ <Input id="email" type="email" />
133
+ </div>
134
+ ```
135
+
136
+ ### 6. Icon Usage
137
+
138
+ Use Lucide React icons only:
139
+
140
+ ```tsx
141
+ import { Plus, Trash2, Edit, Check, X, Search, Filter } from "lucide-react";
142
+
143
+ <Plus className="h-4 w-4" />
144
+ ```
145
+
146
+ ### 7. State Management
147
+
148
+ For complex state, use `useReducer`:
149
+
150
+ ```tsx
151
+ type State = { items: Item[]; filter: string };
152
+ type Action =
153
+ | { type: "ADD_ITEM"; payload: Item }
154
+ | { type: "DELETE_ITEM"; payload: string }
155
+ | { type: "SET_FILTER"; payload: string };
156
+
157
+ function reducer(state: State, action: Action): State {
158
+ switch (action.type) {
159
+ case "ADD_ITEM":
160
+ return { ...state, items: [...state.items, action.payload] };
161
+ // ...
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## Validation Checklist
167
+
168
+ Before submitting a Construct, verify:
169
+
170
+ - [ ] Single `App.tsx` file with default export
171
+ - [ ] Uses only approved shadcn/ui components
172
+ - [ ] localStorage persistence with unique key
173
+ - [ ] Responsive design (works on mobile)
174
+ - [ ] Dark mode support
175
+ - [ ] All buttons/inputs have proper labels
176
+ - [ ] No external API calls
177
+ - [ ] No TypeScript errors
178
+ - [ ] Meaningful title and description
179
+
180
+ ## Example Construct
181
+
182
+ ```tsx
183
+ "use client";
184
+
185
+ import { useState, useEffect } from "react";
186
+ import { Button } from "@/components/ui/button";
187
+ import { Input } from "@/components/ui/input";
188
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
189
+ import { Checkbox } from "@/components/ui/checkbox";
190
+ import { Label } from "@/components/ui/label";
191
+ import { Badge } from "@/components/ui/badge";
192
+ import { Plus, Trash2 } from "lucide-react";
193
+
194
+ interface Todo {
195
+ id: string;
196
+ text: string;
197
+ completed: boolean;
198
+ }
199
+
200
+ const STORAGE_KEY = "work-construct-todo-list";
201
+
202
+ export default function App() {
203
+ const [todos, setTodos] = useState<Todo[]>(() => {
204
+ if (typeof window === "undefined") return [];
205
+ const saved = localStorage.getItem(STORAGE_KEY);
206
+ return saved ? JSON.parse(saved) : [];
207
+ });
208
+ const [newTodo, setNewTodo] = useState("");
209
+
210
+ useEffect(() => {
211
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
212
+ }, [todos]);
213
+
214
+ const addTodo = () => {
215
+ if (!newTodo.trim()) return;
216
+ setTodos([
217
+ ...todos,
218
+ { id: crypto.randomUUID(), text: newTodo.trim(), completed: false },
219
+ ]);
220
+ setNewTodo("");
221
+ };
222
+
223
+ const toggleTodo = (id: string) => {
224
+ setTodos(todos.map((t) =>
225
+ t.id === id ? { ...t, completed: !t.completed } : t
226
+ ));
227
+ };
228
+
229
+ const deleteTodo = (id: string) => {
230
+ setTodos(todos.filter((t) => t.id !== id));
231
+ };
232
+
233
+ const completedCount = todos.filter((t) => t.completed).length;
234
+
235
+ return (
236
+ <div className="min-h-screen bg-background p-4 sm:p-6">
237
+ <Card className="mx-auto max-w-md">
238
+ <CardHeader>
239
+ <div className="flex items-center justify-between">
240
+ <CardTitle>Todo List</CardTitle>
241
+ {todos.length > 0 && (
242
+ <Badge variant="secondary">
243
+ {completedCount}/{todos.length}
244
+ </Badge>
245
+ )}
246
+ </div>
247
+ </CardHeader>
248
+ <CardContent className="space-y-4">
249
+ <div className="flex gap-2">
250
+ <Input
251
+ placeholder="Add a task..."
252
+ value={newTodo}
253
+ onChange={(e) => setNewTodo(e.target.value)}
254
+ onKeyDown={(e) => e.key === "Enter" && addTodo()}
255
+ />
256
+ <Button onClick={addTodo} aria-label="Add task">
257
+ <Plus className="h-4 w-4" />
258
+ </Button>
259
+ </div>
260
+
261
+ <div className="space-y-2">
262
+ {todos.map((todo) => (
263
+ <div
264
+ key={todo.id}
265
+ className="flex items-center gap-3 rounded-lg border p-3"
266
+ >
267
+ <Checkbox
268
+ id={todo.id}
269
+ checked={todo.completed}
270
+ onCheckedChange={() => toggleTodo(todo.id)}
271
+ />
272
+ <Label
273
+ htmlFor={todo.id}
274
+ className={`flex-1 ${
275
+ todo.completed ? "line-through text-muted-foreground" : ""
276
+ }`}
277
+ >
278
+ {todo.text}
279
+ </Label>
280
+ <Button
281
+ variant="ghost"
282
+ size="icon"
283
+ onClick={() => deleteTodo(todo.id)}
284
+ aria-label="Delete task"
285
+ >
286
+ <Trash2 className="h-4 w-4" />
287
+ </Button>
288
+ </div>
289
+ ))}
290
+ </div>
291
+
292
+ {todos.length === 0 && (
293
+ <p className="text-center text-sm text-muted-foreground">
294
+ No tasks yet. Add one above!
295
+ </p>
296
+ )}
297
+ </CardContent>
298
+ </Card>
299
+ </div>
300
+ );
301
+ }
302
+ ```
303
+
304
+ ## Validation
305
+
306
+ Run the validator to check your Construct:
307
+
308
+ ```bash
309
+ npx @useworkapp/construct-skill validate path/to/App.tsx
310
+ ```
311
+
312
+ ## Commands
313
+
314
+ When asked to build or review a Construct:
315
+
316
+ 1. **Build**: Generate complete `App.tsx` following all requirements
317
+ 2. **Review**: Check existing code against the validation checklist
318
+ 3. **Fix**: Identify and correct any violations
319
+ 4. **Optimize**: Suggest performance and UX improvements
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@useworkapp/construct-skill",
3
+ "version": "1.0.0",
4
+ "description": "AI coding assistant skill for building Work platform Constructs. Compatible with Claude Code and OpenCode.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "opencode",
8
+ "ai-coding",
9
+ "skill",
10
+ "react",
11
+ "constructs",
12
+ "work-platform",
13
+ "shadcn-ui",
14
+ "tailwind"
15
+ ],
16
+ "author": "Work <hello@usework.app>",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/usework/construct-skill"
21
+ },
22
+ "homepage": "https://usework.app/docs/runners/constructs",
23
+ "bin": {
24
+ "construct-skill": "./bin/install.js"
25
+ },
26
+ "files": [
27
+ "bin",
28
+ "claude-code",
29
+ "opencode",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "postinstall": "node bin/install.js --auto",
34
+ "validate": "node bin/validate.js"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }