@elyracode/flux-ui 0.3.7

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,57 @@
1
+ # @elyracode/flux-ui
2
+
3
+ Elyra extension for **Flux UI** -- live component index, Blade-to-Flux converter, and page generator for Livewire 4 projects.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/flux-ui
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `flux_component_index` | Fetch the live Flux UI component library from GitHub. Always up-to-date, cached for 24h. |
16
+ | `blade_to_flux` | Analyze a Blade file and suggest Flux UI component replacements. |
17
+ | `flux_generate_page` | Generate a complete page scaffold: Blade view, Livewire component, migration, route. |
18
+
19
+ ## Commands
20
+
21
+ - `/flux` -- Interactive selector for all Flux UI tools
22
+
23
+ ## Usage
24
+
25
+ ### Live Component Reference
26
+ ```
27
+ > Show me all available Flux UI components
28
+ > What props does flux:table support?
29
+ > Show me the flux:modal component API
30
+ ```
31
+
32
+ The agent fetches the actual Flux UI source code from GitHub, so it always has the latest component APIs -- even components added yesterday.
33
+
34
+ ### Convert Existing Views
35
+ ```
36
+ > Convert resources/views/users/index.blade.php to use Flux UI
37
+ > Analyze my dashboard view for Flux conversion opportunities
38
+ ```
39
+
40
+ The agent scans your Blade file, identifies HTML/Tailwind patterns (tables, forms, modals, buttons), and suggests specific Flux replacements.
41
+
42
+ ### Generate New Pages
43
+ ```
44
+ > Generate a products CRUD page with Flux UI
45
+ > Create a user settings page with profile form and password change
46
+ > Build a dashboard with stats cards and a recent orders table
47
+ ```
48
+
49
+ The agent generates all four files: Blade view with Flux components, Livewire class with validation, migration, and route.
50
+
51
+ ## How It Works
52
+
53
+ 1. **Component Index**: Fetches `livewire/flux` from GitHub, filters to component files, extracts props and slots
54
+ 2. **Blade Converter**: Pattern-matches 15+ HTML elements against Flux equivalents (button, input, select, table, modal, tabs, card, dropdown, breadcrumb, tooltip, switch, sidebar, header)
55
+ 3. **Page Generator**: Fetches the component index for accurate API usage, then provides structured generation instructions with Livewire 4 best practices (#[Validate], #[Computed], #[Url])
56
+
57
+ All GitHub fetches are cached at `~/.elyra/cache/github/` for 24 hours.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * GitHub repo fetching with file-based caching.
3
+ * Self-contained -- no dependency on @elyracode/laravel-starters.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+
10
+ export interface FetchedFile {
11
+ path: string;
12
+ content: string;
13
+ }
14
+
15
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
16
+
17
+ interface CacheEntry {
18
+ timestamp: number;
19
+ files: FetchedFile[];
20
+ }
21
+
22
+ function getCacheDir(): string {
23
+ return join(homedir(), ".elyra", "cache", "github");
24
+ }
25
+
26
+ function getCacheKey(repo: string, branch: string): string {
27
+ return `${repo.replace(/\//g, "--")}@${branch}.json`;
28
+ }
29
+
30
+ function loadFromCache(repo: string, branch: string): FetchedFile[] | undefined {
31
+ try {
32
+ const cachePath = join(getCacheDir(), getCacheKey(repo, branch));
33
+ if (!existsSync(cachePath)) return undefined;
34
+ const entry = JSON.parse(readFileSync(cachePath, "utf-8")) as CacheEntry;
35
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return undefined;
36
+ return entry.files;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ function saveToCache(repo: string, branch: string, files: FetchedFile[]): void {
43
+ try {
44
+ const dir = getCacheDir();
45
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
46
+ const entry: CacheEntry = { timestamp: Date.now(), files };
47
+ writeFileSync(join(dir, getCacheKey(repo, branch)), JSON.stringify(entry));
48
+ } catch {
49
+ // Never fail on cache write errors
50
+ }
51
+ }
52
+
53
+ export async function fetchRepoContents(
54
+ repo: string,
55
+ branch: string,
56
+ options?: { maxFiles?: number; extensions?: string[] },
57
+ ): Promise<FetchedFile[]> {
58
+ const maxFiles = options?.maxFiles ?? 300;
59
+ const extensions = options?.extensions ?? [".php", ".blade.php", ".js", ".json"];
60
+
61
+ const cached = loadFromCache(repo, branch);
62
+ if (cached) {
63
+ let files = cached;
64
+ files = files.filter((f) => extensions.some((ext) => f.path.endsWith(ext)));
65
+ files = files.filter((f) => f.content.length <= 100000);
66
+ return files.slice(0, maxFiles);
67
+ }
68
+
69
+ const treeUrl = `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`;
70
+ const treeResponse = await fetch(treeUrl, {
71
+ headers: {
72
+ Accept: "application/vnd.github.v3+json",
73
+ "User-Agent": "elyra-flux-ui",
74
+ },
75
+ });
76
+
77
+ if (!treeResponse.ok) {
78
+ throw new Error(`GitHub API error: ${treeResponse.status} ${treeResponse.statusText}`);
79
+ }
80
+
81
+ const tree = (await treeResponse.json()) as {
82
+ tree: Array<{ path: string; type: string; size?: number }>;
83
+ };
84
+
85
+ const relevantFiles = tree.tree
86
+ .filter((entry) => {
87
+ if (entry.type !== "blob") return false;
88
+ if (entry.size && entry.size > 100000) return false;
89
+ return extensions.some((ext) => entry.path.endsWith(ext));
90
+ })
91
+ .slice(0, maxFiles);
92
+
93
+ const batchSize = 10;
94
+ const files: FetchedFile[] = [];
95
+
96
+ for (let i = 0; i < relevantFiles.length; i += batchSize) {
97
+ const batch = relevantFiles.slice(i, i + batchSize);
98
+ const results = await Promise.all(
99
+ batch.map(async (entry) => {
100
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/${branch}/${entry.path}`;
101
+ const response = await fetch(rawUrl, {
102
+ headers: { "User-Agent": "elyra-flux-ui" },
103
+ });
104
+ if (!response.ok) return null;
105
+ const content = await response.text();
106
+ return { path: entry.path, content };
107
+ }),
108
+ );
109
+ for (const result of results) {
110
+ if (result) files.push(result);
111
+ }
112
+ }
113
+
114
+ saveToCache(repo, branch, files);
115
+ return files;
116
+ }
@@ -0,0 +1,318 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
3
+ import { Type } from "typebox";
4
+ import { fetchRepoContents } from "./github-fetch.js";
5
+
6
+ const FLUX_REPO = "livewire/flux";
7
+ const FLUX_BRANCH = "main";
8
+
9
+ export default function (elyra: ExtensionAPI): void {
10
+
11
+ // ── Tool 1: flux_component_index ──
12
+ // Fetches the actual Flux UI source from GitHub and builds a live component reference
13
+ elyra.registerTool({
14
+ name: "flux_component_index",
15
+ label: "Flux UI Component Index",
16
+ description:
17
+ "Fetch the live Flux UI component library from GitHub and return a structured index of all available components, " +
18
+ "their props, slots, and variants. Always use this before generating Flux UI code to ensure you have the latest API. " +
19
+ "Results are cached for 24 hours.",
20
+ parameters: Type.Object({
21
+ component: Type.Optional(
22
+ Type.String({
23
+ description:
24
+ "Filter to a specific component name (e.g., 'button', 'modal', 'table'). Omit to get the full index.",
25
+ }),
26
+ ),
27
+ }),
28
+ execute: async (_toolCallId, params) => {
29
+ try {
30
+ const files = await fetchRepoContents(FLUX_REPO, FLUX_BRANCH, {
31
+ extensions: [".blade.php", ".php"],
32
+ maxFiles: 300,
33
+ });
34
+
35
+ // Find component files (typically in stubs/ or src/components/)
36
+ let componentFiles = files.filter(
37
+ (f) =>
38
+ f.path.includes("stubs/") ||
39
+ f.path.includes("components/") ||
40
+ f.path.includes("views/"),
41
+ );
42
+
43
+ if (params.component) {
44
+ const search = params.component.toLowerCase();
45
+ componentFiles = componentFiles.filter(
46
+ (f) => f.path.toLowerCase().includes(search),
47
+ );
48
+ }
49
+
50
+ if (componentFiles.length === 0) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text" as const,
55
+ text: params.component
56
+ ? `No Flux UI component matching "${params.component}" found in ${FLUX_REPO}.`
57
+ : `No component files found in ${FLUX_REPO}. The repo structure may have changed.`,
58
+ },
59
+ ],
60
+ details: {},
61
+ };
62
+ }
63
+
64
+ const lines: string[] = [
65
+ `# Flux UI Component Index (live from github.com/${FLUX_REPO})`,
66
+ `Components found: ${componentFiles.length}`,
67
+ "",
68
+ "Use these as the authoritative reference for Flux UI component APIs.",
69
+ "",
70
+ ];
71
+
72
+ for (const file of componentFiles) {
73
+ lines.push(`## ${file.path}`);
74
+ lines.push("```blade");
75
+ lines.push(file.content);
76
+ lines.push("```");
77
+ lines.push("");
78
+ }
79
+
80
+ const context = lines.join("\n");
81
+ const truncated =
82
+ context.length > 100000
83
+ ? `${context.slice(0, 100000)}\n\n... (truncated, ${componentFiles.length} files total)`
84
+ : context;
85
+
86
+ return {
87
+ content: [{ type: "text" as const, text: truncated }],
88
+ details: { componentCount: componentFiles.length },
89
+ };
90
+ } catch (error) {
91
+ const msg = error instanceof Error ? error.message : String(error);
92
+ return {
93
+ content: [{ type: "text" as const, text: `Failed to fetch Flux UI components: ${msg}` }],
94
+ details: {},
95
+ };
96
+ }
97
+ },
98
+ });
99
+
100
+ // ── Tool 2: blade_to_flux ──
101
+ // Reads a Blade file and suggests Flux component replacements
102
+ elyra.registerTool({
103
+ name: "blade_to_flux",
104
+ label: "Convert Blade to Flux UI",
105
+ description:
106
+ "Read a Blade view file and analyze it for HTML/Tailwind patterns that can be replaced with Flux UI components. " +
107
+ "Returns the original code alongside specific conversion suggestions. " +
108
+ "Use this when the user wants to modernize existing Blade views with Flux UI.",
109
+ parameters: Type.Object({
110
+ file_path: Type.String({
111
+ description: "Path to the Blade file to analyze (relative to cwd)",
112
+ }),
113
+ }),
114
+ execute: async (_toolCallId, params) => {
115
+ try {
116
+ const content = readFileSync(params.file_path, "utf-8");
117
+
118
+ // Pattern matching for common HTML -> Flux conversions
119
+ const suggestions: string[] = [];
120
+
121
+ const patterns: Array<{ pattern: RegExp; flux: string; description: string }> = [
122
+ { pattern: /<button[\s>]/gi, flux: "<flux:button>", description: "Replace <button> with <flux:button>" },
123
+ { pattern: /<input[\s>]/gi, flux: "<flux:input>", description: "Replace <input> with <flux:input>" },
124
+ { pattern: /<select[\s>]/gi, flux: "<flux:select>", description: "Replace <select> with <flux:select>" },
125
+ { pattern: /<textarea[\s>]/gi, flux: "<flux:textarea>", description: "Replace <textarea> with <flux:textarea>" },
126
+ { pattern: /<table[\s>]/gi, flux: "<flux:table>", description: "Replace <table> with <flux:table> + <flux:columns> + <flux:rows>" },
127
+ { pattern: /<dialog[\s>]|x-data=.*modal|x-show=.*modal/gi, flux: "<flux:modal>", description: "Replace modal pattern with <flux:modal>" },
128
+ { pattern: /<nav[\s>].*tab|role="tablist"/gi, flux: "<flux:tabs>", description: "Replace tab navigation with <flux:tabs>" },
129
+ { pattern: /<div.*class=".*card.*"/gi, flux: "<flux:card>", description: "Replace card div with <flux:card>" },
130
+ { pattern: /class=".*badge.*"/gi, flux: "<flux:badge>", description: "Replace badge with <flux:badge>" },
131
+ { pattern: /<div.*class=".*dropdown.*"|x-data=.*dropdown/gi, flux: "<flux:dropdown>", description: "Replace dropdown with <flux:dropdown>" },
132
+ { pattern: /<div.*class=".*breadcrumb.*"/gi, flux: "<flux:breadcrumbs>", description: "Replace breadcrumb with <flux:breadcrumbs>" },
133
+ { pattern: /<div.*class=".*tooltip.*"|x-data=.*tooltip/gi, flux: "<flux:tooltip>", description: "Replace tooltip with <flux:tooltip>" },
134
+ { pattern: /<div.*class=".*switch.*"|type="checkbox".*role="switch"/gi, flux: "<flux:switch>", description: "Replace switch/toggle with <flux:switch>" },
135
+ { pattern: /<header[\s>]/gi, flux: "<flux:header>", description: "Consider <flux:header> for page headers" },
136
+ { pattern: /<aside[\s>]|class=".*sidebar.*"/gi, flux: "<flux:sidebar>", description: "Consider <flux:sidebar> for side navigation" },
137
+ ];
138
+
139
+ for (const { pattern, description } of patterns) {
140
+ const matches = content.match(pattern);
141
+ if (matches) {
142
+ suggestions.push(`- ${description} (${matches.length} occurrence${matches.length > 1 ? "s" : ""})`);
143
+ }
144
+ }
145
+
146
+ // Check for wire:model patterns that could use Flux form components
147
+ const wireModels = content.match(/wire:model[.\w]*/g);
148
+ if (wireModels) {
149
+ suggestions.push(`- ${wireModels.length} wire:model binding(s) found -- ensure Flux form components use wire:model directly`);
150
+ }
151
+
152
+ const result: string[] = [
153
+ `# Blade to Flux UI Analysis: ${params.file_path}`,
154
+ "",
155
+ ];
156
+
157
+ if (suggestions.length === 0) {
158
+ result.push("No obvious conversion opportunities found. The file may already use Flux UI or contain custom components.");
159
+ } else {
160
+ result.push(`Found ${suggestions.length} conversion opportunities:`, "");
161
+ result.push(...suggestions);
162
+ result.push("");
163
+ result.push("## Original source");
164
+ result.push("```blade");
165
+ result.push(content);
166
+ result.push("```");
167
+ result.push("");
168
+ result.push("To convert, use the flux_component_index tool to get the exact Flux API, then rewrite the file.");
169
+ }
170
+
171
+ return {
172
+ content: [{ type: "text" as const, text: result.join("\n") }],
173
+ details: { suggestions: suggestions.length },
174
+ };
175
+ } catch (error) {
176
+ const msg = error instanceof Error ? error.message : String(error);
177
+ return {
178
+ content: [{ type: "text" as const, text: `Failed to read ${params.file_path}: ${msg}` }],
179
+ details: {},
180
+ };
181
+ }
182
+ },
183
+ });
184
+
185
+ // ── Tool 3: flux_generate_page ──
186
+ // Generates a complete Flux UI page with Livewire component, migration, and routing
187
+ elyra.registerTool({
188
+ name: "flux_generate_page",
189
+ label: "Generate Flux UI Page",
190
+ description:
191
+ "Generate a complete Flux UI page scaffold including: Blade view with Flux components, " +
192
+ "Livewire component class, database migration, and route definition. " +
193
+ "First fetches the live Flux component index to ensure accurate component usage. " +
194
+ "Use this when the user wants to create a new page or feature in a TALL stack project.",
195
+ parameters: Type.Object({
196
+ page_name: Type.String({
197
+ description: "Name of the page/feature (e.g., 'products', 'user-settings', 'dashboard')",
198
+ }),
199
+ description: Type.String({
200
+ description:
201
+ "Description of what the page should do (e.g., 'CRUD table for products with name, price, category columns, edit dialog, and delete confirmation')",
202
+ }),
203
+ layout: Type.Optional(
204
+ Type.Union([Type.Literal("sidebar"), Type.Literal("header")], {
205
+ description: "Layout variant: sidebar (default) or header",
206
+ }),
207
+ ),
208
+ }),
209
+ execute: async (_toolCallId, params) => {
210
+ try {
211
+ // Fetch Flux component index for accurate API usage
212
+ const files = await fetchRepoContents(FLUX_REPO, FLUX_BRANCH, {
213
+ extensions: [".blade.php"],
214
+ maxFiles: 200,
215
+ });
216
+
217
+ const componentFiles = files.filter(
218
+ (f) =>
219
+ f.path.includes("stubs/") ||
220
+ f.path.includes("components/") ||
221
+ f.path.includes("views/"),
222
+ );
223
+
224
+ // Build compact component reference (just names and key props)
225
+ const componentIndex: string[] = [
226
+ "# Available Flux UI Components (from source)",
227
+ "",
228
+ ];
229
+
230
+ for (const file of componentFiles.slice(0, 50)) {
231
+ const name = file.path.split("/").pop()?.replace(".blade.php", "") ?? file.path;
232
+ // Extract @props from the component
233
+ const propsMatch = file.content.match(/@props\s*\(\s*\[([\s\S]*?)\]\s*\)/);
234
+ const props = propsMatch ? propsMatch[1].trim() : "";
235
+ componentIndex.push(`- flux:${name}${props ? ` (props: ${props.slice(0, 200)})` : ""}`);
236
+ }
237
+
238
+ const layout = params.layout ?? "sidebar";
239
+
240
+ const result: string[] = [
241
+ `# Page Generation Context: ${params.page_name}`,
242
+ "",
243
+ `Description: ${params.description}`,
244
+ `Layout: ${layout}`,
245
+ "",
246
+ "## Instructions",
247
+ "",
248
+ "Generate the following files for a TALL stack project:",
249
+ "",
250
+ `1. **Blade view**: \`resources/views/livewire/${params.page_name}.blade.php\``,
251
+ " - Use Flux UI components (flux:table, flux:button, flux:modal, flux:input, etc.)",
252
+ ` - Use the ${layout} layout`,
253
+ " - Include wire:model bindings for all form fields",
254
+ " - Include wire:click for actions",
255
+ "",
256
+ `2. **Livewire component**: \`app/Livewire/${toPascalCase(params.page_name)}.php\``,
257
+ " - Use #[Validate] for form validation",
258
+ " - Use #[Computed] for expensive queries",
259
+ " - Use #[Url] for filterable/sortable parameters",
260
+ " - Include proper public properties for form state",
261
+ "",
262
+ `3. **Migration**: \`database/migrations/xxxx_create_${toSnakeCase(params.page_name)}_table.php\``,
263
+ " - Based on the description, create appropriate columns",
264
+ "",
265
+ `4. **Route**: Add to \`routes/web.php\``,
266
+ ` - \`Route::get('/${params.page_name}', ${toPascalCase(params.page_name)}::class)->name('${params.page_name}')\``,
267
+ "",
268
+ ...componentIndex,
269
+ ];
270
+
271
+ return {
272
+ content: [{ type: "text" as const, text: result.join("\n") }],
273
+ details: { pageName: params.page_name, layout, componentCount: componentFiles.length },
274
+ };
275
+ } catch (error) {
276
+ const msg = error instanceof Error ? error.message : String(error);
277
+ return {
278
+ content: [{ type: "text" as const, text: `Failed to prepare page generation: ${msg}` }],
279
+ details: {},
280
+ };
281
+ }
282
+ },
283
+ });
284
+
285
+ // ── Command: /flux ──
286
+ elyra.registerCommand("flux", {
287
+ description: "Flux UI tools: component index, blade converter, page generator",
288
+ handler: async (_args, ctx) => {
289
+ const options = [
290
+ "Component Index -- fetch live Flux UI component reference",
291
+ "Blade to Flux -- analyze a Blade file for conversion opportunities",
292
+ "Generate Page -- scaffold a complete Flux UI page",
293
+ ];
294
+
295
+ const selected = await ctx.ui.select("Flux UI Tools", options);
296
+ if (!selected) return;
297
+
298
+ if (selected.startsWith("Component Index")) {
299
+ elyra.sendUserMessage("Fetch the Flux UI component index and show me the available components.");
300
+ } else if (selected.startsWith("Blade to Flux")) {
301
+ elyra.sendUserMessage("I want to convert a Blade view to use Flux UI components. Ask me which file to analyze.");
302
+ } else if (selected.startsWith("Generate Page")) {
303
+ elyra.sendUserMessage("I want to generate a new Flux UI page. Ask me what the page should do.");
304
+ }
305
+ },
306
+ });
307
+ }
308
+
309
+ function toPascalCase(str: string): string {
310
+ return str
311
+ .split(/[-_\s]+/)
312
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
313
+ .join("");
314
+ }
315
+
316
+ function toSnakeCase(str: string): string {
317
+ return str.replace(/[-\s]+/g, "_").toLowerCase();
318
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@elyracode/flux-ui",
3
+ "version": "0.3.7",
4
+ "description": "Elyra extension for Flux UI -- live component index, Blade-to-Flux converter, and page generator",
5
+ "type": "module",
6
+ "keywords": ["elyra-package", "flux-ui", "livewire", "laravel", "blade", "tailwind"],
7
+ "license": "MIT",
8
+ "author": "Knut W. Horne",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kwhorne/elyra.git",
12
+ "directory": "packages/flux-ui"
13
+ },
14
+ "elyra": {
15
+ "extensions": ["./extensions/index.ts"]
16
+ },
17
+ "peerDependencies": {
18
+ "@elyracode/coding-agent": "*",
19
+ "typebox": "*"
20
+ },
21
+ "scripts": {
22
+ "clean": "echo 'nothing to clean'",
23
+ "build": "echo 'nothing to build'",
24
+ "check": "echo 'nothing to check'"
25
+ }
26
+ }