@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 +57 -0
- package/extensions/github-fetch.ts +116 -0
- package/extensions/index.ts +318 -0
- package/package.json +26 -0
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
|
+
}
|