feat(forms): migrate all form definitions and configs to Nickel (.ncl)
Some checks failed
CI / Lint (bash) (push) Has been cancelled
CI / Lint (markdown) (push) Has been cancelled
CI / Lint (nickel) (push) Has been cancelled
CI / Lint (nushell) (push) Has been cancelled
CI / Lint (rust) (push) Has been cancelled
CI / Benchmark (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / License Compliance (push) Has been cancelled
CI / Code Coverage (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled

Replace all TOML form definitions in examples/ and config/ with
  type-checked Nickel equivalents. Update cli_loader to prefer .ncl
  (via nickel export) over .toml in config search order.
  TOML support retained as fallback — no breaking change.

  - El loader usa nickel export --format json + serde_json como puente — evita reimplementar un parser Nickel en Rust y aprovecha el binario ya existente.
  - El orden de búsqueda .ncl > .toml permite migración incremental: cualquier config vieja sigue funcionando sin tocarla.
  - Los contratos Nickel (| default, | String) en los configs sustituyen la validación que antes era implícita en el parsing TOML — el error llega antes (en nickel export) con mensajes más descriptivos.
This commit is contained in:
Jesús Pérez 2026-03-08 23:20:50 +00:00
parent baff1c42ec
commit a963adbf5b
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
184 changed files with 5247 additions and 7405 deletions

View File

@ -2,6 +2,29 @@
## [Unreleased]
### Changed - Nickel as primary form and config format
All form definitions and backend configs migrated from TOML to Nickel (`.ncl`).
**Examples** (`examples/01-basic/` through `examples/17-advanced-i18n/`): all `.toml` form files
replaced with type-checked `.ncl` equivalents using Nickel contracts and `| default` annotations.
**Backend configs** (`config/cli/`, `config/tui/`, `config/web/`, `config/ai/`, `config/ag/`,
`config/prov-gen/`): `default.toml`, `dev.toml`, `production.toml` replaced with `config.ncl`,
`dev.ncl`, `production.ncl`. Each expresses schema constraints inline (`| default`, `| String`,
`| Bool`) rather than raw TOML values.
**Config loader** (`crates/typedialog-core/src/config/cli_loader.rs`): search order updated to
prefer `.ncl` over `.toml`. NCL files are loaded via `nickel export --format json` and deserialised
with `serde_json`; TOML path retained as fallback. New order:
1. `{backend}/{TYPEDIALOG_ENV}.ncl`
2. `{backend}/{TYPEDIALOG_ENV}.toml`
3. `{backend}/config.ncl`
4. `{backend}/config.toml`
TOML form definitions remain supported — no breaking change to the parser or any backend.
### Refactored - Eliminate duplicated field execution logic
**Single source of truth for CLI field dispatch**

View File

@ -18,7 +18,7 @@
- **6 Backends**: CLI (inquire), TUI (ratatui), Web (axum), AI (RAG/embeddings), Agent (LLM execution), Prov-gen (IaC generation)
- **8 Prompt Types**: text, confirm, select, multi-select, password, date, editor, custom
- **Declarative Forms**: TOML-based definitions with fragments & composition
- **Declarative Forms**: Nickel (`.ncl`) definitions with type contracts, fragments & composition (TOML also supported)
- **4 Output Formats**: JSON, YAML, TOML, Nickel with roundtrip conversion
- **Zero Runtime Dependencies**: Core library works standalone
@ -68,7 +68,7 @@ just test::all
cargo run --example form
# Run with defaults pre-loaded
typedialog form config.toml --defaults defaults.json
typedialog form config.ncl --defaults defaults.json
```text
## Unified Command Interface
@ -77,15 +77,15 @@ All backends are accessible through the **single `typedialog` command** with aut
```bash
# Each backend can be invoked directly via typedialog
typedialog web form config.toml --port 8080 # Web backend (browser forms)
typedialog tui config.toml # TUI backend (terminal UI)
typedialog web form config.ncl --port 8080 # Web backend (browser forms)
typedialog tui config.ncl # TUI backend (terminal UI)
typedialog ai serve --port 8765 # AI backend (RAG assistant)
typedialog ag run agent.mdx # Agent backend (LLM agents)
typedialog prov-gen generate --spec project.ncl # Provisioning generator
# Or use specific binaries if you prefer
typedialog-web form config.toml --port 8080
typedialog-tui config.toml
typedialog-web form config.ncl --port 8080
typedialog-tui config.ncl
typedialog-ai serve --port 8765
typedialog-ag run agent.mdx
typedialog-prov-gen generate --spec project.ncl
@ -98,7 +98,7 @@ typedialog ag -h
typedialog prov-gen -h
```text
All backends produce identical JSON output from the same TOML form definition, making it easy to switch between interfaces without changing your data.
All backends produce identical JSON output from the same form definition (Nickel or TOML), making it easy to switch between interfaces without changing your data.
## Backends at a Glance
@ -117,7 +117,7 @@ typedialog select "Choose role" Admin User Guest
typedialog text "Email" --format json
# Pre-populate form with defaults
typedialog form schema.toml --defaults config.json --format json
typedialog form schema.ncl --defaults config.json --format json
```text
**Use for:** Scripts, CI/CD pipelines, server tools, piping between tools
@ -129,10 +129,10 @@ Full terminal UI with keyboard navigation and mouse support.
```bash
# Via dispatcher
typedialog tui config.toml
typedialog tui config.ncl
# Or run directly
typedialog-tui config.toml
typedialog-tui config.ncl
cargo run -p typedialog-tui --example form_with_autocompletion
```text
@ -145,11 +145,11 @@ HTTP server with browser-based forms.
```bash
# Via dispatcher
typedialog web form config.toml --port 8080
typedialog web form config.ncl --port 8080
# Or run directly
typedialog-web form config.toml --port 8080
cargo run -p typedialog-web -- --config config/web/dev.toml
typedialog-web form config.ncl --port 8080
cargo run -p typedialog-web -- --config config/web/dev.ncl
# Open http://localhost:8080
```text
@ -168,10 +168,10 @@ typedialog ai serve --port 8765
typedialog-ai serve --port 8765
# Query knowledge base
typedialog-ai --config config/ai/dev.toml --query "How do I configure encryption?"
typedialog-ai --config config/ai/dev.ncl --query "How do I configure encryption?"
# Build knowledge graph
typedialog-ai --config config/ai/production.toml --build-graph ./docs
typedialog-ai --config config/ai/production.ncl --build-graph ./docs
```text
**Use for:** Documentation search, context-aware assistance, knowledge retrieval, semantic search
@ -319,13 +319,10 @@ typedialog-prov-gen --name myproject --dry-run
Generate interactive forms from Nickel schemas, collect user input, and produce validated configuration output:
```bash
# 1. Define schema in Nickel
nickel eval config.ncl > schema.toml
# 1. Define form directly in Nickel
typedialog form config.ncl --backend tui
# 2. Run interactive form
typedialog form schema.toml --backend tui
# 3. Get validated output in any format
# 2. Get validated output in any format
# JSON, YAML, TOML, or back to Nickel with type preservation
```text

86
assets/web/README.md Normal file
View File

@ -0,0 +1,86 @@
# TypeDialog Web Assets
Web-based landing page, architecture diagram, and static content for TypeDialog.
## Directory Structure
```text
assets/web/
├── src/
│ ├── index.html # Source HTML (readable)
│ └── architecture-diagram.html # Source architecture viewer
├── index.html # Minified/Production HTML
├── architecture-diagram.html # Minified architecture viewer
├── typedialog_architecture.svg # Architecture diagram (dark mode)
├── typedialog_architecture_white.svg # Architecture diagram (light mode)
├── minify.sh # Build script
└── README.md
```
## Files
### `src/index.html` - Source Landing Page
- Inline CSS and JavaScript (no external dependencies beyond Inter font)
- Bilingual content (English/Spanish) with localStorage persistence
- Dark/light theme toggle with logo swap
- Responsive design (mobile-first)
- Link to architecture diagram viewer
### `src/architecture-diagram.html` - Architecture Viewer
- Full-page SVG architecture diagram
- Dark/light theme toggle (swaps between two SVG variants)
- Back-link to landing page
- Shares theme preference with landing page via localStorage
### Architecture SVGs
Two variants of the architecture diagram:
- `typedialog_architecture.svg` - Dark background (#0f0f1a)
- `typedialog_architecture_white.svg` - Light background (#ffffff)
Both show the complete TypeDialog architecture:
- Form Definitions layer (Nickel + TOML + load_form)
- typedialog-core (three-phase execution, core modules, BackendFactory)
- 6 Backends (CLI, TUI, Web, AI, Agent, Prov-Gen)
- Output formats (JSON, YAML, TOML, Nickel Roundtrip)
- LLM Providers (Claude, OpenAI, Gemini, Ollama)
- Integrations (Nushell, Nickel Contracts, Tera, Multi-Cloud, CI/CD)
## Development
Edit source files in `src/`, then regenerate minified versions:
```bash
chmod +x assets/web/minify.sh
./assets/web/minify.sh
```
## Deployment
Serve from any static web server:
```bash
# Rust
cargo install static-web-server
static-web-server -d assets/web/
# Python
python3 -m http.server --directory assets/web
# Node.js
npx http-server assets/web
```
Logo SVGs are referenced from the parent `assets/` directory via relative paths (`../typedialog_logo_h.svg`).
## Features
- **Responsive**: Mobile-first with media queries
- **Performance**: Inline CSS/JS, no frameworks, minified production
- **Bilingual**: EN/ES with dynamic switching
- **Theming**: Dark/light with localStorage persistence and logo swap
- **Architecture**: SVG diagram with dark/light variants showing full system

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>TypeDialog — Architecture</title><style> @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");:root{--bg-primary:#0f0f1a;--text-primary:#c8ccd4;--border-light:rgba(255,255,255,0.1);--border-color:rgba(79,70,229,0.3);}html.light-mode{--bg-primary:#ffffff;--text-primary:#1a1a1a;--border-light:rgba(0,0,0,0.1);--border-color:rgba(0,0,0,0.1);}*{margin:0;padding:0;box-sizing:border-box;}body{background:var(--bg-primary);color:var(--text-primary);font-family:"Inter",-apple-system,BlinkMacSystemFont,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:background-color 0.3s ease,color 0.3s ease;}.diagram-container{width:1400px;height:1020px;position:relative;display:inline-block;}.diagram-container img{width:100%;height:100%;}.nav-toggle{position:fixed;top:2rem;right:2rem;z-index:100;display:flex;gap:0.5rem;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:20px;padding:0.3rem;backdrop-filter:blur(10px);box-shadow:0 8px 32px rgba(0,0,0,0.1);}.nav-btn{background:transparent;border:none;color:#94a3b8;padding:0.5rem 1rem;border-radius:18px;cursor:pointer;font-weight:600;font-size:0.85rem;text-transform:uppercase;transition:all 0.3s ease;font-family:"Inter",sans-serif;text-decoration:none;display:inline-block;}.nav-btn.active{background:linear-gradient(135deg,#4f46e5 0%,#6366f1 100%);color:#fff;}.nav-btn:hover{color:#4f46e5;}.theme-toggle{background:transparent;border:none;color:var(--text-primary);padding:0.5rem 1rem;border-radius:18px;cursor:pointer;font-weight:700;font-size:1.2rem;transition:all 0.3s ease;}.theme-toggle:hover{color:#4f46e5;}@media (max-width:768px){.nav-toggle{top:1rem;right:1rem;}.diagram-container{width:100vw;height:auto;}}</style></head><body><div class="nav-toggle"><button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode"><span id="theme-icon">&#9790;</span></button><a href="index.html" class="nav-btn" style="background: rgba(79, 70, 229, 0.2); border: 1px solid rgba(79, 70, 229, 0.5);" >&larr; TYPEDIALOG</a></div><div class="diagram-container"><img id="dark-svg" src="typedialog_architecture.svg" alt="TypeDialog Architecture - Dark Mode" style="width: 100%; height: 100%; display: block;" /><img id="light-svg" src="typedialog_architecture_white.svg" alt="TypeDialog Architecture - Light Mode" style="width: 100%; height: 100%; display: none;" /></div><script> var THEME_KEY = "typedialog-theme";function getTheme(){return localStorage.getItem(THEME_KEY)|| "dark";}function setTheme(theme){localStorage.setItem(THEME_KEY,theme);var html = document.documentElement;var icon = document.getElementById("theme-icon");var darkSvg = document.getElementById("dark-svg");var lightSvg = document.getElementById("light-svg");if(theme === "light"){html.classList.add("light-mode");icon.textContent = "\u263D";if(darkSvg)darkSvg.style.display = "none";if(lightSvg)lightSvg.style.display = "block";}else{html.classList.remove("light-mode");icon.textContent = "\u2600";if(darkSvg)darkSvg.style.display = "block";if(lightSvg)lightSvg.style.display = "none";}}function toggleTheme(){var currentTheme = getTheme();var newTheme = currentTheme === "dark" ? "light" : "dark";setTheme(newTheme);}document.addEventListener("DOMContentLoaded",function(){setTheme(getTheme());});</script></body></html>

1
assets/web/index.html Normal file

File diff suppressed because one or more lines are too long

101
assets/web/minify.sh Executable file
View File

@ -0,0 +1,101 @@
#!/bin/bash
# Minify HTML files from src/ to production versions
# Usage: ./minify.sh
set -e
BASE_DIR="$(dirname "$0")"
FILES=("index.html" "architecture-diagram.html")
minify_file() {
local filename="$1"
local SRC_FILE="${BASE_DIR}/src/${filename}"
local OUT_FILE="${BASE_DIR}/${filename}"
local TEMP_FILE="${OUT_FILE}.tmp"
if [ ! -f "$SRC_FILE" ]; then
echo " Source file not found: $SRC_FILE (skipping)"
return 0
fi
echo ""
echo " Minifying ${filename}..."
echo " Input: $SRC_FILE"
echo " Output: $OUT_FILE"
perl -e "
use strict;
use warnings;
open(my \$fh, '<', '$SRC_FILE') or die \$!;
my \$content = do { local \$/; <\$fh> };
close(\$fh);
# Remove HTML comments
\$content =~ s/<!--.*?-->//gs;
# Compress CSS (remove spaces and comments)
\$content =~ s/(<style[^>]*>)(.*?)(<\/style>)/
my \$before = \$1;
my \$style = \$2;
my \$after = \$3;
\$style =~ s{\/\*.*?\*\/}{}gs;
\$style =~ s{\s+}{ }gs;
\$style =~ s{\s*([{}:;,>+~])\s*}{\$1}gs;
\$before . \$style . \$after;
/gies;
# Compress JavaScript (remove comments and extra spaces)
\$content =~ s/(<script[^>]*>)(.*?)(<\/script>)/
my \$before = \$1;
my \$script = \$2;
my \$after = \$3;
\$script =~ s{\/\/.*\$}{}gm;
\$script =~ s{\s+}{ }gs;
\$script =~ s{\s*([{}();,])\s*}{\$1}gs;
\$before . \$script . \$after;
/gies;
# Remove whitespace between tags
\$content =~ s/>\s+</></gs;
# Compress general whitespace
\$content =~ s/\s+/ /gs;
# Trim
\$content =~ s/^\s+|\s+\$//g;
open(my \$out, '>', '$TEMP_FILE') or die \$!;
print \$out \$content;
close(\$out);
" || {
echo " Minification failed"
rm -f "$TEMP_FILE"
exit 1
}
mv "$TEMP_FILE" "$OUT_FILE"
# Show statistics
original=$(wc -c < "$SRC_FILE")
minified=$(wc -c < "$OUT_FILE")
saved=$((original - minified))
percent=$((saved * 100 / original))
echo ""
echo " Compression statistics:"
printf " Original: %6d bytes\n" "$original"
printf " Minified: %6d bytes\n" "$minified"
printf " Saved: %6d bytes (%d%%)\n" "$saved" "$percent"
echo " ${filename} ready for production"
}
echo "Starting HTML minification..."
for file in "${FILES[@]}"; do
minify_file "$file"
done
echo ""
echo "All files minified."
echo ""

View File

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TypeDialog — Architecture</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");
:root {
--bg-primary: #0f0f1a;
--text-primary: #c8ccd4;
--border-light: rgba(255, 255, 255, 0.1);
--border-color: rgba(79, 70, 229, 0.3);
}
html.light-mode {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--border-light: rgba(0, 0, 0, 0.1);
--border-color: rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: background-color 0.3s ease, color 0.3s ease;
}
.diagram-container {
width: 1400px;
height: 1020px;
position: relative;
display: inline-block;
}
.diagram-container img {
width: 100%;
height: 100%;
}
.nav-toggle {
position: fixed;
top: 2rem;
right: 2rem;
z-index: 100;
display: flex;
gap: 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 0.3rem;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.nav-btn {
background: transparent;
border: none;
color: #94a3b8;
padding: 0.5rem 1rem;
border-radius: 18px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
text-decoration: none;
display: inline-block;
}
.nav-btn.active {
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
color: #fff;
}
.nav-btn:hover {
color: #4f46e5;
}
.theme-toggle {
background: transparent;
border: none;
color: var(--text-primary);
padding: 0.5rem 1rem;
border-radius: 18px;
cursor: pointer;
font-weight: 700;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.theme-toggle:hover {
color: #4f46e5;
}
@media (max-width: 768px) {
.nav-toggle { top: 1rem; right: 1rem; }
.diagram-container { width: 100vw; height: auto; }
}
</style>
</head>
<body>
<div class="nav-toggle">
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode">
<span id="theme-icon">&#9790;</span>
</button>
<a href="index.html" class="nav-btn"
style="background: rgba(79, 70, 229, 0.2); border: 1px solid rgba(79, 70, 229, 0.5);"
>&larr; TYPEDIALOG</a>
</div>
<div class="diagram-container">
<img id="dark-svg" src="typedialog_architecture.svg"
alt="TypeDialog Architecture - Dark Mode"
style="width: 100%; height: 100%; display: block;" />
<img id="light-svg" src="typedialog_architecture_white.svg"
alt="TypeDialog Architecture - Light Mode"
style="width: 100%; height: 100%; display: none;" />
</div>
<script>
var THEME_KEY = "typedialog-theme";
function getTheme() {
return localStorage.getItem(THEME_KEY) || "dark";
}
function setTheme(theme) {
localStorage.setItem(THEME_KEY, theme);
var html = document.documentElement;
var icon = document.getElementById("theme-icon");
var darkSvg = document.getElementById("dark-svg");
var lightSvg = document.getElementById("light-svg");
if (theme === "light") {
html.classList.add("light-mode");
icon.textContent = "\u263D";
if (darkSvg) darkSvg.style.display = "none";
if (lightSvg) lightSvg.style.display = "block";
} else {
html.classList.remove("light-mode");
icon.textContent = "\u2600";
if (darkSvg) darkSvg.style.display = "block";
if (lightSvg) lightSvg.style.display = "none";
}
}
function toggleTheme() {
var currentTheme = getTheme();
var newTheme = currentTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
}
document.addEventListener("DOMContentLoaded", function() {
setTheme(getTheme());
});
</script>
</body>
</html>

807
assets/web/src/index.html Normal file
View File

@ -0,0 +1,807 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title
data-en="TypeDialog - Type-Safe Interactive Dialogs"
data-es="TypeDialog - Diálogos Interactivos con Type-Safety"
>TypeDialog</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg-primary: #0f0f1a;
--bg-gradient-1: rgba(79, 70, 229, 0.12);
--bg-gradient-2: rgba(99, 102, 241, 0.10);
--bg-gradient-3: rgba(58, 58, 80, 0.08);
--text-primary: #ffffff;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--text-dark: #64748b;
--border-light: rgba(255, 255, 255, 0.1);
--accent: #4f46e5;
--accent-light: #6366f1;
--primary-gray: #3a3a50;
}
html.light-mode {
--bg-primary: #f9fafb;
--bg-gradient-1: rgba(79, 70, 229, 0.06);
--bg-gradient-2: rgba(99, 102, 241, 0.05);
--bg-gradient-3: rgba(58, 58, 80, 0.03);
--text-primary: #1a1a1a;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-dark: #9ca3af;
--border-light: rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
overflow-x: hidden;
transition: background-color 0.3s ease, color 0.3s ease;
}
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background:
radial-gradient(circle at 20% 40%, var(--bg-gradient-1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, var(--bg-gradient-2) 0%, transparent 50%),
radial-gradient(circle at 50% 90%, var(--bg-gradient-3) 0%, transparent 50%);
transition: background 0.3s ease;
}
/* Navigation */
.language-toggle {
position: fixed;
top: 2rem;
right: 2rem;
z-index: 100;
display: flex;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(79, 70, 229, 0.3);
border-radius: 20px;
padding: 0.3rem;
}
.lang-btn {
background: transparent;
border: none;
color: #94a3b8;
padding: 0.5rem 1rem;
border-radius: 18px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
text-decoration: none;
display: inline-block;
}
.lang-btn.active {
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
color: #fff;
}
.lang-btn:hover {
color: #4f46e5;
}
.theme-toggle {
background: transparent;
border: none;
color: var(--text-primary);
padding: 0.5rem 1rem;
border-radius: 18px;
cursor: pointer;
font-weight: 700;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.theme-toggle:hover {
color: #4f46e5;
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
position: relative;
}
/* Header */
header {
text-align: center;
padding: 5rem 0 4rem;
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.status-badge {
display: inline-block;
background: rgba(79, 70, 229, 0.2);
border: 1px solid #4f46e5;
color: #4f46e5;
padding: 0.5rem 1.5rem;
border-radius: 50px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
.logo-container {
margin-bottom: 2rem;
text-align: center;
}
.logo-container img {
max-width: 440px;
width: 100%;
height: auto;
filter: drop-shadow(0 0 20px rgba(79, 70, 229, 0.3));
margin: 0 auto;
}
.tagline {
font-size: 0.95rem;
color: #4f46e5;
font-weight: 400;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 1rem;
}
h1 {
font-size: 2.8rem;
font-weight: 800;
line-height: 1.2;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 50%, #818cf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 1.15rem;
color: var(--text-secondary);
max-width: 800px;
margin: 0 auto 2rem;
line-height: 1.8;
}
.highlight {
color: #4f46e5;
font-weight: 700;
}
/* Sections */
.section {
margin: 4rem 0;
animation: fadeInUp 0.8s ease-out;
}
.section-title {
font-size: 2rem;
font-weight: 800;
margin-bottom: 2rem;
color: #4f46e5;
text-align: center;
}
.section-title span {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Problem cards */
.problems-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.problem-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(79, 70, 229, 0.3);
border-radius: 12px;
padding: 2rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.problem-card:hover {
transform: translateY(-5px);
background: rgba(255, 255, 255, 0.05);
border-color: rgba(79, 70, 229, 0.5);
}
.problem-number {
font-size: 2rem;
font-weight: 800;
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.problem-card h3 {
color: #818cf8;
font-size: 1.05rem;
margin-bottom: 0.7rem;
font-weight: 700;
}
.problem-card p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.6;
}
/* Tech stack */
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
justify-content: center;
}
.tech-badge {
background: rgba(79, 70, 229, 0.15);
border: 1px solid #4f46e5;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
color: #4f46e5;
font-weight: 600;
}
/* Feature grid */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.feature-box {
background: linear-gradient(135deg, rgba(79, 70, 229, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
border-radius: 12px;
padding: 2rem;
border-left: 4px solid #4f46e5;
transition: all 0.3s ease;
}
.feature-box:hover {
background: linear-gradient(135deg, rgba(79, 70, 229, 0.12) 0%, rgba(99, 102, 241, 0.12) 100%);
transform: translateY(-3px);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.feature-title {
font-size: 1.15rem;
font-weight: 700;
color: #4f46e5;
margin-bottom: 0.7rem;
}
.feature-text {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.7;
}
/* Backend grid */
.backends-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.backend-item {
background: rgba(79, 70, 229, 0.08);
padding: 1.2rem;
border-radius: 8px;
font-size: 0.9rem;
border: 1px solid rgba(79, 70, 229, 0.3);
transition: all 0.2s ease;
text-align: center;
}
.backend-item:hover {
background: rgba(79, 70, 229, 0.12);
transform: translateY(-2px);
}
.backend-name {
color: #818cf8;
font-weight: 700;
display: block;
margin-bottom: 0.3rem;
}
.backend-role {
color: var(--text-muted);
font-size: 0.85rem;
}
/* CTA */
.cta-section {
text-align: center;
margin: 5rem 0 3rem;
padding: 4rem 2rem;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
border-radius: 20px;
border: 1px solid rgba(79, 70, 229, 0.3);
}
.cta-title {
font-size: 2rem;
font-weight: 800;
margin-bottom: 1rem;
background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.cta-button {
display: inline-block;
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 50%, #818cf8 100%);
color: #fff;
padding: 1.1rem 2.8rem;
border-radius: 50px;
text-decoration: none;
font-weight: 800;
font-size: 1rem;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(79, 70, 229, 0.3);
text-transform: uppercase;
letter-spacing: 0.05em;
border: none;
cursor: pointer;
}
.cta-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 20px 50px rgba(79, 70, 229, 0.5);
}
/* Footer */
footer {
text-align: center;
padding: 3rem 0 2rem;
color: var(--text-dark);
border-top: 1px solid var(--border-light);
margin-top: 4rem;
font-size: 0.9rem;
}
footer p:first-child {
font-weight: 700;
color: var(--text-muted);
}
footer p:last-child {
margin-top: 0.5rem;
font-size: 0.85rem;
}
/* Responsive */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
.hero-subtitle { font-size: 1rem; }
.logo-container img { max-width: 352px; }
.section-title { font-size: 1.6rem; }
.cta-title { font-size: 1.6rem; }
.language-toggle { top: 1rem; right: 1rem; }
}
</style>
</head>
<body>
<div class="gradient-bg"></div>
<div class="language-toggle">
<button class="lang-btn active" data-lang="en" onclick="switchLanguage('en')">EN</button>
<button class="lang-btn" data-lang="es" onclick="switchLanguage('es')">ES</button>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode">
<span id="theme-icon">&#9790;</span>
</button>
<a href="architecture-diagram.html" class="lang-btn"
style="background: rgba(79, 70, 229, 0.2); border: 1px solid rgba(79, 70, 229, 0.5); text-decoration: none;"
data-en="ARCHITECTURE"
data-es="ARQUITECTURA"
>ARCHITECTURE</a>
</div>
<div class="container">
<!-- ═══════════════ HEADER ═══════════════ -->
<header>
<span class="status-badge"
data-en="3,818 Tests | 6 Backends | 4 LLM Providers"
data-es="3.818 Tests | 6 Backends | 4 Proveedores LLM"
>3,818 Tests | 6 Backends | 4 LLM Providers</span>
<div class="logo-container">
<img id="logo-dark" src="../typedialog_logo_h.svg" alt="TypeDialog" style="display: block;" />
<img id="logo-light" src="../typedialog_logo_h.svg" alt="TypeDialog" style="display: none;" />
</div>
<p class="tagline">Typed dialogs for inputs, forms and schemas you can trust</p>
<h1
data-en="Create Type-Safe<br>Interactive Dialogs"
data-es="Crea Di&aacute;logos Interactivos<br>con Type-Safety"
>Create Type-Safe<br>Interactive Dialogs</h1>
<p class="hero-subtitle">
<span class="highlight"
data-en="Declarative forms"
data-es="Formularios declarativos"
>Declarative forms</span>
<span
data-en=" with Nickel/TOML definitions, 6 backends (CLI, TUI, Web, AI, Agent, Prov-Gen), and type-safe validation. From interactive prompts to infrastructure generation."
data-es=" con definiciones Nickel/TOML, 6 backends (CLI, TUI, Web, AI, Agent, Prov-Gen) y validaci&oacute;n type-safe. Desde prompts interactivos hasta generaci&oacute;n de infraestructura."
> with Nickel/TOML definitions, 6 backends (CLI, TUI, Web, AI, Agent, Prov-Gen), and type-safe validation. From interactive prompts to infrastructure generation.</span>
<br>
<strong
data-en="One schema. Every surface."
data-es="Un schema. Toda superficie."
>One schema. Every surface.</strong>
</p>
</header>
<!-- ═══════════════ WHAT IT SOLVES ═══════════════ -->
<section class="section">
<h2 class="section-title">
<span
data-en="What TypeDialog Solves"
data-es="Lo que TypeDialog Resuelve"
>What TypeDialog Solves</span>
</h2>
<div class="problems-grid">
<div class="problem-card">
<div class="problem-number">01</div>
<h3 data-en="Per-Backend Code" data-es="C&oacute;digo Por-Backend">Per-Backend Code</h3>
<p data-en="One Nickel or TOML definition drives CLI, TUI, Web, and AI. No per-backend form code. The schema is the single source of truth."
data-es="Una definici&oacute;n Nickel o TOML controla CLI, TUI, Web y AI. Sin c&oacute;digo de formularios por backend. El schema es la fuente &uacute;nica de verdad."
>One Nickel or TOML definition drives CLI, TUI, Web, and AI. No per-backend form code. The schema is the single source of truth.</p>
</div>
<div class="problem-card">
<div class="problem-number">02</div>
<h3 data-en="Fail-Open Validation" data-es="Validaci&oacute;n Fail-Open">Fail-Open Validation</h3>
<p data-en="Nickel contracts validate every predicate at load time. Unknown predicates cause hard failures, not silent passes. No parallel Rust reimplementation."
data-es="Los contratos Nickel validan cada predicado en tiempo de carga. Predicados desconocidos causan fallos duros, no passes silenciosos. Sin reimplementaci&oacute;n paralela en Rust."
>Nickel contracts validate every predicate at load time. Unknown predicates cause hard failures, not silent passes. No parallel Rust reimplementation.</p>
</div>
<div class="problem-card">
<div class="problem-number">03</div>
<h3 data-en="Hidden I/O in Execution" data-es="I/O Oculto en Ejecuci&oacute;n">Hidden I/O in Execution</h3>
<p data-en="Fragment loading happens once at load_form(). The three-phase executor is pure &mdash; no filesystem access, no side effects during user interaction."
data-es="La carga de fragmentos ocurre una vez en load_form(). El executor de tres fases es puro &mdash; sin acceso al filesystem, sin efectos secundarios durante la interacci&oacute;n."
>Fragment loading happens once at load_form(). The three-phase executor is pure &mdash; no filesystem access, no side effects during user interaction.</p>
</div>
<div class="problem-card">
<div class="problem-number">04</div>
<h3 data-en="Manual IaC Assembly" data-es="Ensamblaje Manual de IaC">Manual IaC Assembly</h3>
<p data-en="Interactive forms generate validated infrastructure configurations for 6 cloud providers. 7-layer validation pipeline from forms to final JSON."
data-es="Formularios interactivos generan configuraciones de infraestructura validadas para 6 proveedores cloud. Pipeline de validaci&oacute;n de 7 capas desde formularios hasta JSON final."
>Interactive forms generate validated infrastructure configurations for 6 cloud providers. 7-layer validation pipeline from forms to final JSON.</p>
</div>
</div>
</section>
<!-- ═══════════════ HOW IT WORKS ═══════════════ -->
<section class="section">
<h2 class="section-title">
<span
data-en="How It Works"
data-es="C&oacute;mo Funciona"
>How It Works</span>
</h2>
<div class="features-grid">
<div class="feature-box">
<div class="feature-icon">&lt;&gt;</div>
<h3 class="feature-title"
data-en="Declarative Forms"
data-es="Formularios Declarativos"
>Declarative Forms</h3>
<p class="feature-text"
data-en="Define forms in Nickel (.ncl) or TOML (.toml). Nickel provides contracts, imports, and type-safe composition. TOML provides zero-dependency simplicity. Both produce identical FormDefinition structs."
data-es="Define formularios en Nickel (.ncl) o TOML (.toml). Nickel provee contratos, imports y composici&oacute;n type-safe. TOML provee simplicidad sin dependencias. Ambos producen structs FormDefinition id&eacute;nticos."
>Define forms in Nickel (.ncl) or TOML (.toml). Nickel provides contracts, imports, and type-safe composition. TOML provides zero-dependency simplicity. Both produce identical FormDefinition structs.</p>
</div>
<div class="feature-box" style="border-left-color: #6366f1;">
<div class="feature-icon">&#9881;</div>
<h3 class="feature-title" style="color: #6366f1;"
data-en="Three-Phase Execution"
data-es="Ejecuci&oacute;n en Tres Fases"
>Three-Phase Execution</h3>
<p class="feature-text"
data-en="Phase 1: Execute selector fields that control conditionals. Phase 2: Build element list (pure, no I/O). Phase 3: Dispatch to backend with when/when_false evaluation. Complete or field-by-field rendering modes."
data-es="Fase 1: Ejecutar campos selector que controlan condicionales. Fase 2: Construir lista de elementos (puro, sin I/O). Fase 3: Despachar al backend con evaluaci&oacute;n when/when_false. Modos de renderizado completo o campo a campo."
>Phase 1: Execute selector fields that control conditionals. Phase 2: Build element list (pure, no I/O). Phase 3: Dispatch to backend with when/when_false evaluation. Complete or field-by-field rendering modes.</p>
</div>
<div class="feature-box" style="border-left-color: #818cf8;">
<div class="feature-icon">&#128268;</div>
<h3 class="feature-title" style="color: #818cf8;"
data-en="BackendFactory"
data-es="BackendFactory"
>BackendFactory</h3>
<p class="feature-text"
data-en="Compile-time feature gates (#[cfg(feature)]) eliminate dead backend code. Runtime BackendType match dispatches to Box&lt;dyn FormBackend&gt;. Auto-detection via TYPEDIALOG_BACKEND env var with CLI fallback."
data-es="Feature gates en tiempo de compilaci&oacute;n (#[cfg(feature)]) eliminan c&oacute;digo muerto. Match runtime de BackendType despacha a Box&lt;dyn FormBackend&gt;. Auto-detecci&oacute;n via env TYPEDIALOG_BACKEND con fallback CLI."
>Compile-time feature gates (#[cfg(feature)]) eliminate dead backend code. Runtime BackendType match dispatches to Box&lt;dyn FormBackend&gt;. Auto-detection via TYPEDIALOG_BACKEND env var with CLI fallback.</p>
</div>
<div class="feature-box" style="border-left-color: #a855f7;">
<div class="feature-icon">&#129302;</div>
<h3 class="feature-title" style="color: #a855f7;"
data-en="AI &amp; Agent Backends"
data-es="Backends AI y Agent"
>AI &amp; Agent Backends</h3>
<p class="feature-text"
data-en="AI backend with RAG, embeddings, and semantic search. Agent backend executes .agent.mdx files with multi-LLM support (Claude, OpenAI, Gemini, Ollama). Template variables, file imports, streaming output."
data-es="Backend AI con RAG, embeddings y b&uacute;squeda sem&aacute;ntica. Backend Agent ejecuta archivos .agent.mdx con soporte multi-LLM (Claude, OpenAI, Gemini, Ollama). Variables de template, imports de archivos, output en streaming."
>AI backend with RAG, embeddings, and semantic search. Agent backend executes .agent.mdx files with multi-LLM support (Claude, OpenAI, Gemini, Ollama). Template variables, file imports, streaming output.</p>
</div>
<div class="feature-box" style="border-left-color: #10b981;">
<div class="feature-icon">&#9729;</div>
<h3 class="feature-title" style="color: #10b981;"
data-en="Infrastructure Generation"
data-es="Generaci&oacute;n de Infraestructura"
>Infrastructure Generation</h3>
<p class="feature-text"
data-en="Prov-Gen transforms form answers into IaC configurations. 6 cloud providers (AWS, GCP, Azure, Hetzner, UpCloud, LXD). 7-layer validation: Forms &rarr; Constraints &rarr; Values &rarr; Validators &rarr; Schemas &rarr; Defaults &rarr; JSON."
data-es="Prov-Gen transforma respuestas de formularios en configuraciones IaC. 6 proveedores cloud (AWS, GCP, Azure, Hetzner, UpCloud, LXD). Validaci&oacute;n de 7 capas: Forms &rarr; Constraints &rarr; Values &rarr; Validators &rarr; Schemas &rarr; Defaults &rarr; JSON."
>Prov-Gen transforms form answers into IaC configurations. 6 cloud providers (AWS, GCP, Azure, Hetzner, UpCloud, LXD). 7-layer validation: Forms &rarr; Constraints &rarr; Values &rarr; Validators &rarr; Schemas &rarr; Defaults &rarr; JSON.</p>
</div>
<div class="feature-box" style="border-left-color: #f59e0b;">
<div class="feature-icon">&#128260;</div>
<h3 class="feature-title" style="color: #f59e0b;"
data-en="Nickel Roundtrip"
data-es="Roundtrip Nickel"
>Nickel Roundtrip</h3>
<p class="feature-text"
data-en="Read .ncl schemas, collect user input via any backend, generate validated .ncl output preserving contracts. ContractParser extracts validators. TemplateRenderer preserves formatting. when_false ensures all schema fields have values."
data-es="Lee schemas .ncl, recolecta input del usuario via cualquier backend, genera output .ncl validado preservando contratos. ContractParser extrae validadores. TemplateRenderer preserva formato. when_false asegura que todos los campos del schema tengan valores."
>Read .ncl schemas, collect user input via any backend, generate validated .ncl output preserving contracts. ContractParser extracts validators. TemplateRenderer preserves formatting. when_false ensures all schema fields have values.</p>
</div>
</div>
</section>
<!-- ═══════════════ TECH STACK ═══════════════ -->
<section class="section">
<h2 class="section-title">
<span
data-en="Technology Stack"
data-es="Stack Tecnol&oacute;gico"
>Technology Stack</span>
</h2>
<div class="tech-stack">
<span class="tech-badge">Rust (8 crates)</span>
<span class="tech-badge">Nickel Contracts</span>
<span class="tech-badge">TOML Forms</span>
<span class="tech-badge">Inquire (CLI)</span>
<span class="tech-badge">Ratatui (TUI)</span>
<span class="tech-badge">Axum (Web)</span>
<span class="tech-badge">Fluent i18n</span>
<span class="tech-badge">Tera Templates</span>
<span class="tech-badge">Nushell Plugin</span>
<span class="tech-badge">Claude API</span>
<span class="tech-badge">OpenAI API</span>
<span class="tech-badge">Gemini API</span>
<span class="tech-badge">Ollama (local)</span>
<span class="tech-badge">Multi-Cloud IaC</span>
</div>
</section>
<!-- ═══════════════ BACKENDS ═══════════════ -->
<section class="section">
<h2 class="section-title">
<span
data-en="Backends"
data-es="Backends"
>Backends</span>
</h2>
<div class="backends-grid">
<div class="backend-item">
<span class="backend-name"
data-en="CLI"
data-es="CLI"
>CLI</span>
<span class="backend-role"
data-en="inquire &mdash; interactive prompts"
data-es="inquire &mdash; prompts interactivos"
>inquire &mdash; interactive prompts</span>
</div>
<div class="backend-item">
<span class="backend-name"
data-en="TUI"
data-es="TUI"
>TUI</span>
<span class="backend-role"
data-en="ratatui &mdash; terminal UI"
data-es="ratatui &mdash; interfaz de terminal"
>ratatui &mdash; terminal UI</span>
</div>
<div class="backend-item">
<span class="backend-name"
data-en="Web"
data-es="Web"
>Web</span>
<span class="backend-role"
data-en="axum &mdash; HTTP forms"
data-es="axum &mdash; formularios HTTP"
>axum &mdash; HTTP forms</span>
</div>
<div class="backend-item">
<span class="backend-name"
data-en="AI"
data-es="AI"
>AI</span>
<span class="backend-role"
data-en="RAG &amp; embeddings"
data-es="RAG y embeddings"
>RAG &amp; embeddings</span>
</div>
<div class="backend-item">
<span class="backend-name"
data-en="Agent"
data-es="Agent"
>Agent</span>
<span class="backend-role"
data-en="Multi-LLM execution"
data-es="Ejecuci&oacute;n multi-LLM"
>Multi-LLM execution</span>
</div>
<div class="backend-item">
<span class="backend-name"
data-en="Prov-Gen"
data-es="Prov-Gen"
>Prov-Gen</span>
<span class="backend-role"
data-en="IaC generation"
data-es="Generaci&oacute;n IaC"
>IaC generation</span>
</div>
</div>
</section>
<!-- ═══════════════ CTA ═══════════════ -->
<div class="cta-section">
<h2 class="cta-title"
data-en="Type-safe dialogs for every surface"
data-es="Di&aacute;logos type-safe para toda superficie"
>Type-safe dialogs for every surface</h2>
<p style="color: #94a3b8; margin-bottom: 2rem; font-size: 1.05rem;"
data-en="Built with Rust | Open Source | MIT License"
data-es="Construido con Rust | Open Source | Licencia MIT"
>Built with Rust | Open Source | MIT License</p>
<a href="https://github.com/anthropics/typedialog" class="cta-button"
data-en="Explore on GitHub &rarr;"
data-es="Explorar en GitHub &rarr;"
>Explore on GitHub &rarr;</a>
</div>
<!-- ═══════════════ FOOTER ═══════════════ -->
<footer>
<p data-en="TypeDialog" data-es="TypeDialog">TypeDialog</p>
<p data-en="Typed dialogs for inputs, forms and schemas you can trust"
data-es="Di&aacute;logos tipados para inputs, formularios y schemas en los que puedes confiar"
>Typed dialogs for inputs, forms and schemas you can trust</p>
<p style="margin-top: 1rem; font-size: 0.8rem;"
data-en="Multi-Backend Form Orchestration | Nickel + TOML | 6 Backends | 4 LLM Providers"
data-es="Orquestaci&oacute;n Multi-Backend de Formularios | Nickel + TOML | 6 Backends | 4 Proveedores LLM"
>Multi-Backend Form Orchestration | Nickel + TOML | 6 Backends | 4 LLM Providers</p>
</footer>
</div>
<script>
const LANG_KEY = "typedialog-lang";
function getCurrentLanguage() {
return localStorage.getItem(LANG_KEY) || "en";
}
function switchLanguage(lang) {
localStorage.setItem(LANG_KEY, lang);
document.querySelectorAll(".lang-btn").forEach(function(btn) {
btn.classList.remove("active");
if (btn.dataset.lang === lang) {
btn.classList.add("active");
}
});
document.querySelectorAll("[data-en][data-es]").forEach(function(el) {
var content = el.dataset[lang];
if (el.tagName === "H1" || el.tagName === "H2" || el.tagName === "H3") {
el.innerHTML = content;
} else {
el.textContent = content;
}
});
document.documentElement.lang = lang;
}
document.addEventListener("DOMContentLoaded", function() {
var currentLang = getCurrentLanguage();
switchLanguage(currentLang);
var currentTheme = getTheme();
setTheme(currentTheme);
});
var THEME_KEY = "typedialog-theme";
function getTheme() {
return localStorage.getItem(THEME_KEY) || "dark";
}
function setTheme(theme) {
localStorage.setItem(THEME_KEY, theme);
var html = document.documentElement;
var icon = document.getElementById("theme-icon");
var logoDark = document.getElementById("logo-dark");
var logoLight = document.getElementById("logo-light");
if (theme === "light") {
html.classList.add("light-mode");
icon.textContent = "\u263D";
if (logoDark) logoDark.style.display = "none";
if (logoLight) logoLight.style.display = "block";
} else {
html.classList.remove("light-mode");
icon.textContent = "\u2600";
if (logoDark) logoDark.style.display = "block";
if (logoLight) logoLight.style.display = "none";
}
}
function toggleTheme() {
var currentTheme = getTheme();
var newTheme = currentTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
}
</script>
</body>
</html>

View File

@ -0,0 +1,253 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #94a3b8; }
.layer-label { font-size: 18px; font-weight: 600; fill: #e2e8f0; }
.module-label { font-size: 13px; font-weight: 600; fill: #ffffff; }
.module-detail { font-size: 11px; font-weight: 400; fill: #cbd5e1; }
.backend-label { font-size: 14px; font-weight: 700; fill: #ffffff; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #a5b4fc; }
.output-label { font-size: 13px; font-weight: 500; fill: #e2e8f0; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #64748b; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; }
.note-text { font-size: 10px; font-weight: 400; fill: #64748b; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
<!-- Rounded rect clip for backend boxes -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#0f0f1a"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGrad)" opacity="0.4"/>
<defs>
<radialGradient id="bgGrad" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.08"/>
<stop offset="100%" stop-color="#0f0f1a" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() &#8212; Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl &#8594; subprocess | .toml &#8594; serde | Fail-fast on errors</text>
<!-- Arrow: Form Defs -> Core -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#111827" stroke="#3a3a50" stroke-opacity="0.5" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<!-- Phase 1 -->
<rect x="80" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<!-- Phase 2 -->
<rect x="310" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<!-- Phase 3 -->
<rect x="540" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<!-- Phase arrows -->
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.5"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.5"/>
<!-- Core modules row -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<!-- form_parser -->
<rect x="80" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<!-- validation -->
<rect x="275" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<!-- i18n -->
<rect x="470" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<!-- encryption -->
<rect x="665" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<!-- templates -->
<rect x="860" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- CLI -->
<rect x="60" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<!-- TUI -->
<rect x="270" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#22d3ee" stroke-opacity="0.6"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<!-- Web -->
<rect x="480" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#10b981" stroke-opacity="0.6"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<!-- AI -->
<rect x="690" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#a855f7" stroke-opacity="0.6"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<!-- Agent -->
<rect x="900" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#ec4899" stroke-opacity="0.6"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<!-- Prov-Gen -->
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#1a3a2e" stroke="#10b981" stroke-opacity="0.6"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Output format boxes -->
<rect x="60" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.5"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#1a1a2e" stroke="#ec4899" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,229 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #64748b; }
.layer-label { font-size: 18px; font-weight: 600; fill: #1e293b; }
.module-label { font-size: 13px; font-weight: 600; fill: #1e293b; }
.module-detail { font-size: 11px; font-weight: 400; fill: #475569; }
.backend-label { font-size: 14px; font-weight: 700; fill: #1e293b; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #4f46e5; }
.output-label { font-size: 13px; font-weight: 500; fill: #1e293b; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #94a3b8; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; letter-spacing: 0.05em; }
.note-text { font-size: 10px; font-weight: 400; fill: #94a3b8; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#ffffff"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGradLight)" opacity="0.3"/>
<defs>
<radialGradient id="bgGradLight" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.04"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.25" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() — Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl → subprocess | .toml → serde | Fail-fast on errors</text>
<!-- Arrow -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#f1f5f9" stroke="#3a3a50" stroke-opacity="0.2" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<rect x="80" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<rect x="310" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<rect x="540" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.4"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.4"/>
<!-- Core modules -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<rect x="80" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<rect x="275" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<rect x="470" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<rect x="665" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<rect x="860" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="610" width="190" height="90" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<rect x="270" y="610" width="190" height="90" rx="8" fill="#ecfeff" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<rect x="480" y="610" width="190" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<rect x="690" y="610" width="190" height="90" rx="8" fill="#ede9fe" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<rect x="900" y="610" width="190" height="90" rx="8" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.3"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#a855f7" stroke-opacity="0.3"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.1" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#22d3ee" stroke-opacity="0.3"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.3"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#ec4899" stroke-opacity="0.3"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,253 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #94a3b8; }
.layer-label { font-size: 18px; font-weight: 600; fill: #e2e8f0; }
.module-label { font-size: 13px; font-weight: 600; fill: #ffffff; }
.module-detail { font-size: 11px; font-weight: 400; fill: #cbd5e1; }
.backend-label { font-size: 14px; font-weight: 700; fill: #ffffff; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #a5b4fc; }
.output-label { font-size: 13px; font-weight: 500; fill: #e2e8f0; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #64748b; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; }
.note-text { font-size: 10px; font-weight: 400; fill: #64748b; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
<!-- Rounded rect clip for backend boxes -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#0f0f1a"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGrad)" opacity="0.4"/>
<defs>
<radialGradient id="bgGrad" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.08"/>
<stop offset="100%" stop-color="#0f0f1a" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() &#8212; Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl &#8594; subprocess | .toml &#8594; serde | Fail-fast on errors</text>
<!-- Arrow: Form Defs -> Core -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#111827" stroke="#3a3a50" stroke-opacity="0.5" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<!-- Phase 1 -->
<rect x="80" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<!-- Phase 2 -->
<rect x="310" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<!-- Phase 3 -->
<rect x="540" y="335" width="200" height="55" rx="6" fill="#1e293b" stroke="#4f46e5" stroke-opacity="0.4"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<!-- Phase arrows -->
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.5"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.4" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.5"/>
<!-- Core modules row -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<!-- form_parser -->
<rect x="80" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<!-- validation -->
<rect x="275" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<!-- i18n -->
<rect x="470" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<!-- encryption -->
<rect x="665" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<!-- templates -->
<rect x="860" y="425" width="175" height="70" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.5"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.4" stroke-width="1.5"/>
<!-- CLI -->
<rect x="60" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-opacity="0.6"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<!-- TUI -->
<rect x="270" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#22d3ee" stroke-opacity="0.6"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<!-- Web -->
<rect x="480" y="610" width="190" height="90" rx="8" fill="#1e3a5f" stroke="#10b981" stroke-opacity="0.6"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<!-- AI -->
<rect x="690" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#a855f7" stroke-opacity="0.6"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<!-- Agent -->
<rect x="900" y="610" width="190" height="90" rx="8" fill="#2d1b69" stroke="#ec4899" stroke-opacity="0.6"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<!-- Prov-Gen -->
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#1a3a2e" stroke="#10b981" stroke-opacity="0.6"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.5" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.6"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#1a1a2e" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<!-- Output format boxes -->
<rect x="60" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#2d1b69" stroke="#7c3aed" stroke-opacity="0.5"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#1a1a2e" stroke="#ec4899" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#0d1117" stroke="#3a3a50" stroke-opacity="0.3" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#6366f1" stroke-opacity="0.4"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#1e293b" stroke="#10b981" stroke-opacity="0.4"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-opacity="0.4"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,229 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1020" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
text { font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; }
.title { font-size: 28px; font-weight: 700; fill: #4f46e5; }
.subtitle { font-size: 14px; font-weight: 400; fill: #64748b; }
.layer-label { font-size: 18px; font-weight: 600; fill: #1e293b; }
.module-label { font-size: 13px; font-weight: 600; fill: #1e293b; }
.module-detail { font-size: 11px; font-weight: 400; fill: #475569; }
.backend-label { font-size: 14px; font-weight: 700; fill: #1e293b; }
.backend-detail { font-size: 11px; font-weight: 400; fill: #4f46e5; }
.output-label { font-size: 13px; font-weight: 500; fill: #1e293b; }
.arrow-text { font-size: 11px; font-weight: 500; fill: #94a3b8; }
.phase-label { font-size: 11px; font-weight: 600; fill: #4f46e5; letter-spacing: 0.05em; }
.note-text { font-size: 10px; font-weight: 400; fill: #94a3b8; }
.section-badge { font-size: 10px; font-weight: 700; fill: #4f46e5; letter-spacing: 0.1em; text-transform: uppercase; }
</style>
</defs>
<!-- Background -->
<rect width="1400" height="1020" rx="16" fill="#ffffff"/>
<rect width="1400" height="1020" rx="16" fill="url(#bgGradLight)" opacity="0.3"/>
<defs>
<radialGradient id="bgGradLight" cx="30%" cy="40%" r="60%">
<stop offset="0%" stop-color="#4f46e5" stop-opacity="0.04"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Title -->
<text x="700" y="50" text-anchor="middle" class="title">TypeDialog Architecture</text>
<text x="700" y="72" text-anchor="middle" class="subtitle">Multi-Backend Form Orchestration Layer</text>
<!-- ═══════════════ LAYER 1: Form Definitions ═══════════════ -->
<text x="60" y="115" class="section-badge">FORM DEFINITIONS</text>
<rect x="40" y="125" width="1320" height="100" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.25" stroke-width="1.5"/>
<!-- Nickel box -->
<rect x="80" y="145" width="280" height="60" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="220" y="170" text-anchor="middle" class="module-label">Nickel (.ncl)</text>
<text x="220" y="188" text-anchor="middle" class="module-detail">nickel export --format json</text>
<!-- TOML box -->
<rect x="400" y="145" width="280" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="540" y="170" text-anchor="middle" class="module-label">TOML (.toml)</text>
<text x="540" y="188" text-anchor="middle" class="module-detail">serde direct deserialization</text>
<!-- load_form box -->
<rect x="780" y="145" width="540" height="60" rx="8" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1050" y="170" text-anchor="middle" class="module-label">load_form() — Unified Entry Point</text>
<text x="1050" y="188" text-anchor="middle" class="module-detail">Extension dispatch: .ncl → subprocess | .toml → serde | Fail-fast on errors</text>
<!-- Arrow -->
<line x1="700" y1="225" x2="700" y2="260" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,256 700,268 706,256" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="248" class="arrow-text">FormDefinition</text>
<!-- ═══════════════ LAYER 2: typedialog-core ═══════════════ -->
<text x="60" y="285" class="section-badge">TYPEDIALOG-CORE</text>
<rect x="40" y="295" width="1320" height="220" rx="10" fill="#f1f5f9" stroke="#3a3a50" stroke-opacity="0.2" stroke-width="1.5"/>
<!-- Three-Phase Execution -->
<text x="80" y="325" class="phase-label">THREE-PHASE EXECUTION</text>
<rect x="80" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="180" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 1: Selectors</text>
<text x="180" y="373" text-anchor="middle" class="module-detail">Identify &amp; execute</text>
<rect x="310" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="410" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 2: Element List</text>
<text x="410" y="373" text-anchor="middle" class="module-detail">Pure, no I/O</text>
<rect x="540" y="335" width="200" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.25"/>
<text x="640" y="355" text-anchor="middle" class="module-label" style="font-size:12px">Phase 3: Dispatch</text>
<text x="640" y="373" text-anchor="middle" class="module-detail">when/when_false eval</text>
<line x1="280" y1="362" x2="310" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="306,358 314,362 306,366" fill="#4f46e5" opacity="0.4"/>
<line x1="510" y1="362" x2="540" y2="362" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="1.5"/>
<polygon points="536,358 544,362 536,366" fill="#4f46e5" opacity="0.4"/>
<!-- Core modules -->
<text x="80" y="415" class="phase-label">CORE MODULES</text>
<rect x="80" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="167" y="450" text-anchor="middle" class="module-label">form_parser</text>
<text x="167" y="466" text-anchor="middle" class="module-detail">TOML/Nickel parsing</text>
<text x="167" y="480" text-anchor="middle" class="module-detail">Field definitions</text>
<rect x="275" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="362" y="450" text-anchor="middle" class="module-label">validation</text>
<text x="362" y="466" text-anchor="middle" class="module-detail">Nickel contracts</text>
<text x="362" y="480" text-anchor="middle" class="module-detail">Pre/post conditions</text>
<rect x="470" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="557" y="450" text-anchor="middle" class="module-label">i18n (Fluent)</text>
<text x="557" y="466" text-anchor="middle" class="module-detail">Locale detection</text>
<text x="557" y="480" text-anchor="middle" class="module-detail">.ftl translations</text>
<rect x="665" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="752" y="450" text-anchor="middle" class="module-label">encryption</text>
<text x="752" y="466" text-anchor="middle" class="module-detail">Field-level encrypt</text>
<text x="752" y="480" text-anchor="middle" class="module-detail">External services</text>
<rect x="860" y="425" width="175" height="70" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.25"/>
<text x="947" y="450" text-anchor="middle" class="module-label">templates (Tera)</text>
<text x="947" y="466" text-anchor="middle" class="module-detail">Jinja2-compatible</text>
<text x="947" y="480" text-anchor="middle" class="module-detail">Variable rendering</text>
<!-- RenderContext -->
<rect x="1055" y="335" width="270" height="55" rx="6" fill="#ffffff" stroke="#4f46e5" stroke-opacity="0.3"/>
<text x="1190" y="355" text-anchor="middle" class="module-label" style="font-size:12px">RenderContext</text>
<text x="1190" y="373" text-anchor="middle" class="module-detail">results: HashMap + locale</text>
<!-- BackendFactory -->
<rect x="1055" y="425" width="270" height="70" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.4"/>
<text x="1190" y="450" text-anchor="middle" class="module-label">BackendFactory</text>
<text x="1190" y="466" text-anchor="middle" class="module-detail">#[cfg(feature)] compile-time</text>
<text x="1190" y="480" text-anchor="middle" class="module-detail">+ runtime BackendType match</text>
<!-- Arrow: Core -> Backends -->
<line x1="700" y1="515" x2="700" y2="555" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,551 700,563 706,551" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="540" class="arrow-text">Box&lt;dyn FormBackend&gt;</text>
<text x="915" y="555" text-anchor="middle" class="note-text">trait FormBackend: Send + Sync { execute_field, execute_form_complete }</text>
<!-- ═══════════════ LAYER 3: Backends ═══════════════ -->
<text x="60" y="580" class="section-badge">BACKENDS (6 CRATES)</text>
<rect x="40" y="590" width="1320" height="130" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="610" width="190" height="90" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-opacity="0.4"/>
<text x="155" y="636" text-anchor="middle" class="backend-label">CLI</text>
<text x="155" y="654" text-anchor="middle" class="backend-detail">inquire 0.9</text>
<text x="155" y="670" text-anchor="middle" class="module-detail">Interactive prompts</text>
<text x="155" y="686" text-anchor="middle" class="module-detail">Scripts, CI/CD</text>
<rect x="270" y="610" width="190" height="90" rx="8" fill="#ecfeff" stroke="#22d3ee" stroke-opacity="0.4"/>
<text x="365" y="636" text-anchor="middle" class="backend-label">TUI</text>
<text x="365" y="654" text-anchor="middle" class="backend-detail">ratatui</text>
<text x="365" y="670" text-anchor="middle" class="module-detail">Terminal UI</text>
<text x="365" y="686" text-anchor="middle" class="module-detail">Keyboard + Mouse</text>
<rect x="480" y="610" width="190" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="575" y="636" text-anchor="middle" class="backend-label">Web</text>
<text x="575" y="654" text-anchor="middle" class="backend-detail">axum</text>
<text x="575" y="670" text-anchor="middle" class="module-detail">HTTP server</text>
<text x="575" y="686" text-anchor="middle" class="module-detail">Browser forms</text>
<rect x="690" y="610" width="190" height="90" rx="8" fill="#ede9fe" stroke="#a855f7" stroke-opacity="0.4"/>
<text x="785" y="636" text-anchor="middle" class="backend-label">AI</text>
<text x="785" y="654" text-anchor="middle" class="backend-detail">RAG + embeddings</text>
<text x="785" y="670" text-anchor="middle" class="module-detail">Semantic search</text>
<text x="785" y="686" text-anchor="middle" class="module-detail">Knowledge graph</text>
<rect x="900" y="610" width="190" height="90" rx="8" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.4"/>
<text x="995" y="636" text-anchor="middle" class="backend-label">Agent</text>
<text x="995" y="654" text-anchor="middle" class="backend-detail">Multi-LLM execution</text>
<text x="995" y="670" text-anchor="middle" class="module-detail">.agent.mdx files</text>
<text x="995" y="686" text-anchor="middle" class="module-detail">Streaming, templates</text>
<rect x="1110" y="610" width="210" height="90" rx="8" fill="#ecfdf5" stroke="#10b981" stroke-opacity="0.4"/>
<text x="1215" y="636" text-anchor="middle" class="backend-label">Prov-Gen</text>
<text x="1215" y="654" text-anchor="middle" class="backend-detail">IaC generation</text>
<text x="1215" y="670" text-anchor="middle" class="module-detail">AWS, GCP, Azure</text>
<text x="1215" y="686" text-anchor="middle" class="module-detail">Hetzner, UpCloud, LXD</text>
<!-- Arrow: Backends -> Output -->
<line x1="700" y1="720" x2="700" y2="755" stroke="#4f46e5" stroke-opacity="0.3" stroke-width="2" stroke-dasharray="6,3"/>
<polygon points="694,751 700,763 706,751" fill="#4f46e5" opacity="0.4"/>
<text x="720" y="743" class="arrow-text">HashMap&lt;String, Value&gt;</text>
<!-- ═══════════════ LAYER 4: Output ═══════════════ -->
<text x="60" y="780" class="section-badge">OUTPUT FORMATS</text>
<rect x="40" y="790" width="640" height="70" rx="10" fill="#f8fafc" stroke="#4f46e5" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="60" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="120" y="829" text-anchor="middle" class="output-label">JSON</text>
<rect x="200" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="260" y="829" text-anchor="middle" class="output-label">YAML</text>
<rect x="340" y="802" width="120" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="400" y="829" text-anchor="middle" class="output-label">TOML</text>
<rect x="480" y="802" width="180" height="44" rx="6" fill="#ede9fe" stroke="#7c3aed" stroke-opacity="0.3"/>
<text x="570" y="829" text-anchor="middle" class="output-label">Nickel Roundtrip</text>
<!-- ═══════════════ LAYER 4b: LLM Providers ═══════════════ -->
<text x="720" y="780" class="section-badge">LLM PROVIDERS</text>
<rect x="700" y="790" width="660" height="70" rx="10" fill="#fdf2f8" stroke="#ec4899" stroke-opacity="0.15" stroke-width="1.5"/>
<rect x="720" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#a855f7" stroke-opacity="0.3"/>
<text x="785" y="829" text-anchor="middle" class="output-label">Claude</text>
<rect x="870" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="935" y="829" text-anchor="middle" class="output-label">OpenAI</text>
<rect x="1020" y="802" width="130" height="44" rx="6" fill="#ffffff" stroke="#3b82f6" stroke-opacity="0.3"/>
<text x="1085" y="829" text-anchor="middle" class="output-label">Gemini</text>
<rect x="1170" y="802" width="170" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1255" y="829" text-anchor="middle" class="output-label">Ollama (local)</text>
<!-- ═══════════════ LAYER 5: Integrations ═══════════════ -->
<text x="60" y="890" class="section-badge">INTEGRATIONS</text>
<rect x="40" y="900" width="1320" height="70" rx="10" fill="#fafbfc" stroke="#3a3a50" stroke-opacity="0.1" stroke-width="1.5"/>
<rect x="60" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#22d3ee" stroke-opacity="0.3"/>
<text x="170" y="939" text-anchor="middle" class="output-label">Nushell Plugin</text>
<rect x="310" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#6366f1" stroke-opacity="0.3"/>
<text x="420" y="939" text-anchor="middle" class="output-label">Nickel Contracts</text>
<rect x="560" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#ec4899" stroke-opacity="0.3"/>
<text x="670" y="939" text-anchor="middle" class="output-label">Template Engine (Tera)</text>
<rect x="810" y="912" width="220" height="44" rx="6" fill="#ffffff" stroke="#10b981" stroke-opacity="0.3"/>
<text x="920" y="939" text-anchor="middle" class="output-label">Multi-Cloud APIs</text>
<rect x="1060" y="912" width="280" height="44" rx="6" fill="#ffffff" stroke="#f59e0b" stroke-opacity="0.3"/>
<text x="1200" y="939" text-anchor="middle" class="output-label">CI/CD (GitHub + Woodpecker)</text>
<!-- Stats footer -->
<text x="700" y="1000" text-anchor="middle" class="subtitle">8 crates | 6 backends | 3,818 tests | 4 output formats | 4 LLM providers | 6 cloud targets</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 64" fill="none">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&amp;display=swap');
.cursor {
animation: blink 1s ease-in-out infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.text-pulse {
animation: text-pulse 3s ease-in-out infinite;
}
@keyframes text-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
</style>
</defs>
<!-- Speech bubble con esquinas redondeadas y cola asimétrica -->
<path
d="M 12 14
C 12 9, 15 6, 19 6
L 45 6
C 49 6, 52 9, 52 14
L 52 32
C 52 37, 49 40, 45 40
L 26 40
L 20 46
L 20 40
L 19 40
C 15 40, 12 37, 12 32
Z"
fill="#3a3a50"
/>
<!-- Type brackets < > -->
<!-- Left bracket < -->
<path d="M 20 17 L 16 23 L 20 29" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<!-- Right bracket > -->
<path d="M 44 17 L 48 23 L 44 29" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<!-- Blinking cursor -->
<rect
class="cursor"
x="30"
y="17"
width="4"
height="12"
rx="2"
fill="#4f46e5"
/>
<!-- Text: TypeDialog with pulse animation -->
<text class="text-pulse" x="58" y="36" font-family="Inter, sans-serif" font-size="20" font-weight="500" fill="#8f96a3">TypeDialog</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

33
config/ag/config.ncl Normal file
View File

@ -0,0 +1,33 @@
{
agent = {
default_provider | default = "claude",
models = {
claude | default = "claude-3-5-haiku-20241022",
openai | default = "gpt-4o-mini",
gemini | default = "gemini-2.0-flash-exp",
ollama | default = "llama2",
},
defaults = {
max_tokens | default = 4096,
temperature | default = 0.7,
streaming | default = true,
},
template = {
engine | default = "tera",
strict_variables | default = false,
},
validation = {
enabled | default = true,
strict | default = false,
},
output = {
format | default = "markdown",
color | default = true,
timestamp | default = false,
},
logging = {
level | default = "info",
file | default = false,
},
},
}

View File

@ -1,39 +0,0 @@
# TypeDialog Agent - Default Configuration
[agent]
# Default LLM provider (claude, openai, gemini, ollama)
default_provider = "claude"
# Default model per provider
[agent.models]
claude = "claude-3-5-haiku-20241022"
gemini = "gemini-2.0-flash-exp"
ollama = "llama2"
openai = "gpt-4o-mini"
# Default settings
[agent.defaults]
max_tokens = 4096
streaming = true
temperature = 0.7
# Template settings
[agent.template]
engine = "tera" # Jinja2-compatible
strict_variables = false
# Validation settings
[agent.validation]
enabled = true
strict = false
# Output settings
[agent.output]
color = true
format = "markdown"
timestamp = false
# Logging
[agent.logging]
file = false
level = "info"

14
config/ag/dev.ncl Normal file
View File

@ -0,0 +1,14 @@
(import "./config.ncl")
& {
agent = {
default_provider = "ollama",
defaults.max_tokens = 2048,
template.strict_variables = true,
validation.strict = true,
output.timestamp = true,
logging = {
level = "debug",
file = true,
},
},
}

View File

@ -1,32 +0,0 @@
# TypeDialog Agent - Development Configuration
[agent]
default_provider = "ollama" # Use local for dev
[agent.models]
claude = "claude-3-5-haiku-20241022"
gemini = "gemini-2.0-flash-exp"
ollama = "llama2"
openai = "gpt-4o-mini"
[agent.defaults]
max_tokens = 2048 # Lower for dev
streaming = true
temperature = 0.7
[agent.template]
engine = "tera"
strict_variables = true # Catch template errors in dev
[agent.validation]
enabled = true
strict = true # Strict validation in dev
[agent.output]
color = true
format = "markdown"
timestamp = true
[agent.logging]
file = true
level = "debug" # Verbose in dev

29
config/ag/production.ncl Normal file
View File

@ -0,0 +1,29 @@
(import "./config.ncl")
& {
agent = {
default_provider = "claude",
models = {
claude = "claude-3-5-sonnet-20241022",
gemini = "gemini-1.5-pro",
openai = "gpt-4o",
},
defaults = {
max_tokens = 8192,
temperature = 0.3,
},
template.strict_variables = true,
validation.strict = true,
output = {
color = false,
timestamp = true,
},
logging = {
level = "warn",
file = true,
},
rate_limit = {
enabled = true,
max_requests_per_minute = 60,
},
},
}

View File

@ -1,37 +0,0 @@
# TypeDialog Agent - Production Configuration
[agent]
default_provider = "claude"
[agent.models]
claude = "claude-3-5-sonnet-20241022" # Higher quality for production
gemini = "gemini-1.5-pro"
ollama = "llama2"
openai = "gpt-4o"
[agent.defaults]
max_tokens = 8192
streaming = true
temperature = 0.3 # More consistent
[agent.template]
engine = "tera"
strict_variables = true
[agent.validation]
enabled = true
strict = true
[agent.output]
color = false # No color in production logs
format = "markdown"
timestamp = true
[agent.logging]
file = true
level = "warn" # Less verbose in production
# Rate limiting (production)
[agent.rate_limit]
enabled = true
max_requests_per_minute = 60

View File

@ -1,8 +0,0 @@
# TypeDialog Agent Server Configuration
# Copy to ~/.config/typedialog/agent-server/config.toml
# Server host (default: 127.0.0.1)
host = "127.0.0.1"
# Server port (default: 8765)
port = 8765

4
config/ag/server.ncl Normal file
View File

@ -0,0 +1,4 @@
{
host | default = "127.0.0.1",
port | default = 8765,
}

29
config/ai/config.ncl Normal file
View File

@ -0,0 +1,29 @@
{
llm = {
provider | default = "openai",
model | default = "gpt-3.5-turbo",
api_endpoint | default = "",
generation = {
temperature | default = 0.7,
max_tokens | default = 2048,
top_p | default = 0.9,
},
},
rag = {
enabled | default = true,
index_path | default = "~/.config/typedialog/ai/rag-index",
embedding_dims | default = 384,
cache_size | default = 1000,
},
microservice = {
host | default = "127.0.0.1",
port | default = 3001,
enable_cors | default = false,
enable_websocket | default = true,
},
appearance = {
interaction_mode | default = "interactive",
show_suggestions | default = true,
suggestion_confidence_threshold | default = 0.5,
},
}

View File

@ -1,66 +0,0 @@
# AI Backend - Default Configuration
# Provides intelligent form assistance using LLM + RAG system
[llm]
# LLM Provider: openai, anthropic, ollama
provider = "openai"
# Model to use for the selected provider
# OpenAI: gpt-4, gpt-3.5-turbo
# Anthropic: claude-3-opus, claude-3-sonnet, claude-3-haiku
# Ollama: depends on locally installed models
model = "gpt-3.5-turbo"
# API endpoint (optional, uses provider defaults if not set)
# OpenAI: https://api.openai.com/v1
# Anthropic: https://api.anthropic.com/v1
# Ollama: http://localhost:11434/api
api_endpoint = ""
[llm.generation]
# Temperature: 0.0-2.0, higher = more creative, lower = more focused
temperature = 0.7
# Maximum tokens in response
max_tokens = 2048
# Top-p (nucleus) sampling: 0.0-1.0
top_p = 0.9
[rag]
# Enable RAG (Retrieval-Augmented Generation) system
enabled = true
# Index directory for cached embeddings and vector store
# If relative path, resolved from ~/.config/typedialog/ai/
index_path = "~/.config/typedialog/ai/rag-index"
# Embedding dimensions: 384, 768, 1024
embedding_dims = 384
# Cache size for vector store (approximate, in embeddings)
cache_size = 1000
[microservice]
# HTTP server settings
host = "127.0.0.1"
port = 3001
# Enable CORS for web clients
enable_cors = false
# WebSocket support for streaming responses
enable_websocket = true
[appearance]
# Interaction mode: interactive, autocomplete, validate_only
# - interactive: LLM suggests, user can override
# - autocomplete: LLM generates all values
# - validate_only: User provides, LLM validates
interaction_mode = "interactive"
# Show LLM suggestions in user-facing prompts
show_suggestions = true
# Confidence threshold for suggestions (0.0-1.0)
suggestion_confidence_threshold = 0.5

18
config/ai/dev.ncl Normal file
View File

@ -0,0 +1,18 @@
(import "./config.ncl")
& {
llm = {
provider = "ollama",
model = "llama2",
api_endpoint = "http://localhost:11434/api",
generation = {
temperature = 0.5,
max_tokens = 1024,
},
},
rag = {
index_path = "~/.config/typedialog/ai/rag-index-dev",
cache_size = 500,
},
microservice.enable_cors = true,
appearance.suggestion_confidence_threshold = 0.3,
}

View File

@ -1,31 +0,0 @@
# AI Backend - Development Configuration
# Inherits defaults from default.toml, override values here for local development
[llm]
# Use ollama for local development (requires local Ollama instance)
api_endpoint = "http://localhost:11434/api"
model = "llama2" # Or whatever model you have installed locally
provider = "ollama"
[llm.generation]
# Faster responses for iteration
max_tokens = 1024
temperature = 0.5
[rag]
# Enable RAG for development
cache_size = 500
embedding_dims = 384
enabled = true
index_path = "~/.config/typedialog/ai/rag-index-dev"
[microservice]
enable_cors = true # Allow localhost:3000, localhost:5173, etc.
enable_websocket = true
host = "127.0.0.1"
port = 3001
[appearance]
interaction_mode = "interactive"
show_suggestions = true
suggestion_confidence_threshold = 0.3 # Lower threshold for dev feedback

27
config/ai/production.ncl Normal file
View File

@ -0,0 +1,27 @@
(import "./config.ncl")
& {
llm = {
provider = "anthropic",
model = "claude-3-sonnet-20240229",
api_endpoint = "",
generation = {
temperature = 0.3,
max_tokens = 1024,
top_p = 0.95,
},
},
rag = {
index_path = "/var/lib/typedialog/ai/rag-index",
embedding_dims = 768,
cache_size = 10000,
},
microservice = {
host = "0.0.0.0",
enable_cors = false,
},
appearance = {
interaction_mode = "validate_only",
show_suggestions = false,
suggestion_confidence_threshold = 0.8,
},
}

View File

@ -1,34 +0,0 @@
# AI Backend - Production Configuration
# Optimized for reliability, cost, and performance at scale
[llm]
# Production uses high-quality, stable models
api_endpoint = "" # Uses provider defaults (api.anthropic.com)
model = "claude-3-sonnet-20240229"
provider = "anthropic"
[llm.generation]
# Conservative settings for production
max_tokens = 1024 # Reasonable limit for cost control
temperature = 0.3 # More focused, less random
top_p = 0.95
[rag]
# Production RAG system with larger cache
cache_size = 10000 # Larger cache for frequently accessed data
embedding_dims = 768 # Higher quality embeddings
enabled = true
index_path = "/var/lib/typedialog/ai/rag-index" # System-wide index path
[microservice]
# Listen on all interfaces for container deployments
enable_cors = false # Restrict CORS for security
enable_websocket = true
host = "0.0.0.0"
port = 3001
[appearance]
# Production uses validation mode
interaction_mode = "validate_only"
show_suggestions = false # Don't show raw LLM output to users
suggestion_confidence_threshold = 0.8 # Only very confident suggestions

27
config/cli/config.ncl Normal file
View File

@ -0,0 +1,27 @@
{
form = {
title | default = "CLI Form",
description | default = "Standard command-line interface form",
validation = {
show_errors_inline | default = true,
validate_on_change | default = true,
strict_validation | default = false,
},
},
output = {
format | default = "json",
pretty_print | default = true,
debug_output | default = false,
},
terminal = {
use_raw_mode | default = true,
enable_mouse | default = false,
use_color | default = true,
},
appearance = {
theme | default = "default",
show_help | default = true,
show_placeholders | default = true,
show_field_types | default = false,
},
}

View File

@ -1,30 +0,0 @@
# CLI Backend - Default Configuration
# Used for standard command-line form rendering
[form]
description = "Standard command-line interface form"
title = "CLI Form"
[form.validation]
show_errors_inline = true
validate_on_change = true
[output]
format = "json"
pretty_print = true
[terminal]
# Use raw mode for better terminal control
use_raw_mode = true
# Enable mouse support if available
enable_mouse = false
# Use color output
use_color = true
[appearance]
# Theme: default, monochrome, dark
theme = "default"
# Show field help text
show_help = true
# Show field placeholders
show_placeholders = true

18
config/cli/dev.ncl Normal file
View File

@ -0,0 +1,18 @@
(import "./config.ncl")
& {
form = {
title = "CLI Form (Dev)",
description = "Development CLI form with debugging enabled",
validation.strict_validation = true,
},
output = {
debug_output = true,
},
terminal.enable_mouse = true,
appearance.show_field_types = true,
debug = {
enabled = true,
log_level = "info",
trace_execution = false,
},
}

View File

@ -1,32 +0,0 @@
# CLI Backend - Development Configuration
# Extended configuration for development and testing
[form]
description = "Development CLI form with debugging enabled"
title = "CLI Form (Dev)"
[form.validation]
show_errors_inline = true
strict_validation = true
validate_on_change = true
[output]
debug_output = true
format = "json"
pretty_print = true
[terminal]
enable_mouse = true
use_color = true
use_raw_mode = true
[appearance]
show_field_types = true
show_help = true
show_placeholders = true
theme = "default"
[debug]
enabled = true
log_level = "info"
trace_execution = false

25
config/cli/production.ncl Normal file
View File

@ -0,0 +1,25 @@
(import "./config.ncl")
& {
form = {
title = "Form",
description = "",
validation.strict_validation = true,
},
output = {
pretty_print = false,
debug_output = false,
},
terminal.enable_mouse = false,
appearance = {
show_help = false,
show_placeholders = false,
},
logging = {
file = "/var/log/typedialog/cli.log",
level = "error",
},
timeout = {
max_duration = 3600,
input_timeout = 300,
},
}

View File

@ -1,37 +0,0 @@
# CLI Backend - Production Configuration
# Optimized for production deployment
[form]
description = ""
title = "Form"
[form.validation]
show_errors_inline = true
strict_validation = true
validate_on_change = true
[output]
format = "json"
pretty_print = false
# Suppress debugging info
debug_output = false
[terminal]
enable_mouse = false
use_color = true
use_raw_mode = true
[appearance]
show_help = false
show_placeholders = false
theme = "default"
[logging]
file = "/var/log/typedialog/cli.log"
level = "error"
[timeout]
# Maximum form completion time (seconds)
max_duration = 3600
# Field input timeout
input_timeout = 300

View File

@ -0,0 +1,32 @@
{
provisioning = {
output_dir | default = "./provisioning",
default_providers | default = ["aws", "hetzner"],
generation = {
dry_run | default = false,
overwrite | default = false,
verbose | default = false,
},
templates = {
base_path | default = "crates/typedialog-prov-gen/templates",
},
infrastructure = {
environment | default = "development",
region | default = "us-east-1",
},
nickel = {
generate_defaults | default = true,
use_constraints | default = true,
validate_schemas | default = true,
},
ai = {
enabled | default = false,
provider | default = "claude",
model | default = "claude-3-5-sonnet-20241022",
},
logging = {
level | default = "info",
file | default = false,
},
},
}

View File

@ -1,42 +0,0 @@
# TypeDialog Provisioning Generator - Default Configuration
[provisioning]
# Default output directory
output_dir = "./provisioning"
# Default providers to include
default_providers = ["aws", "hetzner"]
# Generation settings
[provisioning.generation]
dry_run = false
overwrite = false
verbose = false
# Template settings
[provisioning.templates]
# Use local path in development; installed binaries use ~/.config/typedialog/prov-gen/templates
base_path = "crates/typedialog-prov-gen/templates"
# custom_path = "path/to/custom/templates" # Uncomment to override
# Infrastructure defaults
[provisioning.infrastructure]
environment = "development"
region = "us-east-1"
# Nickel integration
[provisioning.nickel]
generate_defaults = true
use_constraints = true
validate_schemas = true
# AI assistance
[provisioning.ai]
enabled = false
model = "claude-3-5-sonnet-20241022"
provider = "claude"
# Logging
[provisioning.logging]
file = false
level = "info"

27
config/prov-gen/dev.ncl Normal file
View File

@ -0,0 +1,27 @@
(import "./config.ncl")
& {
provisioning = {
default_providers = ["hetzner", "lxd"],
generation = {
overwrite = true,
verbose = true,
},
templates = {
base_path = "templates",
custom_path = "./custom-templates",
},
infrastructure = {
environment = "development",
region = "eu-central-1",
},
ai = {
enabled = true,
provider = "ollama",
model = "llama2",
},
logging = {
level = "debug",
file = true,
},
},
}

View File

@ -1,32 +0,0 @@
# TypeDialog Provisioning Generator - Development Configuration
[provisioning]
default_providers = ["hetzner", "lxd"] # Cheaper for dev
output_dir = "./provisioning"
[provisioning.generation]
dry_run = false
overwrite = true # Allow overwrite in dev
verbose = true # Verbose in dev
[provisioning.templates]
base_path = "templates"
custom_path = "./custom-templates"
[provisioning.infrastructure]
environment = "development"
region = "eu-central-1"
[provisioning.nickel]
generate_defaults = true
use_constraints = true
validate_schemas = true
[provisioning.ai]
enabled = true # Enable AI in dev
model = "llama2"
provider = "ollama" # Use local for dev
[provisioning.logging]
file = true
level = "debug"

View File

@ -0,0 +1,33 @@
(import "./config.ncl")
& {
provisioning = {
default_providers = ["aws", "gcp"],
generation = {
dry_run = false,
overwrite = false,
verbose = false,
},
templates.base_path = "templates",
infrastructure = {
environment = "production",
region = "us-east-1",
},
ai = {
enabled = true,
provider = "claude",
model = "claude-3-5-sonnet-20241022",
},
logging = {
level = "warn",
file = true,
},
validation = {
strict = true,
require_tests = true,
},
security = {
require_encryption = true,
scan_templates = true,
},
},
}

View File

@ -1,41 +0,0 @@
# TypeDialog Provisioning Generator - Production Configuration
[provisioning]
default_providers = ["aws", "gcp"]
output_dir = "./provisioning"
[provisioning.generation]
dry_run = false
overwrite = false # Require explicit --force
verbose = false
[provisioning.templates]
base_path = "templates"
# custom_path = "" # Optional: set custom templates path
[provisioning.infrastructure]
environment = "production"
region = "us-east-1"
[provisioning.nickel]
generate_defaults = true
use_constraints = true
validate_schemas = true
[provisioning.ai]
enabled = true
model = "claude-3-5-sonnet-20241022"
provider = "claude"
[provisioning.logging]
file = true
level = "warn"
# Production-specific settings
[provisioning.validation]
require_tests = true
strict = true
[provisioning.security]
require_encryption = true
scan_templates = true

39
config/tui/config.ncl Normal file
View File

@ -0,0 +1,39 @@
{
form = {
title | default = "TUI Form",
description | default = "Interactive terminal user interface form",
validation = {
show_errors_inline | default = true,
validate_on_change | default = true,
strict_validation | default = false,
},
},
output = {
format | default = "json",
pretty_print | default = true,
debug_output | default = false,
},
terminal = {
use_raw_mode | default = true,
enable_mouse | default = true,
enable_scrolling | default = true,
height | default = -1,
width | default = -1,
},
ui = {
show_borders | default = true,
show_focus | default = true,
highlight_on_hover | default = true,
enable_animations | default = true,
show_field_indices | default = false,
},
appearance = {
theme | default = "default",
border_style | default = "rounded",
color_scheme | default = "default",
},
keyboard = {
vi_mode | default = false,
emacs_mode | default = false,
},
}

View File

@ -1,46 +0,0 @@
# TUI Backend - Default Configuration
# Terminal User Interface rendering
[form]
description = "Interactive terminal user interface form"
title = "TUI Form"
[form.validation]
show_errors_inline = true
validate_on_change = true
[output]
format = "json"
pretty_print = true
[terminal]
# Full TUI features
enable_mouse = true
enable_scrolling = true
use_raw_mode = true
# Fixed height (-1 = auto)
height = -1
# Fixed width (-1 = auto)
width = -1
[ui]
# Show field borders
show_borders = true
# Show field focus indicator
show_focus = true
# Highlight on hover
highlight_on_hover = true
# Animation enabled
enable_animations = true
[appearance]
border_style = "rounded"
theme = "default"
# Color scheme: default, dark, light, high_contrast
color_scheme = "default"
[keyboard]
# Vi-style navigation (hjkl)
vi_mode = false
# Emacs-style navigation
emacs_mode = false

16
config/tui/dev.ncl Normal file
View File

@ -0,0 +1,16 @@
(import "./config.ncl")
& {
form = {
title = "TUI Form (Dev)",
description = "Development TUI form with all features enabled",
validation.strict_validation = true,
},
output.debug_output = true,
ui.show_field_indices = true,
appearance.border_style = "double",
debug = {
enabled = true,
log_level = "debug",
trace_rendering = false,
},
}

View File

@ -1,45 +0,0 @@
# TUI Backend - Development Configuration
# Extended TUI features for development
[form]
description = "Development TUI form with all features enabled"
title = "TUI Form (Dev)"
[form.validation]
show_errors_inline = true
strict_validation = true
validate_on_change = true
[output]
debug_output = true
format = "json"
pretty_print = true
[terminal]
enable_mouse = true
enable_scrolling = true
height = -1
use_raw_mode = true
width = -1
[ui]
enable_animations = true
highlight_on_hover = true
show_borders = true
show_focus = true
# Show field indices for debugging
show_field_indices = true
[appearance]
border_style = "double"
color_scheme = "default"
theme = "default"
[keyboard]
emacs_mode = false
vi_mode = false
[debug]
enabled = true
log_level = "debug"
trace_rendering = false

21
config/tui/production.ncl Normal file
View File

@ -0,0 +1,21 @@
(import "./config.ncl")
& {
form = {
title = "",
description = "",
validation.strict_validation = true,
},
output = {
pretty_print = false,
debug_output = false,
},
ui.enable_animations = false,
logging = {
file = "/var/log/typedialog/tui.log",
level = "error",
},
performance = {
render_throttle = 16,
max_fps = 60,
},
}

View File

@ -1,48 +0,0 @@
# TUI Backend - Production Configuration
# Optimized TUI for production deployment
[form]
description = ""
title = ""
[form.validation]
show_errors_inline = true
strict_validation = true
validate_on_change = true
[output]
debug_output = false
format = "json"
pretty_print = false
[terminal]
enable_mouse = true
enable_scrolling = true
height = -1
use_raw_mode = true
width = -1
[ui]
enable_animations = false
highlight_on_hover = true
show_borders = true
show_focus = true
[appearance]
border_style = "rounded"
color_scheme = "default"
theme = "default"
[keyboard]
emacs_mode = false
vi_mode = false
[logging]
file = "/var/log/typedialog/tui.log"
level = "error"
[performance]
# Render throttle (milliseconds)
render_throttle = 16
# Max update frequency (Hz)
max_fps = 60

39
config/web/config.ncl Normal file
View File

@ -0,0 +1,39 @@
{
server = {
host | default = "localhost",
port | default = 3000,
cors_enabled | default = true,
cors_origins | default = ["localhost", "127.0.0.1"],
debug | default = false,
hot_reload | default = false,
},
form = {
title | default = "Web Form",
description | default = "Interactive web form",
validation = {
client_validation | default = true,
show_errors_inline | default = true,
validate_on_change | default = true,
},
},
output = {
format | default = "json",
},
html = {
css_framework | default = "none",
inline_styles | default = false,
responsive | default = true,
dark_mode | default = true,
},
submission = {
method | default = "post",
webhook_url | default = "",
redirect_on_success | default = false,
redirect_url | default = "",
},
security = {
csrf_enabled | default = true,
rate_limit | default = 0,
require_https | default = false,
},
}

View File

@ -1,48 +0,0 @@
# Web Backend - Default Configuration
# HTTP server and web form rendering
[server]
host = "localhost"
port = 3000
# CORS settings
cors_enabled = true
cors_origins = ["localhost", "127.0.0.1"]
[form]
description = "Interactive web form"
title = "Web Form"
[form.validation]
client_validation = true
show_errors_inline = true
validate_on_change = true
[output]
format = "json"
[html]
# CSS framework: bootstrap, tailwind, none
css_framework = "none"
# Include inline styles
inline_styles = false
# Mobile responsive
responsive = true
# Dark mode support
dark_mode = true
[submission]
# Submission method: post, put, patch
method = "post"
# Optional webhook URL for submissions
webhook_url = ""
# Redirect after submission
redirect_on_success = false
redirect_url = ""
[security]
# CSRF protection
csrf_enabled = true
# Rate limiting (requests per minute)
rate_limit = 0
# Require HTTPS
require_https = false

28
config/web/dev.ncl Normal file
View File

@ -0,0 +1,28 @@
(import "./config.ncl")
& {
server = {
host = "0.0.0.0",
debug = true,
hot_reload = true,
},
form = {
title = "Web Form (Dev)",
description = "Development web form",
},
html = {
inline_styles = true,
show_field_metadata = true,
},
submission = {
webhook_url = "http://localhost:8000/webhook",
log_submissions = true,
},
logging = {
file = "/tmp/typedialog-web.log",
level = "debug",
},
api = {
enable_docs = true,
docs_path = "/docs",
},
}

View File

@ -1,50 +0,0 @@
# Web Backend - Development Configuration
# Enhanced features for development
[server]
host = "0.0.0.0"
port = 3000
# Enable hot reload
hot_reload = true
# Debug mode
debug = true
[form]
description = "Development web form"
title = "Web Form (Dev)"
[form.validation]
client_validation = true
show_errors_inline = true
validate_on_change = true
[output]
format = "json"
[html]
css_framework = "none"
dark_mode = true
inline_styles = true
responsive = true
# Show field metadata
show_field_metadata = true
[submission]
log_submissions = true
method = "post"
redirect_on_success = false
webhook_url = "http://localhost:8000/webhook"
[security]
csrf_enabled = true
rate_limit = 0
require_https = false
[logging]
file = "/tmp/typedialog-web.log"
level = "debug"
[api]
# API documentation enabled
docs_path = "/docs"
enable_docs = true

42
config/web/production.ncl Normal file
View File

@ -0,0 +1,42 @@
(import "./config.ncl")
& {
server = {
host = "0.0.0.0",
port = 8080,
debug = false,
hot_reload = false,
workers = 4,
},
form = {
title = "",
description = "",
},
html.inline_styles = false,
submission = {
method = "post",
redirect_on_success = true,
redirect_url = "https://example.com/thank-you",
webhook_url = "${TYPEDIALOG_WEBHOOK_URL}",
},
security = {
csrf_enabled = true,
rate_limit = 100,
require_https = true,
add_security_headers = true,
},
logging = {
file = "/var/log/typedialog/web.log",
level = "error",
},
performance = {
cache_static = true,
cache_ttl = 3600,
enable_compression = true,
compression_threshold = 1024,
},
tls = {
enabled = false,
cert_path = "/etc/typedialog/cert.pem",
key_path = "/etc/typedialog/key.pem",
},
}

View File

@ -1,65 +0,0 @@
# Web Backend - Production Configuration
# Hardened configuration for production deployment
[server]
host = "0.0.0.0"
port = 8080
# Disable development features
debug = false
hot_reload = false
# Worker threads
workers = 4
[form]
description = ""
title = ""
[form.validation]
client_validation = true
show_errors_inline = true
validate_on_change = true
[output]
format = "json"
[html]
css_framework = "none"
dark_mode = true
inline_styles = false
responsive = true
[submission]
method = "post"
# Required: webhook for production submissions
redirect_on_success = true
redirect_url = "https://example.com/thank-you"
webhook_url = "https://api.example.com/forms"
[security]
# Strict CSRF protection
csrf_enabled = true
# Rate limiting: requests per minute per IP
rate_limit = 100
# Require HTTPS
require_https = true
# Security headers
add_security_headers = true
[logging]
file = "/var/log/typedialog/web.log"
level = "error"
[performance]
# Cache static assets
cache_static = true
cache_ttl = 3600
# Enable gzip compression
enable_compression = true
# Minimum response size for compression (bytes)
compression_threshold = 1024
[tls]
# Optional TLS configuration
cert_path = "/etc/typedialog/cert.pem"
enabled = false
key_path = "/etc/typedialog/key.pem"

View File

@ -2,6 +2,7 @@
//!
//! Provides a unified pattern for loading backend configuration files
//! with support for both explicit `-c FILE` and environment-based search.
//! Supports both `.ncl` (via `nickel export`) and `.toml` formats.
use crate::error::{Error, Result};
use std::path::Path;
@ -9,10 +10,12 @@ use std::path::Path;
/// Load backend-specific configuration file
///
/// If `cli_config_path` is provided, uses that file exclusively.
/// Otherwise, searches in order:
/// 1. `~/.config/typedialog/{backend_name}/{TYPEDIALOG_ENV}.toml`
/// 2. `~/.config/typedialog/{backend_name}/config.toml`
/// 3. Returns default value
/// Otherwise, searches in order (NCL preferred over TOML):
/// 1. `~/.config/typedialog/{backend_name}/{TYPEDIALOG_ENV}.ncl`
/// 2. `~/.config/typedialog/{backend_name}/{TYPEDIALOG_ENV}.toml`
/// 3. `~/.config/typedialog/{backend_name}/config.ncl`
/// 4. `~/.config/typedialog/{backend_name}/config.toml`
/// 5. Returns default value
///
/// # Arguments
/// * `backend_name` - Name of backend (cli, tui, web, ai)
@ -37,7 +40,7 @@ use std::path::Path;
/// let config = load_backend_config::<CliConfig>("cli", None, CliConfig::default())?;
///
/// // With explicit path
/// let path = PathBuf::from("custom.toml");
/// let path = PathBuf::from("custom.ncl");
/// let config = load_backend_config::<CliConfig>("cli", Some(path.as_path()), CliConfig::default())?;
/// # Ok(())
/// # }
@ -50,12 +53,10 @@ pub fn load_backend_config<T>(
where
T: serde::de::DeserializeOwned + Default,
{
// If CLI path provided, use it exclusively
if let Some(path) = cli_config_path {
return load_from_file(path);
}
// Otherwise try search order
let config_dir = dirs::config_dir()
.unwrap_or_else(|| {
std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
@ -63,29 +64,73 @@ where
.join("typedialog")
.join(backend_name);
// Try environment-specific config first
let env = std::env::var("TYPEDIALOG_ENV").unwrap_or_else(|_| "default".to_string());
let env_config_path = config_dir.join(format!("{}.toml", env));
if env_config_path.exists() {
if let Ok(config) = load_from_file::<T>(&env_config_path) {
return Ok(config);
// NCL env-specific → TOML env-specific → NCL generic → TOML generic
let candidates = [
config_dir.join(format!("{env}.ncl")),
config_dir.join(format!("{env}.toml")),
config_dir.join("config.ncl"),
config_dir.join("config.toml"),
];
for path in &candidates {
if path.exists() {
if let Ok(config) = load_from_file::<T>(path) {
return Ok(config);
}
}
}
// Try generic config.toml
let generic_config_path = config_dir.join("config.toml");
if generic_config_path.exists() {
if let Ok(config) = load_from_file::<T>(&generic_config_path) {
return Ok(config);
}
}
// Return default
Ok(default)
}
/// Load configuration from TOML file
/// Dispatch to NCL or TOML loader by extension.
fn load_from_file<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
match path.extension().and_then(|e| e.to_str()) {
Some("ncl") => load_from_ncl(path),
_ => load_from_toml(path),
}
}
/// Export NCL via `nickel export --format json` and deserialise.
fn load_from_ncl<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let output = std::process::Command::new("nickel")
.args(["export", "--format", "json", &path.to_string_lossy()])
.output()
.map_err(|e| {
Error::validation_failed(format!(
"Failed to run `nickel export` for '{}': {}",
path.display(),
e
))
})?;
if !output.status.success() {
return Err(Error::validation_failed(format!(
"nickel export failed for '{}': {}",
path.display(),
String::from_utf8_lossy(&output.stderr)
)));
}
serde_json::from_slice(&output.stdout).map_err(|e| {
Error::validation_failed(format!(
"Failed to deserialize NCL config '{}': {}",
path.display(),
e
))
})
}
/// Parse TOML file and deserialise.
fn load_from_toml<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
@ -123,30 +168,24 @@ mod tests {
value: 42,
};
let config = load_backend_config::<TestConfig>("test", None, default.clone()).unwrap();
// Should return default when no config found
assert_eq!(config, default);
}
#[test]
fn test_load_with_explicit_path() {
// Create test config file
fn test_load_with_explicit_toml_path() {
let temp_dir = std::env::temp_dir();
let test_config_path = temp_dir.join("test-backend-config.toml");
let test_content = r#"
name = "test"
value = 100
"#;
std::fs::write(&test_config_path, test_content).ok();
std::fs::write(&test_config_path, "name = \"test\"\nvalue = 100\n").ok();
let default = TestConfig::default();
let config =
load_backend_config::<TestConfig>("test", Some(test_config_path.as_path()), default)
.unwrap();
let config = load_backend_config::<TestConfig>(
"test",
Some(test_config_path.as_path()),
TestConfig::default(),
)
.unwrap();
assert_eq!(config.name, "test");
assert_eq!(config.value, 100);
// Cleanup
std::fs::remove_file(test_config_path).ok();
}
}

View File

@ -204,6 +204,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,

View File

@ -3,14 +3,15 @@
//! Handles form execution with various backends and execution modes.
use crate::error::Result;
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use super::conditions::evaluate_condition;
use super::parser::load_elements_from_file;
use super::translation::{translate_display_item, translate_field_definition};
use super::types::{DisplayItem, DisplayMode, FieldDefinition, FormDefinition, FormElement};
use super::types::{
DisplayItem, DisplayMode, FieldDefinition, FormDefinition, FormElement, WhenFalse,
};
#[cfg(feature = "i18n")]
use crate::i18n::I18nBundle;
@ -82,60 +83,31 @@ pub fn render_display_item(item: &DisplayItem, results: &HashMap<String, serde_j
#[cfg(feature = "cli")]
pub fn execute_with_base_dir(
mut form: FormDefinition,
base_dir: &Path,
_base_dir: &Path,
) -> Result<HashMap<String, serde_json::Value>> {
let mut results = HashMap::new();
// Print form header
if let Some(desc) = &form.description {
println!("\n{}\n{}\n", form.name, desc);
} else {
println!("\n{}\n", form.name);
}
// Migrate legacy format to unified elements
form.migrate_to_elements();
// Expand groups with includes using the unified expand_includes function
let expanded_form = super::fragments::expand_includes(form, base_dir)?;
// Build ordered element map
let mut element_map: BTreeMap<usize, FormElement> = BTreeMap::new();
let mut order_counter = 0;
for element in expanded_form.elements {
let order = match &element {
FormElement::Item(item) => {
if item.order == 0 {
order_counter += 1;
order_counter - 1
} else {
item.order
}
}
FormElement::Field(field) => {
if field.order == 0 {
order_counter += 1;
order_counter - 1
} else {
field.order
}
}
};
element_map.insert(order, element);
}
// Process elements in order
for (_, element) in element_map.iter() {
for element in &form.elements {
match element {
FormElement::Item(item) => {
render_display_item(item, &results);
}
FormElement::Field(field) => {
// Check if field should be shown based on conditional
if let Some(condition) = &field.when {
if !evaluate_condition(condition, &results) {
// Field condition not met, skip it
if field.when_false == WhenFalse::Default {
if let Some(default) = &field.default {
results.insert(field.name.clone(), serde_json::json!(default));
}
}
continue;
}
}
@ -161,102 +133,41 @@ pub fn execute(form: FormDefinition) -> Result<HashMap<String, serde_json::Value
pub fn load_and_execute_from_file(
path: impl AsRef<Path>,
) -> Result<HashMap<String, serde_json::Value>> {
use super::parser::{parse_toml, resolve_constraints_in_content};
let path_ref = path.as_ref();
let content = std::fs::read_to_string(path_ref)?;
// Get the directory of the current file for relative path resolution
let base_dir = path_ref.parent().unwrap_or_else(|| Path::new("."));
// Resolve constraint interpolations before parsing
let resolved_content = resolve_constraints_in_content(&content, base_dir)?;
let form = parse_toml(&resolved_content)?;
let form = super::parser::load_from_file(path_ref)?;
execute_with_base_dir(form, base_dir)
}
/// Build element list from form definition with lazy loading of fragments
fn build_element_list(
form: &FormDefinition,
base_dir: &Path,
_results: &HashMap<String, serde_json::Value>,
) -> Result<Vec<(usize, FormElement)>> {
let mut element_list: Vec<(usize, FormElement)> = Vec::new();
let mut order_counter = 0;
/// Build ordered element list from form definition.
///
/// Elements are returned in declaration order (array index). No file I/O: group expansion
/// via Nickel imports or `expand_includes` must happen before form execution.
fn build_element_list(form: &FormDefinition) -> Vec<(usize, FormElement)> {
form.elements.iter().cloned().enumerate().collect()
}
// Process unified elements (expand groups and maintain insertion order)
for element in form.elements.iter() {
match element {
FormElement::Item(item) => {
let mut item_clone = item.as_ref().clone();
// Handle group type with includes
if item.item_type == "group" {
let group_condition = item.when.clone();
if let Some(includes) = &item.includes {
for include_path in includes {
// Load elements from fragment (unified format)
// Note: We load ALL fragments regardless of condition
// Phase 3 filtering will hide/show based on conditions
match load_elements_from_file(include_path, base_dir) {
Ok(loaded_elements) => {
for mut loaded_element in loaded_elements {
// Apply group condition to loaded elements if they don't have one
if let Some(ref condition) = group_condition {
match &mut loaded_element {
FormElement::Item(ref mut loaded_item) => {
if loaded_item.when.is_none() {
loaded_item.when = Some(condition.clone());
}
}
FormElement::Field(ref mut loaded_field) => {
if loaded_field.when.is_none() {
loaded_field.when = Some(condition.clone());
}
}
}
}
// Assign order based on position counter (insertion order)
match &mut loaded_element {
FormElement::Item(ref mut loaded_item) => {
loaded_item.order = order_counter;
}
FormElement::Field(ref mut loaded_field) => {
loaded_field.order = order_counter;
}
}
order_counter += 1;
element_list.push((order_counter - 1, loaded_element));
}
}
Err(_) => {
// Fragment failed to load, skip silently
}
}
}
/// Inject default values into results for fields whose `when` condition is false
/// and `when_false = default`. Called after complete-form execution where the backend
/// never sees the hidden fields.
fn apply_when_false_defaults(
element_list: &[(usize, FormElement)],
results: &mut HashMap<String, serde_json::Value>,
) {
for (_, element) in element_list {
if let FormElement::Field(field) = element {
if let Some(condition) = &field.when {
if !evaluate_condition(condition, results) && field.when_false == WhenFalse::Default
{
if let Some(default) = &field.default {
results
.entry(field.name.clone())
.or_insert_with(|| serde_json::json!(default));
}
} else {
// Non-group items get order from position counter (insertion order)
item_clone.order = order_counter;
order_counter += 1;
element_list.push((item_clone.order, FormElement::Item(Box::new(item_clone))));
}
}
FormElement::Field(field) => {
let mut field_clone = field.as_ref().clone();
// Assign order based on position counter (insertion order)
field_clone.order = order_counter;
order_counter += 1;
element_list.push((field_clone.order, FormElement::Field(Box::new(field_clone))));
}
}
}
// No need to sort - elements are already in insertion order from the counter
// element_list is already sorted by construction
Ok(element_list)
}
/// Recompute visible elements based on current results
@ -277,15 +188,14 @@ fn build_element_list(
/// based on current conditions and lazy loading rules
pub fn recompute_visible_elements(
form: &FormDefinition,
base_dir: &Path,
_base_dir: &Path,
results: &HashMap<String, serde_json::Value>,
) -> Result<(Vec<DisplayItem>, Vec<FieldDefinition>)> {
// Clone and migrate form to ensure elements are populated
let mut form_clone = form.clone();
form_clone.migrate_to_elements();
// Build complete element list with lazy loading
let element_list = build_element_list(&form_clone, base_dir, results)?;
let element_list = build_element_list(&form_clone);
// Separate and filter items and fields based on conditions
let mut visible_items = Vec::new();
@ -375,10 +285,9 @@ pub async fn execute_with_backend_complete(
}
}
// PHASE 2: Build element list with lazy loading based on Phase 1 results
let element_list = build_element_list(&form, base_dir, &results)?;
let element_list = build_element_list(&form);
// PHASE 3: Execute remaining fields based on display mode
// Execute remaining fields based on display mode
if form.display_mode == DisplayMode::Complete {
// Complete mode: pass all fields to backend for complete form display
let items: Vec<&DisplayItem> = element_list
@ -442,6 +351,12 @@ pub async fn execute_with_backend_complete(
if let Some(condition) = &field.when {
if !evaluate_condition(condition, &results) {
if field.when_false == WhenFalse::Default {
if let Some(default) = &field.default {
Arc::make_mut(&mut results)
.insert(field.name.clone(), serde_json::json!(default));
}
}
continue;
}
}
@ -479,7 +394,7 @@ pub async fn execute_with_backend_two_phase_with_defaults(
mut form: FormDefinition,
backend: &mut dyn crate::backends::FormBackend,
i18n_bundle: Option<&I18nBundle>,
base_dir: &Path,
_base_dir: &Path,
initial_values: Option<HashMap<String, serde_json::Value>>,
) -> Result<HashMap<String, serde_json::Value>> {
use crate::backends::RenderContext;
@ -521,8 +436,7 @@ pub async fn execute_with_backend_two_phase_with_defaults(
}
}
// PHASE 2: Build element list with lazy loading based on Phase 1 results
let mut element_list = build_element_list(&form, base_dir, &results)?;
let mut element_list = build_element_list(&form);
// Apply initial_values to field.default for all expanded elements
// This ensures defaults from nickel-roundtrip input files are shown in the UI
@ -563,6 +477,12 @@ pub async fn execute_with_backend_two_phase_with_defaults(
if let Some(condition) = &field.when {
if !evaluate_condition(condition, &results) {
if field.when_false == WhenFalse::Default {
if let Some(default) = &field.default {
Arc::make_mut(&mut results)
.insert(field.name.clone(), serde_json::json!(default));
}
}
continue;
}
}
@ -637,8 +557,7 @@ pub async fn execute_with_backend_i18n_with_defaults(
// Initialize backend
backend.initialize().await?;
// Build element list directly (no two-phase)
let element_list = build_element_list(&form, base_dir, &results)?;
let element_list = build_element_list(&form);
// Check display mode and execute accordingly
if form.display_mode == DisplayMode::Complete {
@ -686,9 +605,10 @@ pub async fn execute_with_backend_i18n_with_defaults(
.map(|f| translate_field_definition(f, i18n_bundle))
.collect();
let complete_results = backend
let mut complete_results = backend
.execute_form_complete(&form, base_dir, items_owned, fields_owned, initial_backup)
.await?;
apply_when_false_defaults(&element_list, &mut complete_results);
results = Arc::new(complete_results);
} else {
// Field-by-field mode
@ -707,6 +627,12 @@ pub async fn execute_with_backend_i18n_with_defaults(
FormElement::Field(field) => {
if let Some(condition) = &field.when {
if !evaluate_condition(condition, &results) {
if field.when_false == WhenFalse::Default {
if let Some(default) = &field.default {
Arc::make_mut(&mut results)
.insert(field.name.clone(), serde_json::json!(default));
}
}
continue;
}
}

View File

@ -19,11 +19,12 @@ mod types;
// Re-export public types
pub use types::{
DisplayItem, DisplayMode, FieldDefinition, FieldType, FormDefinition, FormElement, SelectOption,
DisplayItem, DisplayMode, FieldDefinition, FieldType, FormDefinition, FormElement,
SelectOption, WhenFalse,
};
// Re-export public functions - parser
pub use parser::{load_from_file, parse_toml};
pub use parser::{load_form, load_from_file, load_from_ncl, parse_toml};
// Re-export public functions - executor (async backend paths, always available)
pub use executor::{

View File

@ -142,6 +142,34 @@ pub fn parse_toml(content: &str) -> Result<FormDefinition> {
toml::from_str(content).map_err(|e| e.into())
}
/// Load form from a Nickel (.ncl) file via `nickel export --format json`
///
/// Invokes the `nickel` CLI as a subprocess. If the file contains contract violations
/// or syntax errors, `nickel export` fails and this function returns an error, aborting
/// the caller before any form execution begins.
pub fn load_from_ncl(path: impl AsRef<Path>) -> Result<FormDefinition> {
use crate::nickel::NickelCli;
let json = NickelCli::export(path.as_ref())?;
serde_json::from_value(json).map_err(|e| {
crate::error::ErrorWrapper::new(format!(
"Failed to deserialize Nickel export as FormDefinition: {}",
e
))
})
}
/// Load a form from either a `.ncl` or `.toml` file, detected by extension
///
/// `.ncl` files are loaded via `nickel export` (schema-validated at load time).
/// All other extensions fall back to TOML parsing.
pub fn load_form(path: impl AsRef<Path>) -> Result<FormDefinition> {
let path = path.as_ref();
match path.extension().and_then(|e| e.to_str()) {
Some("ncl") => load_from_ncl(path),
_ => load_from_file(path),
}
}
/// Load form from TOML file (returns FormDefinition, doesn't execute)
pub fn load_from_file(path: impl AsRef<Path>) -> Result<FormDefinition> {
let path_ref = path.as_ref();

View File

@ -476,6 +476,11 @@ pub struct FieldDefinition {
pub order: usize,
/// Optional conditional display (e.g., "role == admin", "country != US")
pub when: Option<String>,
/// Output behavior when `when` condition is false
/// - `exclude` (default): field absent from results
/// - `default`: field injected with its default value into results
#[serde(default)]
pub when_false: WhenFalse,
/// Optional flag indicating if prompt/placeholder/options are i18n keys
pub i18n: Option<bool>,
/// Optional semantic grouping for form organization
@ -565,6 +570,17 @@ pub enum FieldType {
RepeatingGroup,
}
/// Output behavior for a field when its `when` condition evaluates to false
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WhenFalse {
/// Field is excluded from results entirely (default)
#[default]
Exclude,
/// Field's `default` value is injected into results without prompting
Default,
}
/// Form display mode - how fields are presented to user
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]

View File

@ -444,6 +444,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,
@ -499,6 +500,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,
@ -554,6 +556,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,
@ -610,6 +613,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,
@ -649,6 +653,7 @@ mod tests {
week_start: None,
order: 1,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,
@ -700,6 +705,7 @@ mod tests {
week_start: None,
order: 0,
when: None,
when_false: Default::default(),
i18n: None,
group: None,
nickel_contract: None,

View File

@ -1,327 +1,8 @@
//! Contract Validator and Analyzer
//! Contract Analyzer
//!
//! Validates Nickel contracts and predicates against JSON values.
//! Analyzes contracts to infer conditional expressions for form generation.
//!
//! Supported validation predicates:
//! - `std.string.NonEmpty` - Non-empty string
//! - `std.string.length.min N` - Minimum string length
//! - `std.string.length.max N` - Maximum string length
//! - `std.string.Email` - Valid email address format
//! - `std.string.Url` - Valid URL format (http/https/ftp/ftps)
//! - `std.number.between A B` - Number in range [A, B]
//! - `std.number.greater_than N` - Number > N
//! - `std.number.less_than N` - Number < N
//! Analyzes Nickel contracts to infer conditional expressions for form generation.
use super::schema_ir::{NickelFieldIR, NickelSchemaIR, NickelType};
use crate::error::ErrorWrapper;
use crate::Result;
use serde_json::Value;
/// Validator for Nickel contracts and predicates
pub struct ContractValidator;
impl ContractValidator {
/// Validate a value against a Nickel contract
///
/// # Arguments
///
/// * `value` - The JSON value to validate
/// * `contract` - The Nickel contract string (e.g., "String | std.string.NonEmpty")
///
/// # Returns
///
/// Ok if validation succeeds, Err with descriptive message if it fails
pub fn validate(value: &Value, contract: &str) -> Result<()> {
// Extract the predicate from the contract (after the pipe)
let predicate = contract
.rfind('|')
.map(|i| contract[i + 1..].trim())
.unwrap_or(contract);
// Match common predicates
if predicate.contains("std.string.NonEmpty") {
return Self::validate_non_empty_string(value);
}
if predicate.contains("std.string.length.min") {
if let Some(n) = Self::extract_number(predicate, "std.string.length.min") {
return Self::validate_min_length(value, n);
}
}
if predicate.contains("std.string.length.max") {
if let Some(n) = Self::extract_number(predicate, "std.string.length.max") {
return Self::validate_max_length(value, n);
}
}
if predicate.contains("std.number.between") {
if let Some((a, b)) = Self::extract_range(predicate) {
return Self::validate_between(value, a, b);
}
}
if predicate.contains("std.number.greater_than") {
if let Some(n) = Self::extract_number(predicate, "std.number.greater_than") {
return Self::validate_greater_than(value, n as f64);
}
}
if predicate.contains("std.number.less_than") {
if let Some(n) = Self::extract_number(predicate, "std.number.less_than") {
return Self::validate_less_than(value, n as f64);
}
}
if predicate.contains("std.string.Email") {
return Self::validate_email(value);
}
if predicate.contains("std.string.Url") {
return Self::validate_url(value);
}
// Unknown predicate - pass validation
Ok(())
}
/// Validate that string is non-empty
fn validate_non_empty_string(value: &Value) -> Result<()> {
match value {
Value::String(s) => {
if s.is_empty() {
Err(ErrorWrapper::new(
"String must not be empty (std.string.NonEmpty)".to_string(),
))
} else {
Ok(())
}
}
_ => Err(ErrorWrapper::new("Expected string value".to_string())),
}
}
/// Validate minimum string length
fn validate_min_length(value: &Value, min: usize) -> Result<()> {
match value {
Value::String(s) => {
if s.len() < min {
Err(ErrorWrapper::new(format!(
"String must be at least {} characters (std.string.length.min {})",
min, min
)))
} else {
Ok(())
}
}
_ => Err(ErrorWrapper::new("Expected string value".to_string())),
}
}
/// Validate maximum string length
fn validate_max_length(value: &Value, max: usize) -> Result<()> {
match value {
Value::String(s) => {
if s.len() > max {
Err(ErrorWrapper::new(format!(
"String must be at most {} characters (std.string.length.max {})",
max, max
)))
} else {
Ok(())
}
}
_ => Err(ErrorWrapper::new("Expected string value".to_string())),
}
}
/// Validate number is in range [a, b]
fn validate_between(value: &Value, a: f64, b: f64) -> Result<()> {
match value {
Value::Number(n) => {
if let Some(num) = n.as_f64() {
if num >= a && num <= b {
Ok(())
} else {
Err(ErrorWrapper::new(format!(
"Number must be between {} and {} (std.number.between {} {})",
a, b, a, b
)))
}
} else {
Err(ErrorWrapper::new("Invalid number value".to_string()))
}
}
_ => Err(ErrorWrapper::new("Expected number value".to_string())),
}
}
/// Validate number is greater than n
fn validate_greater_than(value: &Value, n: f64) -> Result<()> {
match value {
Value::Number(num) => {
if let Some(val) = num.as_f64() {
if val > n {
Ok(())
} else {
Err(ErrorWrapper::new(format!(
"Number must be greater than {} (std.number.greater_than {})",
n, n
)))
}
} else {
Err(ErrorWrapper::new("Invalid number value".to_string()))
}
}
_ => Err(ErrorWrapper::new("Expected number value".to_string())),
}
}
/// Validate number is less than n
fn validate_less_than(value: &Value, n: f64) -> Result<()> {
match value {
Value::Number(num) => {
if let Some(val) = num.as_f64() {
if val < n {
Ok(())
} else {
Err(ErrorWrapper::new(format!(
"Number must be less than {} (std.number.less_than {})",
n, n
)))
}
} else {
Err(ErrorWrapper::new("Invalid number value".to_string()))
}
}
_ => Err(ErrorWrapper::new("Expected number value".to_string())),
}
}
/// Extract a single number from predicate string
fn extract_number(predicate: &str, pattern: &str) -> Option<usize> {
let start = predicate.find(pattern)? + pattern.len();
let rest = &predicate[start..];
// Extract digits after the pattern
rest.split_whitespace()
.next()
.and_then(|s| s.trim_matches(|c: char| !c.is_ascii_digit()).parse().ok())
}
/// Extract a range (a, b) from between predicate
fn extract_range(predicate: &str) -> Option<(f64, f64)> {
// Parse patterns like "std.number.between 0 100" or "std.number.between 0.5 99.9"
let start = predicate.find("std.number.between")? + "std.number.between".len();
let rest = predicate[start..].trim();
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() < 2 {
return None;
}
let a = parts[0].parse::<f64>().ok()?;
let b = parts[1].parse::<f64>().ok()?;
Some((a, b))
}
/// Validate email address format
///
/// Uses a simple regex pattern to check basic email format.
/// Pattern: local@domain where local can contain alphanumeric, dots, hyphens, underscores
/// and domain must have at least one dot.
pub fn validate_email(value: &Value) -> Result<()> {
match value {
Value::String(s) => {
if s.is_empty() {
return Err(ErrorWrapper::new(
"Email address cannot be empty".to_string(),
));
}
let at_count = s.matches('@').count();
if at_count != 1 {
return Err(ErrorWrapper::new(
"Email must contain exactly one @ symbol".to_string(),
));
}
let parts: Vec<&str> = s.split('@').collect();
let local = parts[0];
let domain = parts[1];
if local.is_empty() {
return Err(ErrorWrapper::new(
"Email local part cannot be empty".to_string(),
));
}
if domain.is_empty() {
return Err(ErrorWrapper::new(
"Email domain cannot be empty".to_string(),
));
}
if !domain.contains('.') {
return Err(ErrorWrapper::new(
"Email domain must contain at least one dot".to_string(),
));
}
if domain.starts_with('.') || domain.ends_with('.') {
return Err(ErrorWrapper::new(
"Email domain cannot start or end with a dot".to_string(),
));
}
Ok(())
}
_ => Err(ErrorWrapper::new(
"Expected string value for email".to_string(),
)),
}
}
/// Validate URL format
///
/// Checks for basic URL structure: scheme://host with optional path.
/// Accepted schemes: http, https, ftp, ftps.
pub fn validate_url(value: &Value) -> Result<()> {
match value {
Value::String(s) => {
if s.is_empty() {
return Err(ErrorWrapper::new("URL cannot be empty".to_string()));
}
let valid_schemes = ["http://", "https://", "ftp://", "ftps://"];
let has_valid_scheme = valid_schemes.iter().any(|scheme| s.starts_with(scheme));
if !has_valid_scheme {
return Err(ErrorWrapper::new(format!(
"URL must start with one of: {}",
valid_schemes.join(", ")
)));
}
let scheme_end = s.find("://").unwrap() + 3;
let rest = &s[scheme_end..];
if rest.is_empty() {
return Err(ErrorWrapper::new(
"URL must contain a host after the scheme".to_string(),
));
}
Ok(())
}
_ => Err(ErrorWrapper::new(
"Expected string value for URL".to_string(),
)),
}
}
}
/// Analyzer for inferring conditional expressions from Nickel contracts
pub struct ContractAnalyzer;
@ -332,26 +13,18 @@ impl ContractAnalyzer {
/// Returns a `when` expression (e.g., "tls_enabled == true") if the field appears to be
/// conditionally dependent on another field's value, typically a boolean enable flag.
pub fn infer_condition(field: &NickelFieldIR, schema: &NickelSchemaIR) -> Option<String> {
// Only optional fields might have conditionals
if !field.optional {
return None;
}
// Try to find a boolean field that enables this optional field
Self::find_boolean_dependency(field, schema)
}
/// Find a boolean field that enables this optional field based on naming convention
fn find_boolean_dependency(field: &NickelFieldIR, schema: &NickelSchemaIR) -> Option<String> {
// Extract prefix: "tls_cert_path" -> "tls"
let field_prefix = Self::extract_prefix(&field.flat_name)?;
// Look for a boolean field like "tls_enabled"
for candidate in &schema.fields {
if candidate.nickel_type == NickelType::Bool {
let candidate_prefix = Self::extract_prefix(&candidate.flat_name)?;
// Match pattern: same prefix + "_enabled" suffix
if candidate_prefix == field_prefix && candidate.flat_name.ends_with("_enabled") {
return Some(format!("{} == true", candidate.flat_name));
}
@ -361,9 +34,6 @@ impl ContractAnalyzer {
None
}
/// Extract the common prefix from a field name
/// "tls_cert_path" -> "tls"
/// "database_url" -> "database"
fn extract_prefix(flat_name: &str) -> Option<String> {
flat_name
.split('_')
@ -372,81 +42,3 @@ impl ContractAnalyzer {
.map(|s| s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_validate_non_empty_string() {
let result = ContractValidator::validate(&json!("hello"), "String | std.string.NonEmpty");
assert!(result.is_ok());
let result = ContractValidator::validate(&json!(""), "String | std.string.NonEmpty");
assert!(result.is_err());
}
#[test]
fn test_validate_min_length() {
let result =
ContractValidator::validate(&json!("hello"), "String | std.string.length.min 3");
assert!(result.is_ok());
let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.min 3");
assert!(result.is_err());
}
#[test]
fn test_validate_max_length() {
let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.max 3");
assert!(result.is_ok());
let result =
ContractValidator::validate(&json!("hello"), "String | std.string.length.max 3");
assert!(result.is_err());
}
#[test]
fn test_validate_between() {
let result = ContractValidator::validate(&json!(50), "Number | std.number.between 0 100");
assert!(result.is_ok());
let result = ContractValidator::validate(&json!(150), "Number | std.number.between 0 100");
assert!(result.is_err());
}
#[test]
fn test_validate_greater_than() {
let result = ContractValidator::validate(&json!(50), "Number | std.number.greater_than 10");
assert!(result.is_ok());
let result = ContractValidator::validate(&json!(5), "Number | std.number.greater_than 10");
assert!(result.is_err());
}
#[test]
fn test_validate_less_than() {
let result = ContractValidator::validate(&json!(5), "Number | std.number.less_than 10");
assert!(result.is_ok());
let result = ContractValidator::validate(&json!(50), "Number | std.number.less_than 10");
assert!(result.is_err());
}
#[test]
fn test_extract_range() {
let range = ContractValidator::extract_range("std.number.between 0 100");
assert_eq!(range, Some((0.0, 100.0)));
let range = ContractValidator::extract_range("std.number.between 0.5 99.9");
assert_eq!(range, Some((0.5, 99.9)));
}
#[test]
fn test_unknown_predicate_passes() {
let result =
ContractValidator::validate(&json!("anything"), "String | some.unknown.predicate");
assert!(result.is_ok());
}
}

View File

@ -48,7 +48,6 @@ pub mod types;
pub use alias_generator::AliasGenerator;
pub use cli::NickelCli;
pub use contract_parser::{ContractParser, ParsedContracts};
pub use contracts::ContractValidator;
pub use defaults_extractor::DefaultsExtractor;
pub use encryption_contract_parser::EncryptionContractParser;
pub use field_mapper::FieldMapper;

View File

@ -361,23 +361,13 @@ impl RoundtripConfig {
backend: &mut dyn FormBackend,
initial_values: HashMap<String, Value>,
) -> Result<HashMap<String, Value>> {
// Read form definition
let form_content = fs::read_to_string(form_path).map_err(|e| {
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
})?;
// Parse TOML form definition
let mut form = form_parser::parse_toml(&form_content)?;
// Read and parse form definition (.ncl or .toml by extension)
let mut form = form_parser::load_form(form_path)?;
// Migrate to unified elements format if needed
form.migrate_to_elements();
// NOTE: We don't apply defaults here because execute_with_backend_two_phase_with_defaults
// will call build_element_list which reloads fragments from disk, losing any modifications.
// Instead, we pass initial_values to execute_with_backend_two_phase_with_defaults
// which will apply them after fragment expansion.
// Extract base directory for resolving relative paths (includes, fragments)
// Extract base directory for resolving relative paths
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// execute_with_backend_i18n_with_defaults dispatches on display_mode,
@ -399,23 +389,17 @@ impl RoundtripConfig {
) -> Result<HashMap<String, Value>> {
use std::collections::BTreeMap;
// Read form definition
let form_content = fs::read_to_string(form_path).map_err(|e| {
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
})?;
// Parse TOML form definition
let mut form = form_parser::parse_toml(&form_content)?;
// Read and parse form definition (.ncl or .toml by extension)
let mut form = form_parser::load_form(form_path)?;
// Migrate to unified elements format
form.migrate_to_elements();
// Extract base directory for resolving relative paths (includes, fragments)
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// Extract base directory for resolving relative paths
let _base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// CRITICAL FIX: Expand fragments BEFORE applying defaults
// This ensures defaults are applied to real fields, not to groups with includes
let mut expanded_form = form_parser::expand_includes(form, base_dir)?;
// let mut expanded_form = form_parser::expand_includes(form, _base_dir)?; // rescued: fragment lazy-loading pre-ADR-001
let mut expanded_form = form;
// Now apply initial values as defaults to the EXPANDED fields
for element in &mut expanded_form.elements {
@ -512,20 +496,15 @@ impl RoundtripConfig {
}
};
// Load form to get field definitions with nickel_path
let form_content = fs::read_to_string(form_path).map_err(|e| {
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
})?;
let mut form = form_parser::parse_toml(&form_content)?;
// Load form to get field definitions with nickel_path (.ncl or .toml by extension)
let mut form = form_parser::load_form(form_path)?;
form.migrate_to_elements();
// Extract base directory for resolving fragment includes
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// Extract base directory for resolving paths
let _base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
// Expand fragments to get ALL fields (including those in conditional groups)
// Uses expand_includes to process group elements with includes
let expanded_form = form_parser::expand_includes(form, base_dir)?;
// let expanded_form = form_parser::expand_includes(form, _base_dir)?; // rescued: fragment lazy-loading pre-ADR-001
let expanded_form = form;
// Extract field definitions that have nickel_path
let fields_with_paths: Vec<_> = expanded_form

View File

@ -332,6 +332,7 @@ impl TomlGenerator {
week_start: None,
order,
when: when_condition,
when_false: Default::default(),
i18n: None,
group: field.group.clone(),
nickel_contract: field.contract.clone(),
@ -491,6 +492,7 @@ impl TomlGenerator {
week_start: None,
order,
when: None,
when_false: Default::default(),
i18n: None,
group: field.group.clone(),
nickel_contract: field.contract.clone(),

View File

@ -55,6 +55,7 @@ mod encryption_tests {
sensitive: Some(sensitive),
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
}
}
@ -160,6 +161,7 @@ mod encryption_tests {
sensitive: None, // Not explicitly set
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
};
assert!(
@ -218,6 +220,7 @@ mod encryption_tests {
sensitive: Some(false), // Explicitly NOT sensitive
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
};
assert!(
@ -275,6 +278,7 @@ mod encryption_tests {
sensitive: Some(true),
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
};
let mut backend_config = HashMap::new();
@ -370,6 +374,7 @@ mod age_roundtrip_tests {
sensitive: Some(true),
encryption_backend: Some("age".to_string()),
encryption_config: None,
when_false: Default::default(),
}
}

View File

@ -10,8 +10,8 @@ use serde_json::json;
use std::collections::HashMap;
use typedialog_core::form_parser;
use typedialog_core::nickel::{
ContractValidator, MetadataParser, NickelFieldIR, NickelSchemaIR, NickelSerializer, NickelType,
TemplateEngine, TomlGenerator,
MetadataParser, NickelFieldIR, NickelSchemaIR, NickelSerializer, NickelType, TemplateEngine,
TomlGenerator,
};
#[test]
@ -211,48 +211,6 @@ fn test_array_field_serialization() {
assert!(nickel_output.contains("]"));
}
#[test]
fn test_contract_validation_non_empty_string() {
// Valid non-empty string
let result = ContractValidator::validate(&json!("hello"), "String | std.string.NonEmpty");
assert!(result.is_ok());
// Empty string should fail
let result = ContractValidator::validate(&json!(""), "String | std.string.NonEmpty");
assert!(result.is_err());
}
#[test]
fn test_contract_validation_number_range() {
// Valid number in range
let result = ContractValidator::validate(&json!(50), "Number | std.number.between 0 100");
assert!(result.is_ok());
// Number out of range
let result = ContractValidator::validate(&json!(150), "Number | std.number.between 0 100");
assert!(result.is_err());
}
#[test]
fn test_contract_validation_string_length() {
// Valid length
let result = ContractValidator::validate(&json!("hello"), "String | std.string.length.min 3");
assert!(result.is_ok());
// Too short
let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.min 3");
assert!(result.is_err());
// Valid max length
let result = ContractValidator::validate(&json!("hi"), "String | std.string.length.max 5");
assert!(result.is_ok());
// Too long
let result =
ContractValidator::validate(&json!("hello world"), "String | std.string.length.max 5");
assert!(result.is_err());
}
#[test]
fn test_form_definition_from_schema_ir() {
// Create schema
@ -672,13 +630,7 @@ fn test_full_workflow_integration() {
results.insert("server_host".to_string(), json!("0.0.0.0"));
results.insert("server_port".to_string(), json!(3000));
// Step 4: Validate contracts
assert!(ContractValidator::validate(&json!("MyApp"), "String | std.string.NonEmpty").is_ok());
assert!(
ContractValidator::validate(&json!(3000), "Number | std.number.between 1 65535").is_ok()
);
// Step 5: Serialize to Nickel
// Step 4: Serialize to Nickel
let nickel_output =
NickelSerializer::serialize(&results, &schema).expect("Serialization failed");
@ -1231,6 +1183,7 @@ fn test_encryption_roundtrip_with_redaction() {
sensitive: Some(false),
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
},
form_parser::FieldDefinition {
name: "password".to_string(),
@ -1270,6 +1223,7 @@ fn test_encryption_roundtrip_with_redaction() {
sensitive: Some(true),
encryption_backend: Some("age".to_string()),
encryption_config: None,
when_false: Default::default(),
},
form_parser::FieldDefinition {
name: "api_key".to_string(),
@ -1309,6 +1263,7 @@ fn test_encryption_roundtrip_with_redaction() {
sensitive: Some(true),
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
},
];
@ -1386,6 +1341,7 @@ fn test_encryption_auto_detection_from_field_type() {
sensitive: None, // Not explicitly set
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
};
assert!(
@ -1451,6 +1407,7 @@ fn test_sensitive_field_explicit_override() {
sensitive: Some(false), // Explicitly override
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
};
assert!(
@ -1522,6 +1479,7 @@ fn test_mixed_sensitive_and_non_sensitive_fields() {
sensitive,
encryption_backend: None,
encryption_config: None,
when_false: Default::default(),
}
};

View File

@ -1,322 +0,0 @@
use proptest::prelude::*;
use serde_json::json;
use typedialog_core::nickel::ContractValidator;
proptest! {
#[test]
fn test_email_validation_never_panics(input in "\\PC*") {
let value = json!(input);
drop(ContractValidator::validate_email(&value));
}
#[test]
fn test_valid_emails_accepted(
local in "[a-zA-Z0-9._-]{1,20}",
domain_parts in prop::collection::vec("[a-zA-Z0-9-]{1,10}", 2..5),
tld in "[a-z]{2,6}"
) {
let domain = format!("{}.{}", domain_parts.join("."), tld);
let email = format!("{}@{}", local, domain);
let value = json!(email);
let result = ContractValidator::validate_email(&value);
prop_assert!(
result.is_ok(),
"Valid email format '{}' was rejected: {:?}",
email,
result.err()
);
}
#[test]
fn test_invalid_emails_rejected_no_at(input in "[a-zA-Z0-9._-]+") {
let value = json!(input);
let result = ContractValidator::validate_email(&value);
prop_assert!(result.is_err(), "Email without @ symbol should be rejected");
}
#[test]
fn test_invalid_emails_rejected_multiple_at(
local in "[a-zA-Z0-9._-]{1,10}",
domain in "[a-zA-Z0-9.-]{1,20}"
) {
let email = format!("{}@{}@extra", local, domain);
let value = json!(email);
let result = ContractValidator::validate_email(&value);
prop_assert!(result.is_err(), "Email with multiple @ symbols should be rejected");
}
#[test]
fn test_invalid_emails_rejected_no_domain_dot(
local in "[a-zA-Z0-9._-]{1,10}",
domain in "[a-zA-Z0-9-]{1,10}"
) {
let email = format!("{}@{}", local, domain);
let value = json!(email);
let result = ContractValidator::validate_email(&value);
prop_assert!(
result.is_err(),
"Email without dot in domain '{}' should be rejected",
email
);
}
#[test]
fn test_url_validation_never_panics(input in "\\PC*") {
let value = json!(input);
drop(ContractValidator::validate_url(&value));
}
#[test]
fn test_valid_urls_accepted(
scheme in prop::sample::select(&["http://", "https://", "ftp://", "ftps://"]),
host in "[a-zA-Z0-9.-]{1,30}",
path in prop::option::of("[a-zA-Z0-9/_-]{0,50}")
) {
let url = if let Some(p) = path {
format!("{}{}/{}", scheme, host, p)
} else {
format!("{}{}", scheme, host)
};
let value = json!(url);
let result = ContractValidator::validate_url(&value);
prop_assert!(
result.is_ok(),
"Valid URL format '{}' was rejected: {:?}",
url,
result.err()
);
}
#[test]
fn test_invalid_urls_rejected_no_scheme(
host in "[a-zA-Z0-9.-]{1,20}",
path in "[a-zA-Z0-9/_-]{0,20}"
) {
let url = format!("{}/{}", host, path);
let value = json!(url);
let result = ContractValidator::validate_url(&value);
prop_assert!(result.is_err(), "URL without scheme should be rejected");
}
#[test]
fn test_invalid_urls_rejected_invalid_scheme(
scheme in "[a-z]{3,8}",
host in "[a-zA-Z0-9.-]{1,20}"
) {
prop_assume!(!["http", "https", "ftp", "ftps"].contains(&scheme.as_str()));
let url = format!("{}://{}", scheme, host);
let value = json!(url);
let result = ContractValidator::validate_url(&value);
prop_assert!(
result.is_err(),
"URL with invalid scheme '{}' should be rejected",
scheme
);
}
#[test]
fn test_invalid_urls_rejected_empty_host(
scheme in prop::sample::select(&["http://", "https://", "ftp://", "ftps://"])
) {
let url = scheme.to_string();
let value = json!(url);
let result = ContractValidator::validate_url(&value);
prop_assert!(result.is_err(), "URL with empty host should be rejected");
}
#[test]
fn test_email_validation_type_safety(value in prop_oneof![
Just(json!(42)),
Just(json!(true)),
Just(json!(null)),
Just(json!([])),
Just(json!({})),
]) {
let result = ContractValidator::validate_email(&value);
prop_assert!(
result.is_err(),
"Email validation should reject non-string types"
);
}
#[test]
fn test_url_validation_type_safety(value in prop_oneof![
Just(json!(42)),
Just(json!(true)),
Just(json!(null)),
Just(json!([])),
Just(json!({})),
]) {
let result = ContractValidator::validate_url(&value);
prop_assert!(
result.is_err(),
"URL validation should reject non-string types"
);
}
#[test]
fn test_empty_string_email_rejected(whitespace in "[ \\t\\n\\r]*") {
let value = json!(whitespace);
let result = ContractValidator::validate_email(&value);
prop_assert!(
result.is_err(),
"Empty or whitespace-only email should be rejected"
);
}
#[test]
fn test_empty_string_url_rejected(whitespace in "[ \\t\\n\\r]*") {
let value = json!(whitespace);
let result = ContractValidator::validate_url(&value);
prop_assert!(
result.is_err(),
"Empty or whitespace-only URL should be rejected"
);
}
#[test]
fn test_email_domain_boundary_dots_rejected(
local in "[a-zA-Z0-9._-]{1,10}",
domain in "[a-zA-Z0-9-]{1,10}",
tld in "[a-z]{2,6}"
) {
let email_start_dot = format!("{}@.{}.{}", local, domain, tld);
let email_end_dot = format!("{}@{}.{}.", local, domain, tld);
let value_start = json!(email_start_dot);
let result_start = ContractValidator::validate_email(&value_start);
prop_assert!(
result_start.is_err(),
"Email with domain starting with dot should be rejected"
);
let value_end = json!(email_end_dot);
let result_end = ContractValidator::validate_email(&value_end);
prop_assert!(
result_end.is_err(),
"Email with domain ending with dot should be rejected"
);
}
#[test]
fn test_contract_validate_integration_email(
local in "[a-zA-Z0-9._-]{1,15}",
domain in "[a-zA-Z0-9.-]{3,20}\\.[a-z]{2,6}"
) {
let email = format!("{}@{}", local, domain);
let value = json!(email);
let result = ContractValidator::validate(&value, "String | std.string.Email");
prop_assert!(
result.is_ok() || result.is_err(),
"Contract validation should never panic for any email input"
);
}
#[test]
fn test_contract_validate_integration_url(
scheme in prop::sample::select(&["http", "https", "ftp", "ftps", "gopher", "file"]),
host in "[a-zA-Z0-9.-]{1,30}"
) {
let url = format!("{}://{}", scheme, host);
let value = json!(url);
let result = ContractValidator::validate(&value, "String | std.string.Url");
prop_assert!(
result.is_ok() || result.is_err(),
"Contract validation should never panic for any URL input"
);
}
}
#[test]
fn test_known_valid_emails() {
let valid_emails = vec![
"user@example.com",
"test.user@example.com",
"user+tag@example.com",
"user_name@example.co.uk",
"a@b.c",
"very.long.email.address@subdomain.example.org",
];
for email in valid_emails {
let value = json!(email);
let result = ContractValidator::validate_email(&value);
assert!(
result.is_ok(),
"Valid email '{}' was rejected: {:?}",
email,
result.err()
);
}
}
#[test]
fn test_known_invalid_emails() {
let invalid_emails = vec![
"",
"@",
"@@",
"user@",
"@example.com",
"user",
"user@@example.com",
"user@example",
"user@.example.com",
"user@example.com.",
];
for email in invalid_emails {
let value = json!(email);
let result = ContractValidator::validate_email(&value);
assert!(result.is_err(), "Invalid email '{}' was accepted", email);
}
}
#[test]
fn test_known_valid_urls() {
let valid_urls = vec![
"http://example.com",
"https://example.com",
"https://example.com/path",
"https://subdomain.example.com/path/to/resource",
"ftp://ftp.example.com",
"ftps://secure.example.com",
"http://localhost",
"https://192.168.1.1",
];
for url in valid_urls {
let value = json!(url);
let result = ContractValidator::validate_url(&value);
assert!(
result.is_ok(),
"Valid URL '{}' was rejected: {:?}",
url,
result.err()
);
}
}
#[test]
fn test_known_invalid_urls() {
let invalid_urls = vec![
"",
"http://",
"https://",
"example.com",
"ftp:/example.com",
"gopher://example.com",
"file://example.com",
"htp://example.com",
];
for url in invalid_urls {
let value = json!(url);
let result = ContractValidator::validate_url(&value);
assert!(result.is_err(), "Invalid URL '{}' was accepted", url);
}
}

View File

@ -284,13 +284,8 @@ impl ValidatorGenerator {
fn get_common_validator_for_field(field: &crate::models::ConfigField) -> Option<String> {
match field.field_type {
FieldType::Number => {
if field.min.is_some() && field.max.is_some() {
Some(format!(
"common.Range {} {} \"{}\"",
field.min.unwrap(),
field.max.unwrap(),
field.name
))
if let (Some(min), Some(max)) = (field.min, field.max) {
Some(format!("common.Range {} {} \"{}\"", min, max, field.name))
} else if field.min == Some(0) {
Some(format!("common.NonNegativeNumber \"{}\"", field.name))
} else if field.min == Some(1) {

View File

@ -19,8 +19,7 @@ pub async fn execute_form(
output_file: &Option<PathBuf>,
cli_locale: &Option<String>,
) -> Result<()> {
let toml_content = fs::read_to_string(&config).map_err(Error::io)?;
let mut form = form_parser::parse_toml(&toml_content)?;
let mut form = form_parser::load_form(&config)?;
// TUI backend uses unified elements array internally, migrate if using legacy format
form.migrate_to_elements();

View File

@ -376,9 +376,7 @@ async fn execute_form(
cli_locale: &Option<String>,
open_browser: bool,
) -> Result<()> {
let toml_content = fs::read_to_string(&config).map_err(Error::io)?;
let mut form = form_parser::parse_toml(&toml_content)?;
let mut form = form_parser::load_form(&config)?;
// Auto-detect format from output filename if not explicitly json
let actual_format = detect_output_format(output.as_ref(), format);
@ -389,8 +387,7 @@ async fn execute_form(
// Extract base directory for resolving relative paths in includes
let base_dir = config.parent().unwrap_or_else(|| std::path::Path::new("."));
// Expand groups with includes to load fragment files
form = form_parser::expand_includes(form, base_dir)?;
// form = form_parser::expand_includes(form, base_dir)?; // handled by load_form for .ncl
// Load default values from JSON, TOML, or .ncl file if provided
let initial_values = if let Some(defaults_path_input) = defaults {
@ -603,11 +600,7 @@ async fn nickel_roundtrip_cmd(
eprintln!("[roundtrip] Loading form: {}", form_path.display());
}
let form_content = fs::read_to_string(&form_path)
.map_err(|e| Error::validation_failed(format!("Failed to read form: {}", e)))?;
let mut form = form_parser::parse_toml(&form_content)
.map_err(|e| Error::validation_failed(format!("Failed to parse form: {}", e)))?;
let mut form = form_parser::load_form(&form_path)?;
// Web backend uses unified elements array internally, migrate if using legacy format
form.migrate_to_elements();
@ -616,9 +609,7 @@ async fn nickel_roundtrip_cmd(
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
// Expand groups with includes to load fragment files
form = form_parser::expand_includes(form, form_base_dir)
.map_err(|e| Error::validation_failed(format!("Failed to expand includes: {}", e)))?;
// form = form_parser::expand_includes(form, form_base_dir)?; // handled by load_form for .ncl
// Step 3: Load default values from input .ncl if exists
let initial_values = if input.exists() {

View File

@ -85,8 +85,7 @@ pub async fn execute_form(
vault_token: Option<String>,
vault_key_path: Option<String>,
) -> Result<()> {
let toml_content = fs::read_to_string(&config).map_err(Error::io)?;
let form = form_parser::parse_toml(&toml_content)?;
let form = form_parser::load_form(&config)?;
let base_dir = config.parent().unwrap_or_else(|| std::path::Path::new("."));
// Auto-detect format from output filename if not explicitly specified

21
docs/adr/README.md Normal file
View File

@ -0,0 +1,21 @@
# Architecture Decision Records
ADRs documenting key architectural decisions and their rationale for TypeDialog.
## ADR Index
- **[ADR-001: Nickel as First-Class Form Definition Format](./adr-001-nickel-form-definition.md)** —
Nickel (`.ncl`) forms loaded via `nickel export` subprocess, replacing `ContractValidator`,
fragment lazy-loading, and `${constraint.*}` interpolation. TOML remains supported.
## Decision Format
Each ADR follows this structure:
- **Status**: Accepted, Proposed, Deprecated, Superseded
- **Context**: Problem statement and constraints
- **Decision**: The chosen approach
- **Rationale**: Numbered list of reasons
- **Consequences**: Benefits and trade-offs
- **Implementation**: Code showing how it works
- **Alternatives Considered**: Rejected options with reasoning

View File

@ -0,0 +1,146 @@
# ADR-001: Nickel as First-Class Form Definition Format
**Status**: Accepted | **Date**: 2026-03-01 | **Supersedes**: None
## Context
TypeDialog forms were defined exclusively in TOML. TOML works well as a runtime serialization
target — zero overhead, pure data, direct `serde` deserialization — but has no type system, no
schema language, and no composition mechanism. Three structural problems accumulated:
**Incomplete contract validation.** `ContractValidator` was a Rust reimplementation of a subset
of Nickel stdlib predicates. The critical failure mode was fail-open: unknown predicates returned
`Ok(())` silently. A field with `String | std.string.IpAddr` received no validation, no error.
Every new predicate required a parallel Rust implementation guaranteed to drift from the Nickel
reference.
**I/O coupled to form execution.** Fragment lazy-loading (`includes = ["tls.toml"]`) triggered
`std::fs::read_to_string` inside the executor loop. Execution had hidden I/O side-effects. The
two-phase execution design (selectors first, then load fragments) existed specifically to work
around not knowing which fragments to load before Phase 1 ran. `${constraint.*}` interpolation
required a raw-string pre-pass before `toml::from_str` could be called.
**No static guarantees at load time.** A malformed TOML form was only detected when the executor
reached the bad field, mid-interaction, after the user had already answered several questions.
## Decision
Support Nickel (`.ncl`) as a first-class form definition format alongside TOML. Forms in Nickel
are loaded via `nickel export --format json` as a subprocess, deserializing the JSON output
directly into `FormDefinition`. If `nickel export` fails for any reason — contract violation,
syntax error, missing import — the process aborts before any interaction begins.
TOML remains fully supported. No migration is required for existing forms.
Three structural changes accompany this decision:
1. Remove `ContractValidator`. Validation is the Nickel interpreter's responsibility.
2. Replace fragment lazy-loading with Nickel native imports. `build_element_list` becomes a
pure in-memory operation.
3. Add `when_false` to `FieldDefinition` to control output behavior for conditionally-hidden
fields, enabling correct Nickel roundtrip for all schema fields.
## Rationale
1. **Fail-safe by default**`nickel export` failing hard is better than fail-open validation.
The user receives Nickel's own error message, which is precise and references the contract.
2. **Single source of truth for contracts** — The Nickel interpreter evaluates every predicate in
the stdlib. No parallel Rust implementation, no drift, no coverage gaps.
3. **Static composition at load time** — Nickel's module system (`import`, `let`, merge operators)
replaces runtime fragment loading. The executor receives a fully composed form; it performs no I/O.
4. **Proven pattern** — Config loading in typedialog already uses `nickel export` via subprocess.
The contract between the Nickel CLI and the Rust consumer is established and tested.
5. **Backward compatibility** — TOML continues to work unchanged. Adoption of `.ncl` is opt-in
per form, not a flag day.
## Consequences
- **Positive**:
- Form validation is complete: every Nickel contract is evaluated by the Nickel interpreter
- `build_element_list` is pure; executor has no I/O side-effects
- `${constraint.*}` interpolation eliminated; Nickel handles constant references natively
- Two-phase execution simplified: Phase 2 iterates an in-memory list, no file I/O
- Form authors gain Nickel's full composition model: imports, let bindings, merge operators
- **Negative**:
- `.ncl` forms require the `nickel` CLI binary on `PATH` at runtime
- TOML forms do not benefit from the new static guarantees
- `when_false = "default"` requires form authors to declare `default` values on all
conditionally-hidden fields used in Nickel roundtrip
## Implementation
### Loading a `.ncl` form
```rust
pub fn load_form(path: impl AsRef<Path>) -> Result<FormDefinition> {
match path.as_ref().extension().and_then(|e| e.to_str()) {
Some("ncl") => load_from_ncl(path),
_ => load_from_file(path), // TOML path, unchanged
}
}
pub fn load_from_ncl(path: impl AsRef<Path>) -> Result<FormDefinition> {
let json = NickelCli::export(path.as_ref())?; // aborts if nickel export fails
serde_json::from_value(json).map_err(|e| ErrorWrapper::new(format!(
"Failed to deserialize Nickel export as FormDefinition: {}", e
)))
}
```
### Form defined in Nickel
```nickel
# form.ncl
let tls = import "fragments/tls.ncl" in
{
name = "Server Configuration",
elements = [
{ type = "field", name = "host", field_type = "text", prompt = "Host: " },
{ type = "field", name = "tls_enabled", field_type = "confirm", prompt = "Enable TLS?" },
] @ tls.elements,
}
```
`nickel export --format json form.ncl` produces the fully composed `FormDefinition` JSON.
If `tls.ncl` is missing or has a type error, the subprocess exits non-zero and the process
aborts immediately.
### `when_false` for conditional fields in roundtrip
```toml
[fields.tls_cert_path]
when = "tls_enabled == true"
when_false = "default" # inject field.default into results when condition is false
default = "/etc/ssl/cert.pem"
```
When `tls_enabled` is false, `tls_cert_path` is never shown. With `when_false = "default"`,
the executor inserts `"/etc/ssl/cert.pem"` into results, so the roundtrip writer always has a
value for every field in the Nickel schema regardless of which branches the user navigated.
## Related ADRs
- No prior ADRs. This is the first recorded decision.
## Alternatives Considered
1. **Expand `ContractValidator`** — Rejected. Every new predicate requires a parallel Rust
implementation. Fail-open default is a correctness hazard. Duplication introduces drift
by construction.
2. **`nickel-lang-core` as a library crate** — Considered. Would eliminate the subprocess and
the `PATH` dependency. Rejected: `nickel-lang-core` has no stable crates.io API surface;
linking the Nickel runtime increases binary size for all backends; the subprocess model is
already proven in the config-loading path.
3. **Per-field `nickel check` during interaction** — Considered for interactive validation per
keystroke. Rejected: form execution is stateful (answer N may be required to evaluate
contract N+1). Subprocess overhead per field is not acceptable. Load-time `nickel export`
plus post-execution `nickel typecheck` (existing roundtrip path) covers both ends without
per-field cost.
4. **JSON Schema or CUE as schema layer over TOML** — Rejected. Structurally inferior to Nickel
contracts for configuration data: no gradual types, no first-class functions, no `std`
predicate library. Adopting a second schema language while Nickel is already a first-class
citizen adds complexity without comparable expressiveness.

View File

@ -13,8 +13,8 @@ The comparison point is relevant: Inquire (`inquire = "0.9"`) is one of TypeDial
```text
┌─────────────────────────────────────────────────────┐
│ typedialog │
│ Declarative TOML forms + i18n + templates + AI │
│ BackendFactory → dispatch by feature
│ Declarative forms (Nickel / TOML) + i18n + AI │
load_form → BackendFactory → dispatch by feature │
├──────────────┬──────────────┬──────────────────────-┤
│ CLI backend │ TUI backend │ Web backend │
│ (Inquire) │ (Ratatui) │ (Axum) │
@ -33,7 +33,7 @@ The comparison point is relevant: Inquire (`inquire = "0.9"`) is one of TypeDial
### Differentiators
**Unified schema** — a single TOML file describes the same form for CLI, TUI, and HTTP. No per-backend code.
**Unified schema** — a single Nickel (`.ncl`) or TOML file describes the same form for CLI, TUI, and HTTP. No per-backend code. See [ADR-001](./adr/adr-001-nickel-form-definition.md).
**Nushell as integration runtime** — outputs flow as structured data into Nu pipelines via the Nushell plugin protocol (`nu-plugin = "0.110.0"`), not as text.
@ -43,6 +43,40 @@ The comparison point is relevant: Inquire (`inquire = "0.9"`) is one of TypeDial
---
## Form Loading
### `load_form` — Unified Entry Point
All binaries and roundtrip paths call `form_parser::load_form(path)`. Extension determines the loader:
```rust
pub fn load_form(path: impl AsRef<Path>) -> Result<FormDefinition> {
match path.as_ref().extension().and_then(|e| e.to_str()) {
Some("ncl") => load_from_ncl(path),
_ => load_from_file(path), // TOML path, unchanged
}
}
```
`.ncl` forms are loaded via `nickel export --format json` as a subprocess. If the export fails for any reason — contract violation, syntax error, missing import — the process aborts before any interaction begins. TOML forms are deserialized directly via `serde`.
Fragment lazy-loading (`includes:` in TOML groups) is replaced by Nickel native imports. `build_element_list` is now a pure in-memory operation with no I/O. The functions `expand_includes` and `load_fragment_form` remain available in `fragments.rs` as rescue code.
### `when_false` — Conditional Field Output
Fields with a `when:` condition that evaluates to false are skipped by default. `when_false = "default"` instructs the executor to inject the field's `default` value into results even when the condition is false:
```toml
[fields.tls_cert_path]
when = "tls_enabled == true"
when_false = "default"
default = "/etc/ssl/cert.pem"
```
This ensures Nickel roundtrip writers always have a value for every schema field regardless of which branches the user navigated.
---
## BackendFactory
### The `FormBackend` Trait
@ -126,26 +160,27 @@ receives this as `previous_results` and uses it for `options_from` filtering in
**Phase 1 — Selector fields first**
Fields that control `when:` conditionals are identified and executed before loading any fragments.
This ensures conditional branches are known before lazy-loading dependent content.
Fields that control `when:` conditionals are identified and executed before Phase 2.
This ensures conditional branches are known before iterating the element list.
```rust
let selector_field_names = identify_selector_fields(&form);
// execute only selectors → populate results
```
**Phase 2 — Build element list with lazy loading**
**Phase 2 — Build element list (pure)**
With Phase 1 results known, fragments (`includes:`) are loaded only if their controlling condition is met.
With Phase 1 results known, the element list is built from the fully-composed in-memory form.
No I/O occurs here — fragments were resolved by `load_form` before execution began.
```rust
let element_list = build_element_list(&form, base_dir, &results)?;
let element_list = build_element_list(&form);
```
**Phase 3 — Execute remaining fields**
Iterates the element list, evaluates `when:` conditions per-element, and dispatches to the backend.
Two sub-modes:
Iterates the element list, evaluates `when:` conditions per-element, applies `when_false` defaults
for skipped fields, and dispatches to the backend. Two sub-modes:
- `DisplayMode::Complete` — passes all items and fields to `execute_form_complete` at once
(used by TUI and Web for reactive rendering)
@ -156,13 +191,13 @@ Two sub-modes:
```text
typedialog binary
├── parse TOML → FormDefinition
├── form_parser::load_form(path) → FormDefinition (.ncl via nickel export | .toml via serde)
├── BackendFactory::create(BackendType::Cli) → Box<dyn FormBackend>
├── execute_with_backend_two_phase_with_defaults(form, backend, i18n, base_dir, defaults)
│ │
│ ├── Phase 1: selector fields → backend.execute_field()
│ ├── Phase 2: build_element_list() with lazy fragment loading
│ └── Phase 3: iterate → render_display_item() / execute_field()
│ ├── Phase 2: build_element_list() — pure, no I/O
│ └── Phase 3: iterate → when/when_false → render_display_item() / execute_field()
└── HashMap<String, Value> → JSON / YAML / TOML output
```

View File

@ -0,0 +1,25 @@
{
name = "Simple Contact Form",
description = "A basic contact form with all field types",
elements = [
{ type = "text", name = "name", prompt = "Your name", required = true, placeholder = "John Doe" },
{ type = "text", name = "email", prompt = "Your email", required = true, placeholder = "john@example.com" },
{ type = "text", name = "subject", prompt = "Subject", required = true },
{ type = "editor", name = "message", prompt = "Your message", file_extension = "txt" },
{ type = "select", name = "priority", prompt = "Priority level", required = true, options = [
{ value = "Low", label = "Low Priority - Can wait" },
{ value = "Medium", label = "Medium Priority - Normal schedule" },
{ value = "High", label = "High Priority - Urgent attention" },
{ value = "Urgent", label = "Urgent - Critical issue" },
]
},
{ type = "multiselect", name = "category", prompt = "Message categories", options = [
{ value = "Bug Report", label = "🐛 Bug Report" },
{ value = "Feature Request", label = "✨ Feature Request" },
{ value = "Documentation", label = "📚 Documentation" },
{ value = "Other", label = "❓ Other" },
]
},
{ type = "confirm", name = "newsletter", prompt = "Subscribe to updates?", default = false },
],
}

View File

@ -1,57 +0,0 @@
description = "A basic contact form with all field types"
name = "Simple Contact Form"
[[elements]]
name = "name"
placeholder = "John Doe"
prompt = "Your name"
required = true
type = "text"
[[elements]]
name = "email"
placeholder = "john@example.com"
prompt = "Your email"
required = true
type = "text"
[[elements]]
name = "subject"
prompt = "Subject"
required = true
type = "text"
[[elements]]
file_extension = "txt"
name = "message"
prompt = "Your message"
type = "editor"
[[elements]]
name = "priority"
options = [
{ value = "Low", label = "Low Priority - Can wait" },
{ value = "Medium", label = "Medium Priority - Normal schedule" },
{ value = "High", label = "High Priority - Urgent attention" },
{ value = "Urgent", label = "Urgent - Critical issue" },
]
prompt = "Priority level"
required = true
type = "select"
[[elements]]
name = "category"
options = [
{ value = "Bug Report", label = "🐛 Bug Report" },
{ value = "Feature Request", label = "✨ Feature Request" },
{ value = "Documentation", label = "📚 Documentation" },
{ value = "Other", label = "❓ Other" },
]
prompt = "Message categories"
type = "multiselect"
[[elements]]
default = false
name = "newsletter"
prompt = "Subscribe to updates?"
type = "confirm"

View File

@ -0,0 +1,8 @@
{
name = "Simple Debug Form",
description = "Two fields to test form execution flow",
elements = [
{ type = "text", name = "first_name", prompt = "First Name", required = true },
{ type = "text", name = "last_name", prompt = "Last Name", required = true },
],
}

View File

@ -1,17 +0,0 @@
description = "Two fields to test form execution flow"
locale = "en-US"
name = "Simple Debug Form"
[[elements]]
name = "first_name"
order = 1
prompt = "First Name"
required = true
type = "text"
[[elements]]
name = "last_name"
order = 2
prompt = "Last Name"
required = true
type = "text"

View File

@ -0,0 +1,25 @@
{
name = "Simple Contact Form",
description = "A basic contact form with all field types",
elements = [
{ type = "text", name = "name", prompt = "Your name", required = true, placeholder = "John Doe" },
{ type = "text", name = "email", prompt = "Your email", required = true, placeholder = "john@example.com" },
{ type = "text", name = "subject", prompt = "Subject", required = true },
{ type = "editor", name = "message", prompt = "Your message", file_extension = "txt" },
{ type = "select", name = "priority", prompt = "Priority level", required = true, options = [
{ value = "Low", label = "Low Priority - Can wait" },
{ value = "Medium", label = "Medium Priority - Normal schedule" },
{ value = "High", label = "High Priority - Urgent attention" },
{ value = "Urgent", label = "Urgent - Critical issue" },
]
},
{ type = "multiselect", name = "category", prompt = "Message categories", display_mode = "grid", searchable = true, default = "Bug Report", options = [
{ value = "Bug Report", label = "🐛 Bug Report" },
{ value = "Feature Request", label = "✨ Feature Request" },
{ value = "Documentation", label = "📚 Documentation" },
{ value = "Other", label = "❓ Other" },
]
},
{ type = "confirm", name = "newsletter", prompt = "Subscribe to updates?", default = false },
],
}

View File

@ -1,60 +0,0 @@
description = "A basic contact form with all field types"
name = "Simple Contact Form"
[[elements]]
name = "name"
placeholder = "John Doe"
prompt = "Your name"
required = true
type = "text"
[[elements]]
name = "email"
placeholder = "john@example.com"
prompt = "Your email"
required = true
type = "text"
[[elements]]
name = "subject"
prompt = "Subject"
required = true
type = "text"
[[elements]]
file_extension = "txt"
name = "message"
prompt = "Your message"
type = "editor"
[[elements]]
name = "priority"
options = [
{ value = "Low", label = "Low Priority - Can wait" },
{ value = "Medium", label = "Medium Priority - Normal schedule" },
{ value = "High", label = "High Priority - Urgent attention" },
{ value = "Urgent", label = "Urgent - Critical issue" },
]
prompt = "Priority level"
required = true
type = "select"
[[elements]]
default = "Bug Report"
display_mode = "grid"
name = "category"
options = [
{ value = "Bug Report", label = "🐛 Bug Report" },
{ value = "Feature Request", label = "✨ Feature Request" },
{ value = "Documentation", label = "📚 Documentation" },
{ value = "Other", label = "❓ Other" },
]
prompt = "Message categories"
searchable = true
type = "multiselect"
[[elements]]
default = "false"
name = "newsletter"
prompt = "Subscribe to updates?"
type = "confirm"

View File

@ -0,0 +1,50 @@
{
name = "Form with Grouped Display Items",
description = "Demonstrates grouping related display items together",
elements = [
# Main header (no group - always shown)
{ type = "header", name = "main_header", title = "✨ Grouped Items Example", align = "center", border_top = true, border_bottom = true },
# Account selection
{ type = "select", name = "account_type", prompt = "Select account type", required = true, options = [
{ value = "Personal", label = "Personal - Individual Users" },
{ value = "Premium", label = "Premium - Growing Teams" },
{ value = "Enterprise", label = "Enterprise - Large Organizations" },
]
},
# PREMIUM GROUP
{ type = "section", name = "premium_header", title = "🌟 Premium Features", border_top = true, group = "premium", when = "account_type == Premium" },
{ type = "section", name = "premium_features", content = "✓ Unlimited storage\n✓ Advanced analytics\n✓ Priority support\n✓ Custom branding", group = "premium", when = "account_type == Premium" },
{ type = "section", name = "premium_price", content = "Pricing: $29/month", border_bottom = true, group = "premium", when = "account_type == Premium" },
{ type = "select", name = "premium_payment", prompt = "Payment method", required = true, when = "account_type == Premium", options = [
{ value = "Credit Card", label = "💳 Credit Card" },
{ value = "Bank Transfer", label = "🏦 Bank Transfer" },
{ value = "PayPal", label = "🅿️ PayPal" },
]
},
# ENTERPRISE GROUP
{ type = "section", name = "enterprise_header", title = "🏛️ Enterprise Solution", border_top = true, group = "enterprise", when = "account_type == Enterprise" },
{ type = "section", name = "enterprise_features", content = "✓ Unlimited everything\n✓ Dedicated support\n✓ Custom integration\n✓ SLA guarantee\n✓ On-premise option", group = "enterprise", when = "account_type == Enterprise" },
{ type = "section", name = "enterprise_note", content = "⚠️ Requires enterprise agreement", group = "enterprise", when = "account_type == Enterprise" },
{ type = "section", name = "enterprise_contact_info", content = "Contact our sales team for pricing", border_bottom = true, group = "enterprise", when = "account_type == Enterprise" },
{ type = "text", name = "enterprise_contact_email", prompt = "Your email", required = true, when = "account_type == Enterprise" },
# SUPPORT GROUP
{ type = "section", name = "support_section_header", title = "📞 Support Options", border_top = true, group = "support" },
{ type = "section", name = "support_info", content = "Choose your preferred support level", group = "support" },
{ type = "select", name = "support_level", prompt = "Support Level", required = true, group = "support", options = [
{ value = "Basic", label = "Basic - Email only" },
{ value = "Standard", label = "Standard - Email & chat" },
{ value = "Premium", label = "Premium - Phone & live support" },
]
},
{ type = "section", name = "support_footer", content = "Support is available 24/7", border_bottom = true, group = "support" },
# FINAL GROUP
{ type = "section", name = "final_header", title = "✅ Complete Registration", border_top = true, group = "final" },
{ type = "confirm", name = "agree_terms", prompt = "I agree to the terms and conditions", required = true, group = "final" },
{ type = "cta", name = "final_cta", title = "Ready?", content = "Click submit to complete your registration", align = "center", border_top = true, border_bottom = true, group = "final" },
],
}

View File

@ -1,155 +0,0 @@
description = "Demonstrates grouping related display items together"
name = "Form with Grouped Display Items"
# Main header (no group - always shown)
[[elements]]
align = "center"
border_bottom = true
border_top = true
name = "main_header"
title = "✨ Grouped Items Example"
type = "header"
# Account selection
[[elements]]
name = "account_type"
options = [
{ value = "Personal", label = "Personal - Individual Users" },
{ value = "Premium", label = "Premium - Growing Teams" },
{ value = "Enterprise", label = "Enterprise - Large Organizations" },
]
prompt = "Select account type"
required = true
type = "select"
# PREMIUM GROUP - All these items are grouped together
[[elements]]
border_top = true
group = "premium"
name = "premium_header"
title = "🌟 Premium Features"
type = "section"
when = "account_type == Premium"
[[elements]]
content = "✓ Unlimited storage\n✓ Advanced analytics\n✓ Priority support\n✓ Custom branding"
group = "premium"
name = "premium_features"
type = "section"
when = "account_type == Premium"
[[elements]]
border_bottom = true
content = "Pricing: $29/month"
group = "premium"
name = "premium_price"
type = "section"
when = "account_type == Premium"
[[elements]]
name = "premium_payment"
options = [
{ value = "Credit Card", label = "💳 Credit Card" },
{ value = "Bank Transfer", label = "🏦 Bank Transfer" },
{ value = "PayPal", label = "🅿️ PayPal" },
]
prompt = "Payment method"
required = true
type = "select"
when = "account_type == Premium"
# ENTERPRISE GROUP - All these items grouped together
[[elements]]
border_top = true
group = "enterprise"
name = "enterprise_header"
title = "🏛️ Enterprise Solution"
type = "section"
when = "account_type == Enterprise"
[[elements]]
content = "✓ Unlimited everything\n✓ Dedicated support\n✓ Custom integration\n✓ SLA guarantee\n✓ On-premise option"
group = "enterprise"
name = "enterprise_features"
type = "section"
when = "account_type == Enterprise"
[[elements]]
content = "⚠️ Requires enterprise agreement"
group = "enterprise"
name = "enterprise_note"
type = "section"
when = "account_type == Enterprise"
[[elements]]
border_bottom = true
content = "Contact our sales team for pricing"
group = "enterprise"
name = "enterprise_contact_info"
type = "section"
when = "account_type == Enterprise"
[[elements]]
name = "enterprise_contact_email"
prompt = "Your email"
required = true
type = "text"
when = "account_type == Enterprise"
# SUPPORT GROUP - Organized support options
[[elements]]
border_top = true
group = "support"
name = "support_section_header"
title = "📞 Support Options"
type = "section"
[[elements]]
content = "Choose your preferred support level"
group = "support"
name = "support_info"
type = "section"
[[elements]]
group = "support"
name = "support_level"
options = [
{ value = "Basic", label = "Basic - Email only" },
{ value = "Standard", label = "Standard - Email & chat" },
{ value = "Premium", label = "Premium - Phone & live support" },
]
prompt = "Support Level"
required = true
type = "select"
[[elements]]
border_bottom = true
content = "Support is available 24/7"
group = "support"
name = "support_footer"
type = "section"
# FINAL GROUP - Completion items
[[elements]]
border_top = true
group = "final"
name = "final_header"
title = "✅ Complete Registration"
type = "section"
[[elements]]
group = "final"
name = "agree_terms"
prompt = "I agree to the terms and conditions"
required = true
type = "confirm"
[[elements]]
align = "center"
border_bottom = true
border_top = true
content = "Click submit to complete your registration"
group = "final"
name = "final_cta"
title = "Ready?"
type = "cta"

View File

@ -0,0 +1,61 @@
{
name = "Professional Service Registration",
description = "Multi-section registration form with headers and CTAs",
elements = [
# Header section
{ type = "header", name = "main_header", title = "🎯 Professional Services Registration", align = "center", border_top = true, border_bottom = true },
# Welcome section
{ type = "section", name = "welcome", content = "Welcome to our professional services platform. Please fill in your information to get started." },
# Contact information section header
{ type = "section_header", name = "contact_header", title = "📋 Contact Information", border_top = true, margin_left = 0 },
# Contact fields
{ type = "text", name = "full_name", prompt = "Full Name", required = true },
{ type = "text", name = "email", prompt = "Email Address", required = true },
{ type = "text", name = "phone", prompt = "Phone Number", required = false },
# Services section
{ type = "section_header", name = "services_header", title = "🔧 Services Selection", border_top = true, margin_left = 0 },
{ type = "select", name = "primary_service", prompt = "Primary Service Needed", required = true, options = [
{ value = "Consulting", label = "💼 Consulting & Strategy" },
{ value = "Development", label = "🚀 Development & Engineering" },
{ value = "Design", label = "🎨 Design & UX" },
{ value = "Support", label = "🛠️ Support & Maintenance" },
]
},
{ type = "multiselect", name = "additional_services", prompt = "Additional Services", options = [
{ value = "Training", label = "📚 Training Programs" },
{ value = "Documentation", label = "📖 Documentation" },
{ value = "Maintenance", label = "🔧 Maintenance & Support" },
{ value = "Security Audit", label = "🔐 Security Audit" },
]
},
# Preferences section
{ type = "section_header", name = "prefs_header", title = "⚙️ Preferences", border_top = true, margin_left = 0 },
{ type = "select", name = "experience_level", prompt = "Your Experience Level", options = [
{ value = "Beginner", label = "Beginner - Just getting started" },
{ value = "Intermediate", label = "Intermediate - Some experience" },
{ value = "Advanced", label = "Advanced - Deep knowledge" },
{ value = "Expert", label = "Expert - Full mastery" },
]
},
{ type = "select", name = "preferred_contact", prompt = "Preferred Contact Method", options = [
{ value = "Email", label = "📧 Email" },
{ value = "Phone", label = "📞 Phone" },
{ value = "Video Call", label = "🎥 Video Call" },
]
},
# Agreement section
{ type = "section_header", name = "agreement_header", title = "📜 Agreement", border_top = true, margin_left = 0 },
{ type = "confirm", name = "agree_terms", prompt = "I agree to the terms and conditions", required = true, default = false },
{ type = "confirm", name = "agree_privacy", prompt = "I agree to the privacy policy", required = true, default = false },
{ type = "confirm", name = "marketing_consent", prompt = "I consent to receive marketing communications", default = false },
# Footer with CTA
{ type = "cta", name = "final_cta", title = "Thank you for your information!", content = "Click submit to complete your registration.", align = "center", border_top = true, border_bottom = true },
],
}

View File

@ -1,142 +0,0 @@
description = "Multi-section registration form with headers and CTAs"
name = "Professional Service Registration"
# Header section
[[elements]]
align = "center"
border_bottom = true
border_top = true
name = "main_header"
title = "🎯 Professional Services Registration"
type = "header"
# Welcome section
[[elements]]
content = "Welcome to our professional services platform. Please fill in your information to get started."
name = "welcome"
type = "section"
# Contact information section header
[[elements]]
border_top = true
margin_left = 0
name = "contact_header"
title = "📋 Contact Information"
type = "section_header"
# Contact fields
[[elements]]
name = "full_name"
prompt = "Full Name"
required = true
type = "text"
[[elements]]
name = "email"
prompt = "Email Address"
required = true
type = "text"
[[elements]]
name = "phone"
prompt = "Phone Number"
required = false
type = "text"
# Services section
[[elements]]
border_top = true
margin_left = 0
name = "services_header"
title = "🔧 Services Selection"
type = "section_header"
[[elements]]
name = "primary_service"
options = [
{ value = "Consulting", label = "💼 Consulting & Strategy" },
{ value = "Development", label = "🚀 Development & Engineering" },
{ value = "Design", label = "🎨 Design & UX" },
{ value = "Support", label = "🛠️ Support & Maintenance" },
]
prompt = "Primary Service Needed"
required = true
type = "select"
[[elements]]
name = "additional_services"
options = [
{ value = "Training", label = "📚 Training Programs" },
{ value = "Documentation", label = "📖 Documentation" },
{ value = "Maintenance", label = "🔧 Maintenance & Support" },
{ value = "Security Audit", label = "🔐 Security Audit" },
]
prompt = "Additional Services"
type = "multiselect"
# Preferences section
[[elements]]
border_top = true
margin_left = 0
name = "prefs_header"
title = "⚙️ Preferences"
type = "section_header"
[[elements]]
name = "experience_level"
options = [
{ value = "Beginner", label = "Beginner - Just getting started" },
{ value = "Intermediate", label = "Intermediate - Some experience" },
{ value = "Advanced", label = "Advanced - Deep knowledge" },
{ value = "Expert", label = "Expert - Full mastery" },
]
prompt = "Your Experience Level"
type = "select"
[[elements]]
name = "preferred_contact"
options = [
{ value = "Email", label = "📧 Email" },
{ value = "Phone", label = "📞 Phone" },
{ value = "Video Call", label = "🎥 Video Call" },
]
prompt = "Preferred Contact Method"
type = "select"
# Agreement section
[[elements]]
border_top = true
margin_left = 0
name = "agreement_header"
title = "📜 Agreement"
type = "section_header"
[[elements]]
default = false
name = "agree_terms"
prompt = "I agree to the terms and conditions"
required = true
type = "confirm"
[[elements]]
default = false
name = "agree_privacy"
prompt = "I agree to the privacy policy"
required = true
type = "confirm"
[[elements]]
default = false
name = "marketing_consent"
prompt = "I consent to receive marketing communications"
type = "confirm"
# Footer with CTA
[[elements]]
align = "center"
border_bottom = true
border_top = true
content = "Click submit to complete your registration."
name = "final_cta"
title = "Thank you for your information!"
type = "cta"

View File

@ -0,0 +1,59 @@
{
name = "User Account Setup",
description = "Setup account with conditional fields based on user type",
elements = [
# First, select account type
{ type = "select", name = "account_type", prompt = "What type of account do you want?", required = true, options = [
{ value = "Personal", label = "Personal - Individual use" },
{ value = "Business", label = "Business - Company account" },
{ value = "Developer", label = "Developer - Technical team" },
]
},
# Business name is only shown if account_type == Business
{ type = "text", name = "business_name", prompt = "Enter your business name", required = true, placeholder = "Acme Corporation", when = "account_type == Business" },
{ type = "text", name = "business_registration", prompt = "Business registration number", placeholder = "123-456-789", when = "account_type == Business" },
# Developer specific fields
{ type = "text", name = "github_username", prompt = "GitHub username (optional)", when = "account_type == Developer" },
{ type = "select", name = "preferred_language", prompt = "Preferred programming language", when = "account_type == Developer", options = [
{ value = "Rust", label = "🦀 Rust - Systems programming" },
{ value = "Python", label = "🐍 Python - Data & scripting" },
{ value = "Go", label = "🐹 Go - Cloud native" },
{ value = "Java", label = "☕ Java - Enterprise" },
{ value = "JavaScript", label = "🟨 JavaScript - Web development" },
]
},
# Email (required for all)
{ type = "text", name = "email", prompt = "Email address", required = true, placeholder = "user@example.com" },
# Enable 2FA
{ type = "confirm", name = "enable_2fa", prompt = "Enable two-factor authentication?", required = true, default = true },
# 2FA method only if enabled
{ type = "select", name = "2fa_method", prompt = "Choose 2FA method", required = true, when = "enable_2fa == true", options = [
{ value = "TOTP", label = "🔐 TOTP - Authenticator app" },
{ value = "SMS", label = "📱 SMS - Text message" },
{ value = "Email", label = "📧 Email" },
]
},
# Phone number for SMS 2FA
{ type = "text", name = "phone_number", prompt = "Phone number (for SMS 2FA)", required = true, placeholder = "+1234567890", when = "2fa_method == SMS" },
# Newsletter subscription
{ type = "confirm", name = "subscribe_newsletter", prompt = "Subscribe to our newsletter?", default = false },
# Newsletter frequency (only if subscribed)
{ type = "select", name = "newsletter_frequency", prompt = "How often would you like to receive newsletters?", required = true, when = "subscribe_newsletter == true", options = [
{ value = "Weekly", label = "📬 Weekly - Every 7 days" },
{ value = "Monthly", label = "📅 Monthly - Once per month" },
{ value = "Quarterly", label = "📊 Quarterly - Every 3 months" },
]
},
# Terms and conditions (always required)
{ type = "confirm", name = "agree_terms", prompt = "I agree to the terms and conditions *", required = true, default = false },
],
}

View File

@ -1,117 +0,0 @@
description = "Setup account with conditional fields based on user type"
name = "User Account Setup"
# First, select account type
[[elements]]
name = "account_type"
options = [
{ value = "Personal", label = "Personal - Individual use" },
{ value = "Business", label = "Business - Company account" },
{ value = "Developer", label = "Developer - Technical team" },
]
prompt = "What type of account do you want?"
required = true
type = "select"
# Business name is only shown if account_type == Business
[[elements]]
name = "business_name"
placeholder = "Acme Corporation"
prompt = "Enter your business name"
required = true
type = "text"
when = "account_type == Business"
# Business registration is only needed for Business
[[elements]]
name = "business_registration"
placeholder = "123-456-789"
prompt = "Business registration number"
type = "text"
when = "account_type == Business"
# Developer specific fields
[[elements]]
name = "github_username"
prompt = "GitHub username (optional)"
type = "text"
when = "account_type == Developer"
[[elements]]
name = "preferred_language"
options = [
{ value = "Rust", label = "🦀 Rust - Systems programming" },
{ value = "Python", label = "🐍 Python - Data & scripting" },
{ value = "Go", label = "🐹 Go - Cloud native" },
{ value = "Java", label = "☕ Java - Enterprise" },
{ value = "JavaScript", label = "🟨 JavaScript - Web development" },
]
prompt = "Preferred programming language"
type = "select"
when = "account_type == Developer"
# Email (required for all)
[[elements]]
name = "email"
placeholder = "user@example.com"
prompt = "Email address"
required = true
type = "text"
# Enable 2FA
[[elements]]
default = true
name = "enable_2fa"
prompt = "Enable two-factor authentication?"
required = true
type = "confirm"
# 2FA method only if enabled
[[elements]]
name = "2fa_method"
options = [
{ value = "TOTP", label = "🔐 TOTP - Authenticator app" },
{ value = "SMS", label = "📱 SMS - Text message" },
{ value = "Email", label = "📧 Email" },
]
prompt = "Choose 2FA method"
required = true
type = "select"
when = "enable_2fa == true"
# Phone number for SMS 2FA
[[elements]]
name = "phone_number"
placeholder = "+1234567890"
prompt = "Phone number (for SMS 2FA)"
required = true
type = "text"
when = "2fa_method == SMS"
# Newsletter subscription
[[elements]]
default = false
name = "subscribe_newsletter"
prompt = "Subscribe to our newsletter?"
type = "confirm"
# Newsletter frequency (only if subscribed)
[[elements]]
name = "newsletter_frequency"
options = [
{ value = "Weekly", label = "📬 Weekly - Every 7 days" },
{ value = "Monthly", label = "📅 Monthly - Once per month" },
{ value = "Quarterly", label = "📊 Quarterly - Every 3 months" },
]
prompt = "How often would you like to receive newsletters?"
required = true
type = "select"
when = "subscribe_newsletter == true"
# Terms and conditions (always required)
[[elements]]
default = false
name = "agree_terms"
prompt = "I agree to the terms and conditions *"
required = true
type = "confirm"

View File

@ -0,0 +1,75 @@
{
name = "Dynamic Section Management",
description = "Form with sections that appear/disappear based on selections",
elements = [
# Main header (always visible)
{ type = "header", name = "main_header", title = "✨ Dynamic Form with Conditional Sections", align = "center", border_top = true, border_bottom = true },
# Instructions (always visible)
{ type = "section", name = "instructions", content = "Select your preferences below. Additional sections will appear based on your choices.", margin_left = 2 },
# Account type selection
{ type = "select", name = "account_type", prompt = "What type of account do you need?", required = true, options = [
{ value = "Personal", label = "Personal - Individual use" },
{ value = "Business", label = "Business - Small to medium teams" },
{ value = "Enterprise", label = "Enterprise - Large organizations" },
]
},
# Business section (only if account_type == Business)
{ type = "section", name = "business_section_header", title = "🏢 Business Information", border_top = true, when = "account_type == Business" },
{ type = "text", name = "company_name", prompt = "Company Name", required = true, when = "account_type == Business" },
{ type = "select", name = "company_size", prompt = "Company Size", when = "account_type == Business", options = [
{ value = "1-10", label = "1-10 - Startup" },
{ value = "11-50", label = "11-50 - Small business" },
{ value = "51-200", label = "51-200 - Growth stage" },
{ value = "200+", label = "200+ - Enterprise scale" },
]
},
# Enterprise section (only if account_type == Enterprise)
{ type = "section", name = "enterprise_section_header", title = "🏛️ Enterprise Setup", border_top = true, when = "account_type == Enterprise" },
{ type = "section", name = "enterprise_warning", content = "⚠️ Enterprise accounts require additional verification and support setup.", when = "account_type == Enterprise" },
{ type = "text", name = "enterprise_contact", prompt = "Enterprise Account Manager Email", required = true, when = "account_type == Enterprise" },
# Infrastructure selection (visible for Business)
{ type = "section", name = "infrastructure_header", title = "🔧 Infrastructure Preferences", border_top = true, when = "account_type == Business" },
{ type = "section", name = "infrastructure_header_enterprise", title = "🔧 Infrastructure Preferences", border_top = true, when = "account_type == Enterprise" },
{ type = "select", name = "hosting_preference", prompt = "Preferred Hosting", when = "account_type == Business", options = [
{ value = "Cloud", label = "☁️ Cloud - AWS/Azure/GCP" },
{ value = "On-Premise", label = "🏢 On-Premise - Your data center" },
{ value = "Hybrid", label = "🔀 Hybrid - Mix of both" },
]
},
{ type = "select", name = "hosting_preference_enterprise", prompt = "Preferred Hosting", when = "account_type == Enterprise", options = [
{ value = "Cloud", label = "☁️ Cloud - AWS/Azure/GCP" },
{ value = "On-Premise", label = "🏢 On-Premise - Your data center" },
{ value = "Hybrid", label = "🔀 Hybrid - Mix of both" },
{ value = "Multi-Cloud", label = "🌐 Multi-Cloud - Multiple providers" },
]
},
# Support level selection
{ type = "section", name = "support_header", title = "💬 Support Options", border_top = true, margin_left = 0 },
{ type = "select", name = "support_level", prompt = "Support Level", required = true, options = [
{ value = "Community", label = "👥 Community - Free community support" },
{ value = "Basic", label = "🛠️ Basic - Email support" },
{ value = "Premium", label = "⭐ Premium - 24/7 phone & email" },
{ value = "Enterprise", label = "🏛️ Enterprise - Dedicated team" },
]
},
# Premium support details
{ type = "section", name = "premium_support_info", title = "⭐ Premium Support Includes", content = "✓ 24/7 Phone Support\n✓ Dedicated Account Manager\n✓ Priority Queue\n✓ SLA Guarantees", border_top = true, when = "support_level == Premium" },
# Enterprise support details
{ type = "section", name = "enterprise_support_info", title = "⭐⭐ Enterprise Support Includes", content = "✓ 24/7 Dedicated Support Line\n✓ Dedicated Technical Team\n✓ Custom SLA\n✓ Onsite Support Available", border_top = true, when = "support_level == Enterprise" },
{ type = "text", name = "support_email", prompt = "Support Contact Email", required = true, when = "support_level == Premium" },
{ type = "text", name = "support_email_enterprise", prompt = "Support Contact Email", required = true, when = "support_level == Enterprise" },
# Final section
{ type = "section", name = "final_section", title = "✅ Ready to Complete", content = "Review your selections above and click submit to create your account.", align = "center", border_top = true, border_bottom = true },
{ type = "confirm", name = "agree_terms", prompt = "I agree to the terms and conditions", required = true },
],
}

View File

@ -1,184 +0,0 @@
description = "Form with sections that appear/disappear based on selections"
name = "Dynamic Section Management"
# Main header (always visible)
[[elements]]
align = "center"
border_bottom = true
border_top = true
name = "main_header"
title = "✨ Dynamic Form with Conditional Sections"
type = "header"
# Instructions (always visible)
[[elements]]
content = "Select your preferences below. Additional sections will appear based on your choices."
margin_left = 2
name = "instructions"
type = "section"
# Account type selection
[[elements]]
name = "account_type"
options = [
{ value = "Personal", label = "Personal - Individual use" },
{ value = "Business", label = "Business - Small to medium teams" },
{ value = "Enterprise", label = "Enterprise - Large organizations" },
]
prompt = "What type of account do you need?"
required = true
type = "select"
# Business section (only if account_type == Business)
[[elements]]
border_top = true
name = "business_section_header"
title = "🏢 Business Information"
type = "section"
when = "account_type == Business"
[[elements]]
name = "company_name"
prompt = "Company Name"
required = true
type = "text"
when = "account_type == Business"
[[elements]]
name = "company_size"
options = [
{ value = "1-10", label = "1-10 - Startup" },
{ value = "11-50", label = "11-50 - Small business" },
{ value = "51-200", label = "51-200 - Growth stage" },
{ value = "200+", label = "200+ - Enterprise scale" },
]
prompt = "Company Size"
type = "select"
when = "account_type == Business"
# Enterprise section (only if account_type == Enterprise)
[[elements]]
border_top = true
name = "enterprise_section_header"
title = "🏛️ Enterprise Setup"
type = "section"
when = "account_type == Enterprise"
[[elements]]
content = "⚠️ Enterprise accounts require additional verification and support setup."
name = "enterprise_warning"
type = "section"
when = "account_type == Enterprise"
[[elements]]
name = "enterprise_contact"
prompt = "Enterprise Account Manager Email"
required = true
type = "text"
when = "account_type == Enterprise"
# Infrastructure selection (visible for Business & Enterprise)
[[elements]]
border_top = true
name = "infrastructure_header"
title = "🔧 Infrastructure Preferences"
type = "section"
when = "account_type == Business"
[[elements]]
border_top = true
name = "infrastructure_header_enterprise"
title = "🔧 Infrastructure Preferences"
type = "section"
when = "account_type == Enterprise"
[[elements]]
name = "hosting_preference"
options = [
{ value = "Cloud", label = "☁️ Cloud - AWS/Azure/GCP" },
{ value = "On-Premise", label = "🏢 On-Premise - Your data center" },
{ value = "Hybrid", label = "🔀 Hybrid - Mix of both" },
]
prompt = "Preferred Hosting"
type = "select"
when = "account_type == Business"
[[elements]]
name = "hosting_preference_enterprise"
options = [
{ value = "Cloud", label = "☁️ Cloud - AWS/Azure/GCP" },
{ value = "On-Premise", label = "🏢 On-Premise - Your data center" },
{ value = "Hybrid", label = "🔀 Hybrid - Mix of both" },
{ value = "Multi-Cloud", label = "🌐 Multi-Cloud - Multiple providers" },
]
prompt = "Preferred Hosting"
type = "select"
when = "account_type == Enterprise"
# Support level selection
[[elements]]
border_top = true
margin_left = 0
name = "support_header"
title = "💬 Support Options"
type = "section"
[[elements]]
name = "support_level"
options = [
{ value = "Community", label = "👥 Community - Free community support" },
{ value = "Basic", label = "🛠️ Basic - Email support" },
{ value = "Premium", label = "⭐ Premium - 24/7 phone & email" },
{ value = "Enterprise", label = "🏛️ Enterprise - Dedicated team" },
]
prompt = "Support Level"
required = true
type = "select"
# Premium support details (only if support_level == Premium)
[[elements]]
border_top = true
content = "✓ 24/7 Phone Support\n✓ Dedicated Account Manager\n✓ Priority Queue\n✓ SLA Guarantees"
name = "premium_support_info"
title = "⭐ Premium Support Includes"
type = "section"
when = "support_level == Premium"
# Enterprise support details (only if support_level == Enterprise)
[[elements]]
border_top = true
content = "✓ 24/7 Dedicated Support Line\n✓ Dedicated Technical Team\n✓ Custom SLA\n✓ Onsite Support Available"
name = "enterprise_support_info"
title = "⭐⭐ Enterprise Support Includes"
type = "section"
when = "support_level == Enterprise"
[[elements]]
name = "support_email"
prompt = "Support Contact Email"
required = true
type = "text"
when = "support_level == Premium"
[[elements]]
name = "support_email_enterprise"
prompt = "Support Contact Email"
required = true
type = "text"
when = "support_level == Enterprise"
# Final section
[[elements]]
align = "center"
border_bottom = true
border_top = true
content = "Review your selections above and click submit to create your account."
name = "final_section"
title = "✅ Ready to Complete"
type = "section"
[[elements]]
name = "agree_terms"
prompt = "I agree to the terms and conditions"
required = true
type = "confirm"

View File

@ -0,0 +1,35 @@
{
name = "Display Items Showcase",
description = "Demonstrates all display item types and attributes",
elements = [
# Basic Header
{ type = "header", name = "header_basic", title = "Basic Header" },
# Header with borders
{ type = "header", name = "header_bordered", title = "Header with Borders", border_top = true, border_bottom = true },
# Header centered
{ type = "header", name = "header_centered", title = "Centered Header", align = "center", border_top = true, border_bottom = true },
# Simple section with content
{ type = "section", name = "info_section", content = "This is a simple information section. It contains text that guides the user." },
# Section with borders
{ type = "section", name = "important_info", title = "Important Information", content = "This section has both title and content with a border on top.", border_top = true },
# Multi-line content section
{ type = "section", name = "multiline_section", title = "Features", content = "✓ Feature One\n✓ Feature Two\n✓ Feature Three\n✓ Feature Four", border_bottom = true },
# Example field
{ type = "text", name = "example_field", prompt = "Enter something" },
# Left-aligned section
{ type = "section", name = "instructions", content = "Please follow the instructions above.", margin_left = 2 },
# Call To Action
{ type = "cta", name = "cta_submit", title = "Ready?", content = "Click submit when you're done!", align = "center", border_top = true, border_bottom = true },
# Right-aligned footer
{ type = "footer", name = "footer", content = "© 2024 Your Company. All rights reserved.", align = "right", margin_left = 0 },
],
}

View File

@ -1,78 +0,0 @@
description = "Demonstrates all display item types and attributes"
name = "Display Items Showcase"
# Basic Header
[[elements]]
name = "header_basic"
title = "Basic Header"
type = "header"
# Header with borders
[[elements]]
border_bottom = true
border_top = true
name = "header_bordered"
title = "Header with Borders"
type = "header"
# Header centered
[[elements]]
align = "center"
border_bottom = true
border_top = true
name = "header_centered"
title = "Centered Header"
type = "header"
# Simple section with content
[[elements]]
content = "This is a simple information section. It contains text that guides the user."
name = "info_section"
type = "section"
# Section with borders
[[elements]]
border_top = true
content = "This section has both title and content with a border on top."
name = "important_info"
title = "Important Information"
type = "section"
# Multi-line content section
[[elements]]
border_bottom = true
content = "✓ Feature One\n✓ Feature Two\n✓ Feature Three\n✓ Feature Four"
name = "multiline_section"
title = "Features"
type = "section"
# Example field
[[elements]]
name = "example_field"
prompt = "Enter something"
type = "text"
# Left-aligned section
[[elements]]
content = "Please follow the instructions above."
margin_left = 2
name = "instructions"
type = "section"
# Call To Action
[[elements]]
align = "center"
border_bottom = true
border_top = true
content = "Click submit when you're done!"
name = "cta_submit"
title = "Ready?"
type = "cta"
# Right-aligned footer
[[elements]]
align = "right"
content = "© 2024 Your Company. All rights reserved."
margin_left = 0
name = "footer"
type = "footer"

View File

@ -0,0 +1,49 @@
{
name = "MultiSelect Display Modes Demo",
description = "Demonstrates different display modes and features for multiselect fields",
elements = [
# List mode (default) - vertical checkboxes
{ type = "multiselect", name = "features_list", prompt = "Features (List mode - default)", default = "logging", help = "Vertical checkbox list", options = [
{ value = "logging", label = "📝 Logging" },
{ value = "metrics", label = "📊 Metrics" },
{ value = "tracing", label = "🔍 Tracing" },
{ value = "profiling", label = "⚡ Profiling" },
]
},
# Grid mode - responsive grid layout
{ type = "multiselect", name = "languages_grid", prompt = "Programming Languages (Grid mode)", display_mode = "grid", searchable = true, default = "rust,python", help = "Responsive grid with icons", options = [
{ value = "rust", label = "🦀 Rust" },
{ value = "python", label = "🐍 Python" },
{ value = "javascript", label = "📜 JavaScript" },
{ value = "go", label = "🐹 Go" },
{ value = "java", label = "☕ Java" },
{ value = "csharp", label = "🔵 C#" },
]
},
# Dropdown mode - native select multiple with search
{ type = "multiselect", name = "frameworks", prompt = "Web Frameworks", display_mode = "dropdown", searchable = true, required = true, min_selected = 1, max_selected = 3, help = "Use dropdown mode for 10+ options", options = [
{ value = "react", label = "React" },
{ value = "vue", label = "Vue" },
{ value = "angular", label = "Angular" },
{ value = "svelte", label = "Svelte" },
{ value = "nextjs", label = "Next.js" },
{ value = "nuxt", label = "Nuxt" },
{ value = "astro", label = "Astro" },
{ value = "remix", label = "Remix" },
{ value = "gatsby", label = "Gatsby" },
{ value = "qwik", label = "Qwik" },
]
},
# Example with min/max validation
{ type = "multiselect", name = "permissions", prompt = "User Permissions", display_mode = "grid", min_selected = 1, max_selected = 3, default = "read", help = "Select between 1 and 3 permissions", options = [
{ value = "read", label = "📖 Read" },
{ value = "write", label = "✏️ Write" },
{ value = "delete", label = "🗑️ Delete" },
{ value = "admin", label = "🔑 Admin" },
]
},
],
}

View File

@ -1,75 +0,0 @@
description = "Demonstrates different display modes and features for multiselect fields"
name = "MultiSelect Display Modes Demo"
# List mode (default) - vertical checkboxes
[[elements]]
default = "logging"
help = "Vertical checkbox list"
name = "features_list"
options = [
{ value = "logging", label = "📝 Logging" },
{ value = "metrics", label = "📊 Metrics" },
{ value = "tracing", label = "🔍 Tracing" },
{ value = "profiling", label = "⚡ Profiling" },
]
prompt = "Features (List mode - default)"
type = "multiselect"
# Grid mode - responsive grid layout
[[elements]]
default = "rust,python"
display_mode = "grid"
help = "Responsive grid with icons"
name = "languages_grid"
options = [
{ value = "rust", label = "🦀 Rust" },
{ value = "python", label = "🐍 Python" },
{ value = "javascript", label = "📜 JavaScript" },
{ value = "go", label = "🐹 Go" },
{ value = "java", label = "☕ Java" },
{ value = "csharp", label = "🔵 C#" },
]
prompt = "Programming Languages (Grid mode)"
searchable = true
type = "multiselect"
# Dropdown mode - native select multiple with search
[[elements]]
display_mode = "dropdown"
help = "Use dropdown mode for 10+ options"
max_selected = 3
min_selected = 1
name = "frameworks"
options = [
{ value = "react", label = "React" },
{ value = "vue", label = "Vue" },
{ value = "angular", label = "Angular" },
{ value = "svelte", label = "Svelte" },
{ value = "nextjs", label = "Next.js" },
{ value = "nuxt", label = "Nuxt" },
{ value = "astro", label = "Astro" },
{ value = "remix", label = "Remix" },
{ value = "gatsby", label = "Gatsby" },
{ value = "qwik", label = "Qwik" },
]
prompt = "Web Frameworks"
required = true
searchable = true
type = "multiselect"
# Example with min/max validation
[[elements]]
default = "read"
display_mode = "grid"
help = "Select between 1 and 3 permissions"
max_selected = 3
min_selected = 1
name = "permissions"
options = [
{ value = "read", label = "📖 Read" },
{ value = "write", label = "✏️ Write" },
{ value = "delete", label = "🗑️ Delete" },
{ value = "admin", label = "🔑 Admin" },
]
prompt = "User Permissions"
type = "multiselect"

View File

@ -0,0 +1,13 @@
let header = import "../05-fragments/header.ncl" in
let custom_border = import "../05-fragments/custom_border_section.ncl" in
{
name = "Custom Border Demo Form",
description = "Demonstrates custom border_top_char, border_top_len, border_bottom_char, border_bottom_len",
elements =
header.elements
@ custom_border.elements
@ [
{ type = "text", name = "project_name", prompt = "Project name", required = true },
{ type = "section", name = "footer", title = "✓ Complete!", content = "Thank you for filling out this form", border_top = true, border_top_char = "─", border_top_l = "┌", border_top_len = 40, border_top_r = "┐", border_bottom = true, border_bottom_char = "─", border_bottom_l = "└", border_bottom_len = 40, border_bottom_r = "┘", margin_left = 2 },
],
}

View File

@ -1,43 +0,0 @@
description = "Demonstrates custom border_top_char, border_top_len, border_bottom_char, border_bottom_len"
name = "Custom Border Demo Form"
# Standard border with default ═
[[elements]]
includes = ["fragments/header.toml"]
name = "header_group"
order = 1
type = "group"
# Custom border with different top and bottom styles
[[elements]]
includes = ["fragments/custom_border_section.toml"]
name = "custom_group"
order = 2
type = "group"
# Simple field
[[elements]]
name = "project_name"
order = 3
prompt = "Project name"
required = true
type = "text"
# Different border styles with corners and margin
[[elements]]
border_bottom = true
border_bottom_char = "─"
border_bottom_l = "└"
border_bottom_len = 40
border_bottom_r = "┘"
border_top = true
border_top_char = "─"
border_top_l = "┌"
border_top_len = 40
border_top_r = "┐"
content = "Thank you for filling out this form"
margin_left = 2
name = "footer"
order = 4
title = "✓ Complete!"
type = "section"

View File

@ -0,0 +1,20 @@
let fancy_border = import "../05-fragments/fancy_border_section.ncl" in
{
name = "Fancy Borders Demo",
description = "Demonstrates custom corner characters with fancy Unicode borders",
elements =
[
# Fancy bordered header - border at margin 0, content at margin 2
{ type = "section", name = "fancy_header", title = "✨ Welcome to Fancy Forms ✨", border_top = true, border_top_char = "─", border_top_l = "╭", border_top_len = 35, border_top_r = "╮", border_bottom = true, border_bottom_char = "─", border_bottom_l = "╰", border_bottom_len = 35, border_bottom_r = "╯", border_margin_left = 0, content_margin_left = 2 },
]
@ fancy_border.elements
@ [
{ type = "select", name = "favorite_style", prompt = "Your favorite border style", required = true, options = [
{ value = "Fancy Unicode", label = "✨ Fancy Unicode - Modern look" },
{ value = "Simple ASCII", label = "📝 Simple ASCII - Classic style" },
{ value = "Mixed Styles", label = "🎨 Mixed Styles - Custom borders" },
]
},
{ type = "section", name = "box_footer", title = "✓ All Done!", content = "Thanks for exploring fancy borders!", border_top = true, border_top_char = "─", border_top_l = "┌", border_top_len = 40, border_top_r = "┐", border_bottom = true, border_bottom_char = "─", border_bottom_l = "└", border_bottom_len = 40, border_bottom_r = "┘", border_margin_left = 0, content_margin_left = 2 },
],
}

View File

@ -1,61 +0,0 @@
description = "Demonstrates custom corner characters with fancy Unicode borders"
name = "Fancy Borders Demo"
# Fancy bordered header - border at margin 0, content at margin 2
[[elements]]
border_bottom = true
border_bottom_char = "─"
border_bottom_l = "╰"
border_bottom_len = 35
border_bottom_r = "╯"
border_margin_left = 0
border_top = true
border_top_char = "─"
border_top_l = "╭"
border_top_len = 35
border_top_r = "╮"
content_margin_left = 2
name = "fancy_header"
order = 1
title = "✨ Welcome to Fancy Forms ✨"
type = "section"
# Include fancy border fragment - margin settings are in the fragment items
[[elements]]
includes = ["fragments/fancy_border_section.toml"]
name = "fancy_group"
order = 2
type = "group"
# Simple field
[[elements]]
name = "favorite_style"
options = [
{ value = "Fancy Unicode", label = "✨ Fancy Unicode - Modern look" },
{ value = "Simple ASCII", label = "📝 Simple ASCII - Classic style" },
{ value = "Mixed Styles", label = "🎨 Mixed Styles - Custom borders" },
]
order = 3
prompt = "Your favorite border style"
required = true
type = "select"
# Box border for footer - border at 0, content at 2
[[elements]]
border_bottom = true
border_bottom_char = "─"
border_bottom_l = "└"
border_bottom_len = 40
border_bottom_r = "┘"
border_margin_left = 0
border_top = true
border_top_char = "─"
border_top_l = "┌"
border_top_len = 40
border_top_r = "┐"
content = "Thanks for exploring fancy borders!"
content_margin_left = 2
name = "box_footer"
order = 4
title = "✓ All Done!"
type = "section"

View File

@ -0,0 +1,85 @@
{
name = "User Experience Survey",
description = "Comprehensive TUI survey with visual elements and grouped sections",
elements = [
# Welcome header with fancy Unicode borders
{ type = "section", name = "welcome_header", title = "📋 User Experience Survey", content = "Help us improve by sharing your feedback", border_top = true, border_top_char = "─", border_top_l = "╭", border_top_len = 45, border_top_r = "╮", border_bottom = true, border_bottom_char = "─", border_bottom_l = "╰", border_bottom_len = 45, border_bottom_r = "╯", border_margin_left = 0, content_margin_left = 2 },
# Personal Information section header
{ type = "section_header", name = "personal_info_header", title = "👤 Personal Information", margin_left = 2 },
# Personal information fields
{ type = "text", name = "full_name", prompt = "Full name", required = true, placeholder = "Jane Smith", group = "personal_info" },
{ type = "text", name = "email", prompt = "Email address", required = true, placeholder = "jane@example.com", group = "personal_info" },
{ type = "text", name = "phone", prompt = "Phone number (optional)", placeholder = "+1-555-0123", group = "personal_info" },
# Product experience section
{ type = "section_header", name = "product_header", title = "⭐ Product Experience", margin_left = 2 },
{ type = "select", name = "overall_satisfaction", prompt = "Overall satisfaction with product", required = true, group = "product", options = [
{ value = "Very Unsatisfied", label = "😢 Very Unsatisfied" },
{ value = "Unsatisfied", label = "😟 Unsatisfied" },
{ value = "Neutral", label = "😐 Neutral" },
{ value = "Satisfied", label = "😊 Satisfied" },
{ value = "Very Satisfied", label = "😍 Very Satisfied" },
]
},
{ type = "select", name = "usage_frequency", prompt = "How often do you use this product?", required = true, group = "product", options = [
{ value = "Daily", label = "📅 Daily" },
{ value = "Weekly", label = "📊 Weekly" },
{ value = "Monthly", label = "📆 Monthly" },
{ value = "Occasionally", label = "📝 Occasionally" },
{ value = "Never", label = "🚫 Never" },
]
},
{ type = "multiselect", name = "features_used", prompt = "Which features do you use? (select all that apply)", page_size = 5, vim_mode = true, group = "product", options = [
{ value = "Dashboard", label = "📊 Dashboard" },
{ value = "Analytics", label = "📈 Analytics" },
{ value = "Reporting", label = "📑 Reporting" },
{ value = "API Integration", label = "🔌 API Integration" },
{ value = "Mobile App", label = "📱 Mobile App" },
{ value = "Notifications", label = "🔔 Notifications" },
{ value = "Collaboration Tools", label = "👥 Collaboration Tools" },
]
},
# Feedback section
{ type = "section_header", name = "feedback_header", title = "💬 Feedback", margin_left = 2 },
{ type = "editor", name = "improvements", prompt = "What improvements would you suggest?", file_extension = "txt", prefix_text = "# Please describe desired improvements\n", group = "feedback" },
{ type = "select", name = "biggest_pain_point", prompt = "What's your biggest pain point?", required = true, group = "feedback", options = [
{ value = "Performance issues", label = "⚡ Performance issues" },
{ value = "Confusing UI/UX", label = "🎨 Confusing UI/UX" },
{ value = "Missing features", label = "❌ Missing features" },
{ value = "Documentation", label = "📖 Documentation" },
{ value = "Customer support", label = "🆘 Customer support" },
{ value = "Pricing", label = "💰 Pricing" },
{ value = "Other", label = "❓ Other" },
]
},
# Preferences section
{ type = "section_header", name = "preferences_header", title = "⚙️ Preferences", margin_left = 2 },
{ type = "select", name = "contact_preference", prompt = "Preferred contact method", required = true, default = "Email", group = "preferences", options = [
{ value = "Email", label = "📧 Email" },
{ value = "Phone", label = "📞 Phone" },
{ value = "SMS", label = "💬 SMS" },
{ value = "In-app notification", label = "🔔 In-app notification" },
{ value = "No contact", label = "🚫 No contact" },
]
},
{ type = "confirm", name = "newsletter_opt_in", prompt = "Subscribe to our newsletter for updates?", default = false, group = "preferences" },
{ type = "confirm", name = "beta_features", prompt = "Interested in testing beta features?", default = false, group = "preferences" },
# Advanced options (conditional)
{ type = "multiselect", name = "device_types", prompt = "Which devices do you use? (optional)", page_size = 4, vim_mode = true, when = "beta_features == true", group = "preferences", options = [
{ value = "Desktop", label = "🖥️ Desktop" },
{ value = "Laptop", label = "💻 Laptop" },
{ value = "Tablet", label = "📱 Tablet" },
{ value = "Mobile", label = "📲 Mobile Phone" },
{ value = "Smartwatch", label = "⌚ Smartwatch" },
]
},
# Closing footer
{ type = "section", name = "closing_footer", title = "✓ Thank You!", content = "Your feedback helps us build better products", border_top = true, border_top_char = "─", border_top_l = "┌", border_top_len = 50, border_top_r = "┐", border_bottom = true, border_bottom_char = "─", border_bottom_l = "└", border_bottom_len = 50, border_bottom_r = "┘", border_margin_left = 0, content_margin_left = 2 },
],
}

View File

@ -1,226 +0,0 @@
description = "Comprehensive TUI survey with visual elements and grouped sections"
locale = "en-US"
name = "User Experience Survey"
# Welcome header with fancy Unicode borders
[[items]]
border_bottom = true
border_bottom_char = "─"
border_bottom_l = "╰"
border_bottom_len = 45
border_bottom_r = "╯"
border_margin_left = 0
border_top = true
border_top_char = "─"
border_top_l = "╭"
border_top_len = 45
border_top_r = "╮"
content = "Help us improve by sharing your feedback"
content_margin_left = 2
name = "welcome_header"
order = 1
title = "📋 User Experience Survey"
type = "section"
# Personal Information section header
[[items]]
margin_left = 2
name = "personal_info_header"
order = 2
title = "👤 Personal Information"
type = "section_header"
# Personal information fields
[[fields]]
group = "personal_info"
name = "full_name"
order = 3
placeholder = "Jane Smith"
prompt = "Full name"
required = true
type = "text"
[[fields]]
group = "personal_info"
name = "email"
order = 4
placeholder = "jane@example.com"
prompt = "Email address"
required = true
type = "text"
[[fields]]
group = "personal_info"
name = "phone"
order = 5
placeholder = "+1-555-0123"
prompt = "Phone number (optional)"
type = "text"
# Product experience section
[[items]]
margin_left = 2
name = "product_header"
order = 6
title = "⭐ Product Experience"
type = "section_header"
[[fields]]
group = "product"
name = "overall_satisfaction"
options = [
{ value = "Very Unsatisfied", label = "😢 Very Unsatisfied" },
{ value = "Unsatisfied", label = "😟 Unsatisfied" },
{ value = "Neutral", label = "😐 Neutral" },
{ value = "Satisfied", label = "😊 Satisfied" },
{ value = "Very Satisfied", label = "😍 Very Satisfied" },
]
order = 7
prompt = "Overall satisfaction with product"
required = true
type = "select"
[[fields]]
group = "product"
name = "usage_frequency"
options = [
{ value = "Daily", label = "📅 Daily" },
{ value = "Weekly", label = "📊 Weekly" },
{ value = "Monthly", label = "📆 Monthly" },
{ value = "Occasionally", label = "📝 Occasionally" },
{ value = "Never", label = "🚫 Never" },
]
order = 8
prompt = "How often do you use this product?"
required = true
type = "select"
[[fields]]
group = "product"
name = "features_used"
options = [
{ value = "Dashboard", label = "📊 Dashboard" },
{ value = "Analytics", label = "📈 Analytics" },
{ value = "Reporting", label = "📑 Reporting" },
{ value = "API Integration", label = "🔌 API Integration" },
{ value = "Mobile App", label = "📱 Mobile App" },
{ value = "Notifications", label = "🔔 Notifications" },
{ value = "Collaboration Tools", label = "👥 Collaboration Tools" },
]
order = 9
page_size = 5
prompt = "Which features do you use? (select all that apply)"
type = "multiselect"
vim_mode = true
# Feedback section
[[items]]
margin_left = 2
name = "feedback_header"
order = 10
title = "💬 Feedback"
type = "section_header"
[[fields]]
file_extension = "txt"
group = "feedback"
name = "improvements"
order = 11
prefix_text = "# Please describe desired improvements\n"
prompt = "What improvements would you suggest?"
type = "editor"
[[fields]]
group = "feedback"
name = "biggest_pain_point"
options = [
{ value = "Performance issues", label = "⚡ Performance issues" },
{ value = "Confusing UI/UX", label = "🎨 Confusing UI/UX" },
{ value = "Missing features", label = "❌ Missing features" },
{ value = "Documentation", label = "📖 Documentation" },
{ value = "Customer support", label = "🆘 Customer support" },
{ value = "Pricing", label = "💰 Pricing" },
{ value = "Other", label = "❓ Other" },
]
order = 12
prompt = "What's your biggest pain point?"
required = true
type = "select"
# Preferences section
[[items]]
margin_left = 2
name = "preferences_header"
order = 13
title = "⚙️ Preferences"
type = "section_header"
[[fields]]
default = "Email"
group = "preferences"
name = "contact_preference"
options = [
{ value = "Email", label = "📧 Email" },
{ value = "Phone", label = "📞 Phone" },
{ value = "SMS", label = "💬 SMS" },
{ value = "In-app notification", label = "🔔 In-app notification" },
{ value = "No contact", label = "🚫 No contact" },
]
order = 14
prompt = "Preferred contact method"
required = true
type = "select"
[[fields]]
default = "false"
group = "preferences"
name = "newsletter_opt_in"
order = 15
prompt = "Subscribe to our newsletter for updates?"
type = "confirm"
[[fields]]
default = "false"
group = "preferences"
name = "beta_features"
order = 16
prompt = "Interested in testing beta features?"
type = "confirm"
# Advanced options (conditional)
[[fields]]
group = "preferences"
name = "device_types"
options = [
{ value = "Desktop", label = "🖥️ Desktop" },
{ value = "Laptop", label = "💻 Laptop" },
{ value = "Tablet", label = "📱 Tablet" },
{ value = "Mobile", label = "📲 Mobile Phone" },
{ value = "Smartwatch", label = "⌚ Smartwatch" },
]
order = 17
page_size = 4
prompt = "Which devices do you use? (optional)"
type = "multiselect"
vim_mode = true
when = "beta_features == true"
# Closing footer
[[items]]
border_bottom = true
border_bottom_char = "─"
border_bottom_l = "└"
border_bottom_len = 50
border_bottom_r = "┘"
border_margin_left = 0
border_top = true
border_top_char = "─"
border_top_l = "┌"
border_top_len = 50
border_top_r = "┐"
content = "Your feedback helps us build better products"
content_margin_left = 2
name = "closing_footer"
order = 100
title = "✓ Thank You!"
type = "section"

View File

@ -0,0 +1,70 @@
{
name = "User Registration",
description = "Web-optimized registration form with account setup and preferences",
elements = [
# Account Credentials Section
{ type = "section_header", name = "account_section", title = "Create Your Account" },
{ type = "text", name = "username", prompt = "Username", required = true, placeholder = "Choose a unique username", group = "account" },
{ type = "text", name = "email", prompt = "Email Address", required = true, placeholder = "your.email@example.com", group = "account" },
{ type = "password", name = "password", prompt = "Password", required = true, group = "account" },
{ type = "password", name = "confirm_password", prompt = "Confirm Password", required = true, group = "account" },
# Profile Information Section
{ type = "section_header", name = "profile_section", title = "Profile Information" },
{ type = "text", name = "first_name", prompt = "First Name", required = true, placeholder = "Jane", group = "profile" },
{ type = "text", name = "last_name", prompt = "Last Name", required = true, placeholder = "Smith", group = "profile" },
{ type = "text", name = "display_name", prompt = "Display Name (optional)", placeholder = "How you'll appear to other users", group = "profile" },
{ type = "date", name = "birth_date", prompt = "Date of Birth", min_date = "1950-01-01", max_date = "2006-12-31", week_start = "Sun", group = "profile" },
{ type = "editor", name = "bio", prompt = "About Me", file_extension = "md", group = "profile" },
# Account Settings Section
{ type = "section_header", name = "settings_section", title = "Account Settings" },
{ type = "select", name = "account_type", prompt = "Account Type", required = true, default = "Personal", group = "settings", options = [
{ value = "Personal", label = "👤 Personal - Individual use" },
{ value = "Business", label = "💼 Business - Small team" },
{ value = "Organization", label = "🏢 Organization - Large team" },
]
},
{ type = "select", name = "subscription_plan", prompt = "Subscription Plan", required = true, default = "Free", group = "settings", options = [
{ value = "Free", label = "🆓 Free - Essential features" },
{ value = "Pro", label = "⭐ Pro - Advanced features" },
{ value = "Enterprise", label = "🏛️ Enterprise - Full suite" },
]
},
{ type = "text", name = "company_name", prompt = "Company Name", required = true, placeholder = "Your Organization", when = "account_type != Personal", group = "settings" },
{ type = "text", name = "company_url", prompt = "Company Website", placeholder = "https://example.com", when = "account_type != Personal", group = "settings" },
{ type = "confirm", name = "api_access", prompt = "Enable API Access?", default = false, when = "subscription_plan != Free", group = "settings" },
{ type = "custom", name = "team_members_count", prompt = "How many team members?", custom_type = "i32", default = "1", when = "subscription_plan == Enterprise", group = "settings" },
# Preferences Section
{ type = "section_header", name = "preferences_section", title = "Communication Preferences" },
{ type = "confirm", name = "notifications_email", prompt = "Send me email notifications?", default = true, group = "preferences" },
{ type = "confirm", name = "marketing_emails", prompt = "Subscribe to marketing emails and updates?", default = false, group = "preferences" },
{ type = "select", name = "notification_frequency", prompt = "Email notification frequency", default = "Daily Digest", when = "notifications_email == true", group = "preferences", options = [
{ value = "Immediate", label = "⚡ Immediate" },
{ value = "Daily Digest", label = "📅 Daily Digest" },
{ value = "Weekly Summary", label = "📊 Weekly Summary" },
{ value = "Monthly Summary", label = "📈 Monthly Summary" },
{ value = "Never", label = "🚫 Never" },
]
},
{ type = "multiselect", name = "interests", prompt = "Topics you're interested in:", page_size = 4, group = "preferences", options = [
{ value = "Product Updates", label = "🚀 Product Updates" },
{ value = "Security Alerts", label = "🔒 Security Alerts" },
{ value = "Performance Tips", label = "⚡ Performance Tips" },
{ value = "Community News", label = "👥 Community News" },
{ value = "Educational Content", label = "📚 Educational Content" },
{ value = "Exclusive Offers", label = "🎁 Exclusive Offers" },
]
},
# Privacy & Legal Section
{ type = "section_header", name = "legal_section", title = "Privacy & Legal" },
{ type = "confirm", name = "terms_accepted", prompt = "I agree to the Terms of Service", required = true, group = "legal" },
{ type = "confirm", name = "privacy_policy_accepted", prompt = "I have read and accept the Privacy Policy", required = true, group = "legal" },
{ type = "confirm", name = "data_processing", prompt = "I consent to data processing as described", required = true, group = "legal" },
{ type = "confirm", name = "enable_secondary_contact", prompt = "Add secondary contact information?", default = false, group = "legal" },
{ type = "text", name = "secondary_email", prompt = "Secondary Email Address", placeholder = "alternative@example.com", when = "enable_secondary_contact == true", group = "legal" },
{ type = "text", name = "recovery_phone", prompt = "Recovery Phone Number", placeholder = "+1-555-0123", when = "enable_secondary_contact == true", group = "legal" },
],
}

View File

@ -1,283 +0,0 @@
description = "Web-optimized registration form with account setup and preferences"
locale = "en-US"
name = "User Registration"
# Account Credentials Section
[[items]]
name = "account_section"
order = 1
title = "Create Your Account"
type = "section_header"
[[fields]]
group = "account"
name = "username"
order = 2
placeholder = "Choose a unique username"
prompt = "Username"
required = true
type = "text"
[[fields]]
group = "account"
name = "email"
order = 3
placeholder = "your.email@example.com"
prompt = "Email Address"
required = true
type = "text"
[[fields]]
group = "account"
name = "password"
order = 4
prompt = "Password"
required = true
type = "password"
[[fields]]
group = "account"
name = "confirm_password"
order = 5
prompt = "Confirm Password"
required = true
type = "password"
# Profile Information Section
[[items]]
name = "profile_section"
order = 10
title = "Profile Information"
type = "section_header"
[[fields]]
group = "profile"
name = "first_name"
order = 11
placeholder = "Jane"
prompt = "First Name"
required = true
type = "text"
[[fields]]
group = "profile"
name = "last_name"
order = 12
placeholder = "Smith"
prompt = "Last Name"
required = true
type = "text"
[[fields]]
group = "profile"
name = "display_name"
order = 13
placeholder = "How you'll appear to other users"
prompt = "Display Name (optional)"
type = "text"
[[fields]]
group = "profile"
max_date = "2006-12-31"
min_date = "1950-01-01"
name = "birth_date"
order = 14
prompt = "Date of Birth"
type = "date"
week_start = "Sun"
[[fields]]
file_extension = "md"
group = "profile"
name = "bio"
order = 15
prompt = "About Me"
type = "editor"
# Account Settings Section
[[items]]
name = "settings_section"
order = 20
title = "Account Settings"
type = "section_header"
[[fields]]
default = "Personal"
group = "settings"
name = "account_type"
options = [
{ value = "Personal", label = "👤 Personal - Individual use" },
{ value = "Business", label = "💼 Business - Small team" },
{ value = "Organization", label = "🏢 Organization - Large team" },
]
order = 21
prompt = "Account Type"
required = true
type = "select"
[[fields]]
default = "Free"
group = "settings"
name = "subscription_plan"
options = [
{ value = "Free", label = "🆓 Free - Essential features" },
{ value = "Pro", label = "⭐ Pro - Advanced features" },
{ value = "Enterprise", label = "🏛️ Enterprise - Full suite" },
]
order = 22
prompt = "Subscription Plan"
required = true
type = "select"
# Conditional fields for Business/Organization accounts
[[fields]]
group = "settings"
name = "company_name"
order = 23
placeholder = "Your Organization"
prompt = "Company Name"
required = true
type = "text"
when = "account_type != Personal"
[[fields]]
group = "settings"
name = "company_url"
order = 24
placeholder = "https://example.com"
prompt = "Company Website"
type = "text"
when = "account_type != Personal"
# Premium features (conditional on subscription)
[[fields]]
default = "false"
group = "settings"
name = "api_access"
order = 25
prompt = "Enable API Access?"
type = "confirm"
when = "subscription_plan != Free"
[[fields]]
custom_type = "i32"
default = "1"
group = "settings"
name = "team_members_count"
order = 26
prompt = "How many team members?"
type = "custom"
when = "subscription_plan == Enterprise"
# Preferences Section
[[items]]
name = "preferences_section"
order = 30
title = "Communication Preferences"
type = "section_header"
[[fields]]
default = "true"
group = "preferences"
name = "notifications_email"
order = 31
prompt = "Send me email notifications?"
type = "confirm"
[[fields]]
default = "false"
group = "preferences"
name = "marketing_emails"
order = 32
prompt = "Subscribe to marketing emails and updates?"
type = "confirm"
[[fields]]
default = "Daily Digest"
group = "preferences"
name = "notification_frequency"
options = [
{ value = "Immediate", label = "⚡ Immediate" },
{ value = "Daily Digest", label = "📅 Daily Digest" },
{ value = "Weekly Summary", label = "📊 Weekly Summary" },
{ value = "Monthly Summary", label = "📈 Monthly Summary" },
{ value = "Never", label = "🚫 Never" },
]
order = 33
prompt = "Email notification frequency"
type = "select"
when = "notifications_email == true"
[[fields]]
group = "preferences"
name = "interests"
options = [
{ value = "Product Updates", label = "🚀 Product Updates" },
{ value = "Security Alerts", label = "🔒 Security Alerts" },
{ value = "Performance Tips", label = "⚡ Performance Tips" },
{ value = "Community News", label = "👥 Community News" },
{ value = "Educational Content", label = "📚 Educational Content" },
{ value = "Exclusive Offers", label = "🎁 Exclusive Offers" },
]
order = 34
page_size = 4
prompt = "Topics you're interested in:"
type = "multiselect"
# Privacy & Legal Section
[[items]]
name = "legal_section"
order = 40
title = "Privacy & Legal"
type = "section_header"
[[fields]]
group = "legal"
name = "terms_accepted"
order = 41
prompt = "I agree to the Terms of Service"
required = true
type = "confirm"
[[fields]]
group = "legal"
name = "privacy_policy_accepted"
order = 42
prompt = "I have read and accept the Privacy Policy"
required = true
type = "confirm"
[[fields]]
group = "legal"
name = "data_processing"
order = 43
prompt = "I consent to data processing as described"
required = true
type = "confirm"
# Optional secondary contact
[[fields]]
default = "false"
group = "legal"
name = "enable_secondary_contact"
order = 44
prompt = "Add secondary contact information?"
type = "confirm"
[[fields]]
group = "legal"
name = "secondary_email"
order = 45
placeholder = "alternative@example.com"
prompt = "Secondary Email Address"
type = "text"
when = "enable_secondary_contact == true"
[[fields]]
group = "legal"
name = "recovery_phone"
order = 46
placeholder = "+1-555-0123"
prompt = "Recovery Phone Number"
type = "text"
when = "enable_secondary_contact == true"

View File

@ -0,0 +1,10 @@
{
elements = [
{ type = "section", name = "agreement_header", title = "✅ Terms & Conditions", border_top = true, margin_left = 2 },
{ type = "section", name = "agreement_info", content = "Please review and agree to our terms before proceeding", margin_left = 2 },
{ type = "confirm", name = "agree_terms", prompt = "I agree to the terms and conditions", required = true },
{ type = "confirm", name = "agree_privacy", prompt = "I agree to the privacy policy", required = true },
{ type = "confirm", name = "agree_marketing", prompt = "I consent to receive marketing communications", default = false },
{ type = "section", name = "agreement_footer", content = "Click submit to complete your registration", align = "center", border_bottom = true },
],
}

View File

@ -1,45 +0,0 @@
name = "agreement_fragment"
[[elements]]
border_top = true
margin_left = 2
name = "agreement_header"
order = 1
title = "✅ Terms & Conditions"
type = "section"
[[elements]]
content = "Please review and agree to our terms before proceeding"
margin_left = 2
name = "agreement_info"
order = 2
type = "section"
[[elements]]
name = "agree_terms"
order = 3
prompt = "I agree to the terms and conditions"
required = true
type = "confirm"
[[elements]]
name = "agree_privacy"
order = 4
prompt = "I agree to the privacy policy"
required = true
type = "confirm"
[[elements]]
default = "false"
name = "agree_marketing"
order = 5
prompt = "I consent to receive marketing communications"
type = "confirm"
[[elements]]
align = "center"
border_bottom = true
content = "Click submit to complete your registration"
name = "agreement_footer"
order = 6
type = "section"

Some files were not shown because too many files have changed in this diff Show More