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
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:
parent
baff1c42ec
commit
a963adbf5b
23
CHANGELOG.md
23
CHANGELOG.md
@ -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**
|
||||
|
||||
39
README.md
39
README.md
@ -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
86
assets/web/README.md
Normal 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
|
||||
1
assets/web/architecture-diagram.html
Normal file
1
assets/web/architecture-diagram.html
Normal 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">☾</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);" >← 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
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
101
assets/web/minify.sh
Executable 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 ""
|
||||
173
assets/web/src/architecture-diagram.html
Normal file
173
assets/web/src/architecture-diagram.html
Normal 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">☾</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);"
|
||||
>← 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
807
assets/web/src/index.html
Normal 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">☾</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á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ón type-safe. Desde prompts interactivos hasta generació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ó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ón Nickel o TOML controla CLI, TUI, Web y AI. Sin código de formularios por backend. El schema es la fuente ú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ó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ó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ón">Hidden I/O in Execution</h3>
|
||||
<p data-en="Fragment loading happens once at load_form(). The three-phase executor is pure — 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 — sin acceso al filesystem, sin efectos secundarios durante la interacción."
|
||||
>Fragment loading happens once at load_form(). The three-phase executor is pure — 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ó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ómo Funciona"
|
||||
>How It Works</span>
|
||||
</h2>
|
||||
<div class="features-grid">
|
||||
|
||||
<div class="feature-box">
|
||||
<div class="feature-icon"><></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ón type-safe. TOML provee simplicidad sin dependencias. Ambos producen structs FormDefinition idé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">⚙</div>
|
||||
<h3 class="feature-title" style="color: #6366f1;"
|
||||
data-en="Three-Phase Execution"
|
||||
data-es="Ejecució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ó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">🔌</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<dyn FormBackend>. Auto-detection via TYPEDIALOG_BACKEND env var with CLI fallback."
|
||||
data-es="Feature gates en tiempo de compilación (#[cfg(feature)]) eliminan código muerto. Match runtime de BackendType despacha a Box<dyn FormBackend>. Auto-detección via env TYPEDIALOG_BACKEND con fallback CLI."
|
||||
>Compile-time feature gates (#[cfg(feature)]) eliminate dead backend code. Runtime BackendType match dispatches to Box<dyn FormBackend>. 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">🤖</div>
|
||||
<h3 class="feature-title" style="color: #a855f7;"
|
||||
data-en="AI & Agent Backends"
|
||||
data-es="Backends AI y Agent"
|
||||
>AI & 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úsqueda semá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">☁</div>
|
||||
<h3 class="feature-title" style="color: #10b981;"
|
||||
data-en="Infrastructure Generation"
|
||||
data-es="Generació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 → Constraints → Values → Validators → Schemas → Defaults → JSON."
|
||||
data-es="Prov-Gen transforma respuestas de formularios en configuraciones IaC. 6 proveedores cloud (AWS, GCP, Azure, Hetzner, UpCloud, LXD). Validación de 7 capas: Forms → Constraints → Values → Validators → Schemas → Defaults → JSON."
|
||||
>Prov-Gen transforms form answers into IaC configurations. 6 cloud providers (AWS, GCP, Azure, Hetzner, UpCloud, LXD). 7-layer validation: Forms → Constraints → Values → Validators → Schemas → Defaults → JSON.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-box" style="border-left-color: #f59e0b;">
|
||||
<div class="feature-icon">🔄</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ó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 — interactive prompts"
|
||||
data-es="inquire — prompts interactivos"
|
||||
>inquire — 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 — terminal UI"
|
||||
data-es="ratatui — interfaz de terminal"
|
||||
>ratatui — 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 — HTTP forms"
|
||||
data-es="axum — formularios HTTP"
|
||||
>axum — 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 & embeddings"
|
||||
data-es="RAG y embeddings"
|
||||
>RAG & 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ó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ó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á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 →"
|
||||
data-es="Explorar en GitHub →"
|
||||
>Explore on GitHub →</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á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ó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>
|
||||
253
assets/web/src/typedialog_architecture.svg
Normal file
253
assets/web/src/typedialog_architecture.svg
Normal 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&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() — 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: 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 & 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<dyn FormBackend></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<String, Value></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 |
229
assets/web/src/typedialog_architecture_white.svg
Normal file
229
assets/web/src/typedialog_architecture_white.svg
Normal 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&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 & 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<dyn FormBackend></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<String, Value></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 |
253
assets/web/typedialog_architecture.svg
Normal file
253
assets/web/typedialog_architecture.svg
Normal 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&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() — 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: 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 & 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<dyn FormBackend></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<String, Value></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 |
229
assets/web/typedialog_architecture_white.svg
Normal file
229
assets/web/typedialog_architecture_white.svg
Normal 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&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 & 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<dyn FormBackend></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<String, Value></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 |
58
assets/web/typedialog_logo_h.svg
Normal file
58
assets/web/typedialog_logo_h.svg
Normal 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&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
33
config/ag/config.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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
14
config/ag/dev.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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
29
config/ag/production.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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
|
||||
@ -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
4
config/ag/server.ncl
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
host | default = "127.0.0.1",
|
||||
port | default = 8765,
|
||||
}
|
||||
29
config/ai/config.ncl
Normal file
29
config/ai/config.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
18
config/ai/dev.ncl
Normal 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,
|
||||
}
|
||||
@ -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
27
config/ai/production.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
27
config/cli/config.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
18
config/cli/dev.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
25
config/cli/production.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
|
||||
32
config/prov-gen/config.ncl
Normal file
32
config/prov-gen/config.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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
27
config/prov-gen/dev.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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"
|
||||
33
config/prov-gen/production.ncl
Normal file
33
config/prov-gen/production.ncl
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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
39
config/tui/config.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
16
config/tui/dev.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
21
config/tui/production.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
39
config/web/config.ncl
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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
28
config/web/dev.ncl
Normal 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",
|
||||
},
|
||||
}
|
||||
@ -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
42
config/web/production.ncl
Normal 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",
|
||||
},
|
||||
}
|
||||
@ -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"
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,6 +204,7 @@ mod tests {
|
||||
week_start: None,
|
||||
order: 0,
|
||||
when: None,
|
||||
when_false: Default::default(),
|
||||
i18n: None,
|
||||
group: None,
|
||||
nickel_contract: None,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::{
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
21
docs/adr/README.md
Normal 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
|
||||
146
docs/adr/adr-001-nickel-form-definition.md
Normal file
146
docs/adr/adr-001-nickel-form-definition.md
Normal 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.
|
||||
@ -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
|
||||
```
|
||||
|
||||
25
examples/01-basic/base_form.ncl
Normal file
25
examples/01-basic/base_form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
8
examples/01-basic/debug_simple.ncl
Normal file
8
examples/01-basic/debug_simple.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
25
examples/01-basic/form.ncl
Normal file
25
examples/01-basic/form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
50
examples/01-basic/form_with_grouped_items.ncl
Normal file
50
examples/01-basic/form_with_grouped_items.ncl
Normal 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" },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
61
examples/01-basic/form_with_sections.ncl
Normal file
61
examples/01-basic/form_with_sections.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
59
examples/02-advanced/conditional_form.ncl
Normal file
59
examples/02-advanced/conditional_form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
75
examples/02-advanced/conditional_sections.ncl
Normal file
75
examples/02-advanced/conditional_sections.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
35
examples/02-advanced/display_items_demo.ncl
Normal file
35
examples/02-advanced/display_items_demo.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
49
examples/02-multiselect-display-modes/form.ncl
Normal file
49
examples/02-multiselect-display-modes/form.ncl
Normal 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" },
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
13
examples/03-styling/custom_border_form.ncl
Normal file
13
examples/03-styling/custom_border_form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
20
examples/03-styling/fancy_borders_form.ncl
Normal file
20
examples/03-styling/fancy_borders_form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
85
examples/04-backends/tui/tui_survey_form.ncl
Normal file
85
examples/04-backends/tui/tui_survey_form.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
70
examples/04-backends/web/web_registration_form.ncl
Normal file
70
examples/04-backends/web/web_registration_form.ncl
Normal 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" },
|
||||
],
|
||||
}
|
||||
@ -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"
|
||||
10
examples/05-fragments/agreement_section.ncl
Normal file
10
examples/05-fragments/agreement_section.ncl
Normal 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 },
|
||||
],
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user