FluxScript 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of FluxScript might be problematic. Click here for more details.

fluxscript/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """FluxScript: Python-first multi-target application generation."""
2
+ from .dsl import App, Page, Text, Heading, Button, Input
3
+ from .version import __version__
4
+
5
+ __all__ = ["App", "Page", "Text", "Heading", "Button", "Input", "__version__"]
fluxscript/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ raise SystemExit(main())
fluxscript/analyzer.py ADDED
@@ -0,0 +1,72 @@
1
+ """Read-only source/project analyzer. It never imports or executes analyzed files."""
2
+ from __future__ import annotations
3
+ import ast
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+
8
+ SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", "build", "dist", ".tox"}
9
+ MAX_FILES = 5000
10
+ MAX_SOURCE_BYTES = 2_000_000
11
+ RISKY_CALLS = {"eval", "exec", "compile", "__import__", "system", "popen", "Popen", "run", "call", "check_call", "check_output", "pickle", "loads"}
12
+
13
+
14
+ def analyze_path(path: str | Path) -> dict:
15
+ root = Path(path).expanduser().resolve()
16
+ if not root.exists(): raise FileNotFoundError(f"Path does not exist: {root}")
17
+ files = [root] if root.is_file() else sorted(p for p in root.rglob("*") if p.is_file() and not any(part in SKIP_DIRS for part in p.parts))
18
+ if len(files) > MAX_FILES: raise ValueError(f"Analysis limit is {MAX_FILES} files; narrow the input path.")
19
+ report = {"tool": "FluxScript Analyzer", "version": "0.1.0", "root": str(root), "file_count": 0,
20
+ "total_bytes": 0, "files": [], "python_summary": {"files": 0, "functions": [], "classes": [], "imports": [], "syntax_errors": []}, "findings": []}
21
+ summary = report["python_summary"]
22
+ for file in files:
23
+ try: stat = file.stat()
24
+ except OSError as exc:
25
+ report["findings"].append({"severity": "warning", "file": str(file), "message": f"Cannot stat file: {type(exc).__name__}"}); continue
26
+ item = {"path": str(file.relative_to(root) if root.is_dir() else file.name), "extension": file.suffix.lower(), "size_bytes": stat.st_size}
27
+ if stat.st_size > MAX_SOURCE_BYTES:
28
+ item["sha256"] = None
29
+ item["note"] = f"Skipped content analysis: file exceeds {MAX_SOURCE_BYTES} bytes."
30
+ report["findings"].append({"severity": "info", "file": item["path"], "message": item["note"]})
31
+ else:
32
+ try: raw = file.read_bytes()
33
+ except OSError as exc:
34
+ report["findings"].append({"severity": "warning", "file": item["path"], "message": f"Cannot read file: {type(exc).__name__}"}); continue
35
+ item["sha256"] = hashlib.sha256(raw).hexdigest()
36
+ if file.suffix.lower() == ".py":
37
+ summary["files"] += 1
38
+ try:
39
+ tree = ast.parse(raw.decode("utf-8"), filename=str(file))
40
+ functions = [n.name for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
41
+ classes = [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
42
+ imports = []
43
+ for node in ast.walk(tree):
44
+ if isinstance(node, ast.Import): imports.extend(alias.name for alias in node.names)
45
+ elif isinstance(node, ast.ImportFrom): imports.append(node.module or "")
46
+ summary["functions"].extend(f"{item['path']}:{name}" for name in functions)
47
+ summary["classes"].extend(f"{item['path']}:{name}" for name in classes)
48
+ summary["imports"].extend(imports)
49
+ for node in ast.walk(tree):
50
+ if isinstance(node, ast.Call):
51
+ name = node.func.id if isinstance(node.func, ast.Name) else node.func.attr if isinstance(node.func, ast.Attribute) else ""
52
+ if name in RISKY_CALLS:
53
+ report["findings"].append({"severity": "review", "file": item["path"], "line": getattr(node, "lineno", None), "message": f"Review dynamic execution/process/deserialization call: {name}"})
54
+ except (SyntaxError, UnicodeDecodeError) as exc:
55
+ issue = {"file": item["path"], "line": getattr(exc, "lineno", None), "message": str(exc)}
56
+ summary["syntax_errors"].append(issue)
57
+ report["findings"].append({"severity": "error", **issue})
58
+ report["files"].append(item)
59
+ report["total_bytes"] += stat.st_size
60
+ report["file_count"] = len(report["files"])
61
+ summary["imports"] = sorted(set(summary["imports"]))
62
+ report["summary"] = {"errors": sum(1 for f in report["findings"] if f["severity"] == "error"),
63
+ "review_items": sum(1 for f in report["findings"] if f["severity"] == "review"),
64
+ "warnings": sum(1 for f in report["findings"] if f["severity"] == "warning")}
65
+ return report
66
+
67
+
68
+ def write_report(report: dict, destination: str | Path) -> Path:
69
+ output = Path(destination).expanduser()
70
+ output.parent.mkdir(parents=True, exist_ok=True)
71
+ output.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
72
+ return output
@@ -0,0 +1,6 @@
1
+ from .web import generate_web
2
+ from .android import generate_android
3
+ from .ios import generate_ios
4
+ from .cli import generate_cli
5
+
6
+ BACKENDS = {"web": generate_web, "android": generate_android, "ios": generate_ios, "cli": generate_cli}
@@ -0,0 +1,127 @@
1
+ """Generate a multi-page native Android Kotlin/Gradle source starter."""
2
+ from html import escape as xml_escape
3
+ from pathlib import Path
4
+ from ..models import AppSpec
5
+
6
+
7
+ def _kotlin(value: str) -> str:
8
+ return value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("\n", "\\n").replace("\r", "\\r")
9
+
10
+
11
+ def _page_function(page, index: int) -> str:
12
+ body = []
13
+ for element in page.elements:
14
+ text = _kotlin(element.text)
15
+ if element.kind in {"text", "heading"}:
16
+ size = "24f" if element.kind == "heading" else "16f"
17
+ body.append(f''' content.addView(TextView(this).apply {{ text = "{text}"; textSize = {size}; setPadding(0, 12, 0, 12) }})''')
18
+ elif element.kind == "button":
19
+ action = _kotlin(element.action or "")
20
+ body.append(f''' content.addView(Button(this).apply {{ text = "{text}"; setOnClickListener {{ Toast.makeText(this@MainActivity, "Action requested: {action}", Toast.LENGTH_SHORT).show() }} }})''')
21
+ elif element.kind == "input":
22
+ name = _kotlin(element.name or "field")
23
+ input_type = element.attributes.get("input_type", "text")
24
+ body.append(f''' content.addView(EditText(this).apply {{ hint = "{text}"; tag = "{name}"; contentDescription = "{text}" }})''')
25
+ if not body:
26
+ body.append(' content.addView(TextView(this).apply { text = "No controls on this page." })')
27
+ return f''' private fun renderPage{index}() {{
28
+ content.removeAllViews()
29
+ {chr(10).join(body)}
30
+ }}'''
31
+
32
+
33
+ def generate_android(spec: AppSpec, out: Path) -> list[Path]:
34
+ issues = spec.validate()
35
+ if issues: raise ValueError("Invalid application: " + "; ".join(issues))
36
+ package = "dev.fluxscript.generated"
37
+ app_name = _kotlin(spec.name)
38
+ functions = [_page_function(page, i) for i, page in enumerate(spec.pages)]
39
+ tabs = []
40
+ for index, page in enumerate(spec.pages):
41
+ title = _kotlin(page.title or page.path)
42
+ tabs.append(f''' tabBar.addView(Button(this).apply {{ text = "{title}"; setOnClickListener {{ renderPage{index}() }} }})''')
43
+ activity = f'''package {package}
44
+
45
+ import android.app.Activity
46
+ import android.os.Bundle
47
+ import android.view.ViewGroup
48
+ import android.widget.Button
49
+ import android.widget.EditText
50
+ import android.widget.HorizontalScrollView
51
+ import android.widget.LinearLayout
52
+ import android.widget.ScrollView
53
+ import android.widget.TextView
54
+ import android.widget.Toast
55
+
56
+ class MainActivity : Activity() {{
57
+ private lateinit var content: LinearLayout
58
+
59
+ override fun onCreate(savedInstanceState: Bundle?) {{
60
+ super.onCreate(savedInstanceState)
61
+ title = "{app_name}"
62
+ val root = LinearLayout(this).apply {{
63
+ orientation = LinearLayout.VERTICAL
64
+ setPadding(20, 20, 20, 20)
65
+ layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
66
+ }}
67
+ val tabScroll = HorizontalScrollView(this)
68
+ val tabBar = LinearLayout(this).apply {{ orientation = LinearLayout.HORIZONTAL }}
69
+ tabScroll.addView(tabBar)
70
+ root.addView(tabScroll)
71
+ val scroll = ScrollView(this)
72
+ content = LinearLayout(this).apply {{ orientation = LinearLayout.VERTICAL }}
73
+ scroll.addView(content)
74
+ root.addView(scroll, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))
75
+ {chr(10).join(tabs)}
76
+ setContentView(root)
77
+ renderPage0()
78
+ }}
79
+
80
+ {chr(10).join(functions)}
81
+ }}
82
+ '''
83
+ gradle = '''plugins {
84
+ id("com.android.application") version "8.7.3"
85
+ id("org.jetbrains.kotlin.android") version "2.0.21"
86
+ }
87
+
88
+ android {
89
+ namespace = "dev.fluxscript.generated"
90
+ compileSdk = 35
91
+ defaultConfig {
92
+ applicationId = "dev.fluxscript.generated"
93
+ minSdk = 26
94
+ targetSdk = 35
95
+ versionCode = 1
96
+ versionName = "0.1.0"
97
+ }
98
+ }
99
+ '''
100
+ settings = '''pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }
101
+ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } }
102
+ rootProject.name = "FluxScriptGeneratedApp"
103
+ include(":app")
104
+ '''
105
+ manifest = f'''<manifest xmlns:android="http://schemas.android.com/apk/res/android">
106
+ <application android:label="{xml_escape(spec.name, quote=True)}" android:theme="@android:style/Theme.Material.Light.NoActionBar" android:usesCleartextTraffic="false">
107
+ <activity android:name="dev.fluxscript.generated.MainActivity" android:exported="true">
108
+ <intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
109
+ </activity>
110
+ </application>
111
+ </manifest>
112
+ '''
113
+ files = {
114
+ "settings.gradle.kts": settings,
115
+ "build.gradle.kts": 'plugins { id("com.android.application") version "8.7.3" apply false; id("org.jetbrains.kotlin.android") version "2.0.21" apply false }\n',
116
+ "app/build.gradle.kts": gradle,
117
+ "app/src/main/AndroidManifest.xml": manifest,
118
+ "app/src/main/java/dev/fluxscript/generated/MainActivity.kt": activity,
119
+ "README.md": "# Android project generated by FluxScript\n\nThis source starter includes a native Kotlin activity and a tab for every declared page. Buttons emit an action-request toast; implement and test your real application behavior before release. Open in Android Studio with a compatible JDK, Android SDK and Gradle/Android Gradle Plugin. The generator does not sign or package an APK.\n",
120
+ }
121
+ result = []
122
+ for name, content in files.items():
123
+ path = out / name
124
+ path.parent.mkdir(parents=True, exist_ok=True)
125
+ path.write_text(content, encoding="utf-8")
126
+ result.append(path)
127
+ return result
@@ -0,0 +1,50 @@
1
+ """Generate a standalone argparse-based CLI from an application specification."""
2
+ from pathlib import Path
3
+ from ..models import AppSpec
4
+
5
+
6
+ def generate_cli(spec: AppSpec, out: Path) -> list[Path]:
7
+ issues = spec.validate()
8
+ if issues: raise ValueError("Invalid application: " + "; ".join(issues))
9
+ actions = []
10
+ for page in spec.pages:
11
+ for element in page.elements:
12
+ if element.kind == "button" and element.action and element.action not in actions:
13
+ actions.append(element.action)
14
+ input_names = sorted({element.name for page in spec.pages for element in page.elements if element.kind == "input" and element.name})
15
+ command_specs = []
16
+ for index, action in enumerate(actions):
17
+ command_specs.append(f''' command_{index} = commands.add_parser({action!r}, help={("Request action " + action)!r})
18
+ command_{index}.set_defaults(action_name={action!r})''')
19
+ for name in input_names:
20
+ command_specs.append(f''' command_{index}.add_argument({("--" + name.replace("_", "-"))!r}, default="", help={("Value for " + name)!r})''')
21
+ if not actions:
22
+ command_specs.append(''' commands.add_parser("info", help="show application metadata").set_defaults(action_name="info")''')
23
+ value_names = ", ".join(repr(name) for name in input_names)
24
+ command_name = "".join(ch.lower() if ch.isalnum() else "-" for ch in spec.name).strip("-") or "fluxscript-app"
25
+ source = f'''#!/usr/bin/env python3
26
+ """Command-line interface generated by FluxScript for {spec.name!r}."""
27
+ import argparse
28
+ import json
29
+
30
+ APP_NAME = {spec.name!r}
31
+ INPUT_NAMES = [{value_names}]
32
+
33
+ def main(argv=None):
34
+ parser = argparse.ArgumentParser(prog={command_name!r}, description=APP_NAME)
35
+ commands = parser.add_subparsers(dest="command", required=True)
36
+ {chr(10).join(command_specs)}
37
+ args = parser.parse_args(argv)
38
+ values = {{name: getattr(args, name, "") for name in INPUT_NAMES}}
39
+ print(json.dumps({{"application": APP_NAME, "action": args.action_name, "values": values}}, ensure_ascii=False))
40
+ return 0
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())
44
+ '''
45
+ out.mkdir(parents=True, exist_ok=True)
46
+ cli_file = out / "app_cli.py"
47
+ cli_file.write_text(source, encoding="utf-8")
48
+ readme = out / "README.md"
49
+ readme.write_text(f"# {spec.name} command-line starter\n\nRun `python app_cli.py --help` to list generated action commands. Each command emits a JSON action request and supplied input values. Connect application-specific business logic before treating requests as completed operations.\n", encoding="utf-8")
50
+ return [cli_file, readme]
@@ -0,0 +1,66 @@
1
+ """Generate a SwiftUI source starter for every declared page."""
2
+ from pathlib import Path
3
+ import json
4
+ from ..models import AppSpec
5
+
6
+ def _swift(value: str) -> str:
7
+ # JSON string escaping is compatible with the common Swift string-literal escapes.
8
+ return json.dumps(str(value), ensure_ascii=False)
9
+
10
+ def _identifier(name: str) -> str:
11
+ clean = "".join(c if c.isalnum() or c == "_" else "_" for c in name)
12
+ if not clean or clean[0].isdigit(): clean = "field_" + clean
13
+ return "input_" + clean
14
+
15
+ def generate_ios(spec: AppSpec, out: Path) -> list[Path]:
16
+ issues = spec.validate()
17
+ if issues: raise ValueError("Invalid application: " + "; ".join(issues))
18
+ input_names = sorted({e.name for page in spec.pages for e in page.elements if e.kind == "input" and e.name})
19
+ state_lines = [f" @State private var {_identifier(name)}: String = \"\"" for name in input_names]
20
+ state_lines.append(' @State private var statusMessage: String = ""')
21
+ tabs=[]
22
+ for page in spec.pages:
23
+ rows=[]
24
+ for e in page.elements:
25
+ text = _swift(e.text)
26
+ if e.kind == "text": rows.append(f" Text({text}).frame(maxWidth: .infinity, alignment: .leading)")
27
+ elif e.kind == "heading": rows.append(f" Text({text}).font(.system(size: {max(16, 34 - int(e.attributes.get('level', 1)) * 3)}, weight: .bold)).frame(maxWidth: .infinity, alignment: .leading)")
28
+ elif e.kind == "button": rows.append(f" Button({text}) {{ statusMessage = {_swift('Action requested: ' + (e.action or ''))} }}.buttonStyle(.borderedProminent)")
29
+ elif e.kind == "input": rows.append(f" TextField({text}, text: ${_identifier(e.name or 'field')}).textFieldStyle(.roundedBorder)")
30
+ if not rows: rows.append(' Text("Generated by FluxScript")')
31
+ rows.append(' if !statusMessage.isEmpty { Text(statusMessage).font(.footnote).foregroundStyle(.secondary) }')
32
+ page_title = page.title or page.path
33
+ tabs.append(f''' NavigationStack {{
34
+ ScrollView {{
35
+ VStack(alignment: .leading, spacing: 16) {{
36
+ {chr(10).join(rows)}
37
+ }}
38
+ .padding()
39
+ }}
40
+ .navigationTitle({_swift(page_title)})
41
+ }}
42
+ .tabItem {{ Label({_swift(page_title)}, systemImage: "doc.text") }}''')
43
+ swift=f'''import SwiftUI
44
+
45
+ @main
46
+ struct GeneratedApp: App {{
47
+ var body: some Scene {{
48
+ WindowGroup {{ ContentView() }}
49
+ }}
50
+ }}
51
+
52
+ struct ContentView: View {{
53
+ {chr(10).join(state_lines)}
54
+
55
+ var body: some View {{
56
+ TabView {{
57
+ {chr(10).join(tabs)}
58
+ }}
59
+ }}
60
+ }}
61
+ '''
62
+ out.mkdir(parents=True, exist_ok=True)
63
+ source = out / "GeneratedApp.swift"; source.write_text(swift, encoding="utf-8")
64
+ readme = out / "README.md"
65
+ readme.write_text("# Generated SwiftUI source\n\nFluxScript generates a SwiftUI source starter with a tab for each declared page and editable text fields. Create an Xcode project on macOS, add `GeneratedApp.swift`, then build and test with Xcode. This generator does not create an `.xcodeproj`, compile, sign, or create an `.ipa`. Review navigation labels, action handlers, state management, accessibility and platform deployment settings before production use.\n", encoding="utf-8")
66
+ return [source, readme]
@@ -0,0 +1,140 @@
1
+ """Generate a dependency-light, multi-page accessible web starter."""
2
+ from html import escape
3
+ from pathlib import Path
4
+ import json
5
+ from ..models import AppSpec, ElementSpec
6
+
7
+ def _render_element(e: ElementSpec) -> str:
8
+ text = escape(e.text, quote=True)
9
+ if e.kind == "text": return f'<p class="copy">{text}</p>'
10
+ if e.kind == "heading":
11
+ level = int(e.attributes.get("level", 1))
12
+ return f'<h{level}>{text}</h{level}>'
13
+ if e.kind == "button": return f'<button type="button" data-action="{escape(e.action or "", quote=True)}">{text}</button>'
14
+ if e.kind == "input":
15
+ name = escape(e.name or "", quote=True); typ = escape(e.attributes.get("input_type", "text"), quote=True)
16
+ return f'<label>{text}<input name="{name}" type="{typ}" autocomplete="off" /></label>'
17
+ raise ValueError(f"Unknown element kind: {e.kind}")
18
+
19
+ def _json_script(value) -> str:
20
+ # JSON embedded in a script must not be able to close the script element.
21
+ return json.dumps(value, ensure_ascii=False).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
22
+
23
+ def generate_web(spec: AppSpec, out: Path) -> list[Path]:
24
+ issues = spec.validate()
25
+ if issues: raise ValueError("Invalid application: " + "; ".join(issues))
26
+ out.mkdir(parents=True, exist_ok=True)
27
+ title = escape(spec.name, quote=True)
28
+ nav = "\n ".join(f'<a href="#{escape(page.path, quote=True)}" data-route-link="{escape(page.path, quote=True)}">{escape(page.title or page.path)}</a>' for page in spec.pages)
29
+ sections=[]
30
+ for index, page in enumerate(spec.pages):
31
+ page_title = escape(page.title or spec.name, quote=True)
32
+ body = "\n ".join(_render_element(e) for e in page.elements)
33
+ sections.append(f'<section class="page" data-route="{escape(page.path, quote=True)}" aria-label="{page_title}"{"" if index == 0 else " hidden"}>\n <h1 class="page-title">{page_title}</h1>\n {body}\n </section>')
34
+ html = f'''<!doctype html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="utf-8" />
38
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
39
+ <meta name="description" content="{title} — generated with FluxScript" />
40
+ <meta name="color-scheme" content="light dark" />
41
+ <title>{title}</title>
42
+ <link rel="stylesheet" href="styles.css" />
43
+ </head>
44
+ <body>
45
+ <header class="topbar"><a class="brand" href="#/">{title}</a><nav aria-label="Main navigation">{nav}</nav></header>
46
+ <main class="app-shell" data-flux-app="{title}">
47
+ <div class="panel">\n {' '.join(sections)}\n <p class="status" role="status" aria-live="polite" hidden></p>
48
+ </div>
49
+ </main>
50
+ <script src="app.js" defer></script>
51
+ </body>
52
+ </html>
53
+ '''
54
+ css = '''* { box-sizing: border-box; }
55
+ :root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #172033; background: #f3f5f9; }
56
+ body { margin: 0; min-height: 100vh; }
57
+ .topbar { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px clamp(16px, 4vw, 48px); background: #fff; border-bottom: 1px solid #dce2ec; }
58
+ .brand { font-weight: 750; color: inherit; text-decoration: none; }
59
+ nav { display: flex; gap: 16px; flex-wrap: wrap; }
60
+ nav a { color: #2457d6; text-decoration: none; padding: 6px 2px; }
61
+ nav a[aria-current="page"] { font-weight: 700; text-decoration: underline; text-underline-offset: 5px; }
62
+ .app-shell { min-height: calc(100vh - 64px); display: grid; place-items: start center; padding: clamp(16px, 5vw, 56px); }
63
+ .panel { width: min(100%, 820px); padding: clamp(22px, 5vw, 48px); background: #fff; border: 1px solid #dce2ec; border-radius: 20px; box-shadow: 0 12px 36px #17203312; }
64
+ .page-title { margin-top: 0; line-height: 1.2; }
65
+ .copy { line-height: 1.7; color: #46536b; }
66
+ button { border: 0; border-radius: 10px; padding: 12px 18px; background: #2457d6; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
67
+ button:hover { background: #1744b4; }
68
+ button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #86a8ff; outline-offset: 3px; }
69
+ label { display: grid; gap: 8px; margin: 16px 0; font-weight: 550; }
70
+ input { width: 100%; max-width: 480px; padding: 11px 12px; border: 1px solid #aab5c7; border-radius: 8px; font: inherit; }
71
+ .status { margin-top: 24px; padding: 12px; background: #eef4ff; border-radius: 8px; color: #183b83; }
72
+ [hidden] { display: none !important; }
73
+ @media (max-width: 560px) { .topbar { align-items: flex-start; flex-direction: column; } nav { gap: 12px; } }
74
+ @media (prefers-color-scheme: dark) { :root { color: #eef2fa; background: #101521; } .topbar, .panel { background: #192131; border-color: #303b50; } .copy { color: #c0cadd; } input { background: #101521; color: inherit; border-color: #58657a; } .status { background: #20345b; color: #e2eaff; } }
75
+ '''
76
+ js = '''/** FluxScript runtime bridge. App-specific behavior is registered by the developer. */
77
+ (() => {
78
+ const root = document.querySelector("[data-flux-app]");
79
+ if (!root) return;
80
+ const pages = [...root.querySelectorAll("[data-route]")];
81
+ const links = [...document.querySelectorAll("[data-route-link]")];
82
+ const status = root.querySelector("[role=status]");
83
+ const normalize = (route) => route && route.startsWith("/") ? route : "/";
84
+ function navigate() {
85
+ const route = normalize(location.hash.slice(1) || "/");
86
+ const selected = pages.find((page) => page.dataset.route === route) || pages[0];
87
+ pages.forEach((page) => { page.hidden = page !== selected; });
88
+ links.forEach((link) => {
89
+ if (link.dataset.routeLink === selected.dataset.route) link.setAttribute("aria-current", "page");
90
+ else link.removeAttribute("aria-current");
91
+ });
92
+ }
93
+ window.addEventListener("hashchange", navigate);
94
+ navigate();
95
+ root.addEventListener("click", (event) => {
96
+ const target = event.target;
97
+ const button = target instanceof Element ? target.closest("button[data-action]") : null;
98
+ if (!button) return;
99
+ const action = button.dataset.action || "";
100
+ const values = Object.fromEntries([...root.querySelectorAll("input[name]")].map((input) => [input.name, input.value]));
101
+ const accepted = root.dispatchEvent(new CustomEvent("flux:action", { bubbles: true, cancelable: true, detail: { action, values } }));
102
+ if (accepted && status) { status.hidden = false; status.textContent = `Action requested: ${action}. Connect a handler for application behavior.`; }
103
+ });
104
+ // Example integration: root.addEventListener("flux:action", (event) => { ... });
105
+ })();
106
+ '''
107
+ ts = '''/** Typed counterpart of app.js; check with `npm install && npm run typecheck`. */
108
+ type FluxActionDetail = { action: string; values: Record<string, string> };
109
+ const root = document.querySelector<HTMLElement>("[data-flux-app]");
110
+ if (root) {
111
+ const pages = Array.from(root.querySelectorAll<HTMLElement>("[data-route]"));
112
+ const links = Array.from(document.querySelectorAll<HTMLAnchorElement>("[data-route-link]"));
113
+ const status = root.querySelector<HTMLElement>("[role=status]");
114
+ const normalize = (route: string): string => route.startsWith("/") ? route : "/";
115
+ const navigate = (): void => {
116
+ const route = normalize(location.hash.slice(1) || "/");
117
+ const selected = pages.find((page) => page.dataset.route === route) ?? pages[0];
118
+ if (!selected) return;
119
+ pages.forEach((page) => { page.hidden = page !== selected; });
120
+ links.forEach((link) => link.dataset.routeLink === selected.dataset.route ? link.setAttribute("aria-current", "page") : link.removeAttribute("aria-current"));
121
+ };
122
+ window.addEventListener("hashchange", navigate); navigate();
123
+ root.addEventListener("click", (event: MouseEvent) => {
124
+ const target = event.target;
125
+ const button = target instanceof Element ? target.closest<HTMLButtonElement>("button[data-action]") : null;
126
+ if (!button) return;
127
+ const values = Object.fromEntries(Array.from(root.querySelectorAll<HTMLInputElement>("input[name]")).map((input) => [input.name, input.value]));
128
+ root.dispatchEvent(new CustomEvent<FluxActionDetail>("flux:action", { bubbles: true, cancelable: true, detail: { action: button.dataset.action ?? "", values } }));
129
+ if (status) { status.hidden = false; status.textContent = `Action requested: ${button.dataset.action ?? ""}. Connect a handler for application behavior.`; }
130
+ });
131
+ }
132
+ '''
133
+ files = {"index.html": html, "styles.css": css, "app.js": js, "app.ts": ts,
134
+ "package.json": '{\n "name": "fluxscript-generated-app",\n "private": true,\n "version": "0.1.0",\n "scripts": { "typecheck": "tsc --noEmit" },\n "devDependencies": { "typescript": "^5.0.0" }\n}\n',
135
+ "tsconfig.json": '{\n "compilerOptions": { "target": "ES2020", "module": "ES2020", "strict": true, "noEmit": true, "lib": ["ES2020", "DOM"] },\n "include": ["app.ts"]\n}\n',
136
+ "README.md": f"# {spec.name}\n\nGenerated by FluxScript 0.1.0. Supports all declared pages through hash navigation. Run `python -m http.server 8000` and open http://127.0.0.1:8000 to preview. For TypeScript validation, install Node.js/npm then run `npm install && npm run typecheck`. Buttons emit a cancellable `flux:action` event with `{{action, values}}; register an application handler before treating actions as business functionality. Review security, accessibility, persistence, API integration and browser support before production deployment.\n"}
137
+ result=[]
138
+ for name, content in files.items():
139
+ path=out/name; path.parent.mkdir(parents=True,exist_ok=True); path.write_text(content,encoding="utf-8"); result.append(path)
140
+ return result