agentleFS
violit-devviolit-dev/violit/doc/llms.txt

violit / doc

llms.txt474 starsChanged 6 months agoMITView on GitHub

Violit ≈ Streamlit-like authoring + fine-grained reactive updates + built-in app stack If you know Streamlit, start there, but do not assume Violit is only a widget-compatible clone. Violit also has built-in ORM, auth, Tailwind-first styling hooks, background jobs, and interval timers.

  • Reads credentials

We split this llms.txt into the rules it gives an agent

74 rules. About 0 are common, 7 are distinctive, and 67 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 67

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Always create app = vl.App(...) before using widgets.

    line 19
  2. Very specific:

    Always end runnable examples with app.run().

    line 20
  3. Very specific:

    Prefer showing run commands as violit run app.py ....

    line 21
  4. Very specific:

    Prefer app.state() over ad-hoc globals or session_state-style dict code.

    line 22
  5. Very specific:

    Use lambda: when formatting state.value inside strings.

    line 27
  6. Very specific:

    Do not generate @app.fragment.

    line 30
  7. Very specific:

    After defining a @app.reactivity function, call it once to register/render it.

    line 31
  8. Very specific:

    Use app.text_input(..., type="password") for password fields.

    line 34
  9. Very specific:

    Do not write if app.button(...): ....

    line 53
  10. Very specific:

    Do not assume Streamlit's rerun model exists in Violit.

    line 54
  11. Very specific:

    Do not default to st.session_state-style patterns.

    line 55
  12. Very specific:

    Do not generate @app.fragment or app.fragment(...).

    line 56
  13. Very specific:

    Do not define @app.reactivity blocks and forget to call them.

    line 57
  14. Very specific:

    Do not use @app.reactivity when a direct State pass, lambda:, app.If(), or app.For() already solves the problem more simply.

    line 58
  15. Very specific:

    Do not initialize app.selectbox(...) with value=.

    line 59
  16. Distinctive:

    Do not treat styling as UnoCSS-first or invent framework-specific CSS DSLs.

    line 60
  17. Very specific:

    Do not assume ORM and auth require external integration first.

    line 61
  18. Very specific:

    Do not generate app.password_input(...).

    line 62
  19. Very specific:

    Do not assume on_change receives a State.

    line 63
  20. Distinctive:

    Do not assume sidebar content is static or outside the reactive system.

    line 64
  21. Very specific:

    Do not hand-roll <a download> or custom JS download snippets when app.download_button(...) already solves it.

    line 65
  22. Very specific:

    Do not hand-roll modal HTML when @app.dialog(...) fits the task.

    line 66
  23. Distinctive:

    Do not hardcode API keys or other secrets into examples intended for GitHub or docs.

    line 67
  24. Very specific:

    Do not generate app.chat_messages(...).

    line 68
  25. Very specific:

    Do not pass messages=... to primitive app.chat_input(...).

    line 69
  26. Very specific:

    Do not put app.chat_input(...) inside a reactive block when its props are static.

    line 70
  27. Very specific:

    Do not manually append both user and assistant messages when using app.managed_chat_input(messages=..., on_submit=...).

    line 71
  28. Very specific:

    Do not mix manual append_message(...) / replace_last_message(...) transcript mutation with managed_chat_input(...) for the same turn.

    line 72
  29. Very specific:

    Do not invent alternate agent fields such as steps, tool_calls, or reasoning_text when Violit already understands phase, status_text, …

    line 73
  30. Very specific:

    Do not forget that primitive attachment/audio submissions arrive as a dict with text, files, and audio keys when enabled.

    line 74
  31. Very specific:

    Do not manually reimplement streamed chunk rendering inside chat bubbles when app.render_chat_message_body(...) already handles text, …

    line 75
  32. Very specific:

    Must create app = vl.App(...) instance first

    line 97
  33. Very specific:

    All widgets are called on app (e.g., app.title() not st.title())

    line 98
  34. Very specific:

    Must call app.run() at the end of the script

    line 99
  35. Very specific:

    Use lambda: when you need f-string formatting or complex logic

    line 214
  36. Very specific:

    Use .value only in callbacks/calculations, not in widget arguments

    line 215
  37. Very specific:

    When the number of rendered widgets changes with state, do not rely on a top-level Python for loop over state.value.

    line 219
  38. Very specific:

    Use app.For(...), app.If(...), or @app.reactivity so Violit can rerender that block correctly.

    line 220
  39. Very specific:

    Use direct State passing, operators, lambda:, app.If(), or app.For() first.

    line 235
  40. Very specific:

    Use @app.reactivity when you need a reactive block with normal Python control flow, especially if / for logic that should rerender as a …

    line 237
  41. Very specific:

    Prefer @app.reactivity over deprecated @app.fragment

    line 253
  42. Distinctive:

    Use it for grouped reactive control flow, not for every simple text binding

    line 255
  43. Very specific:

    Do not generate that pattern in new Violit code.

    line 270
  44. Distinctive:

    Prefer subscriptions for logging, persistence, analytics, or background coordination, not for normal UI redraws.

    line 294
  45. Very specific:

    All return State objects. Access current value via .value.

    line 333
  46. Very specific:

    When on_submit is provided, text_input(...) submits on Enter by default.

    line 354
  47. Very specific:

    Use app.form(...) for grouped submit flows.

    line 373
  48. Very specific:

    Use app.form_submit_button(...) inside the form instead of a plain button when you want form submission semantics.

    line 374
  49. Very specific:

    Use download_button, link_button, and page_link before inventing custom HTML anchors.

    line 376
  50. Very specific:

    Use on_click= to define what happens when clicked.

    line 399
  51. Very specific:

    Use columns(..., justify=...) for top/center/bottom placement of each column's child box.

    line 482
  52. Very specific:

    Use columns(..., align=...) for start/center/end placement across the horizontal axis inside each column.

    line 483
  53. Very specific:

    Use equal_height=True when vertical placement needs real extra height to be visible.

    line 484
  54. Very specific:

    Use container(fill_height=True, justify=...) when only one side of a row needs local vertical adjustment.

    line 485
  55. Very specific:

    Use widget-native options like label_position and label_visibility before adding extra layout wrappers around forms.

    line 487
  56. Very specific:

    Default button placement should assume full-width behavior in normal vertical layout containers, including inside app.expander(...).

    line 488
  57. Very specific:

    If a widget should be narrower than the container, prefer an explicit style="width: ...;" override instead of assuming special-case layout …

    line 489
  58. Very specific:

    When a widget label, title, or summary depends on state, pass the State object directly or use lambda:.

    line 490
  59. Very specific:

    If exact visual alignment matters, add a shared inner frame with the same width/min-height on both sides, or apply small cls / style …

    line 494
  60. Very specific:

    Use messages = app.state([...]) and pass it to app.chat_history(...) or app.agent_history(...) plus app.managed_chat_input(...).

    line 625
  61. Very specific:

    Keep app.chat_history(...) or app.agent_history(...) inside @app.reactivity when you want only the transcript to rerender.

    line 628
  62. Very specific:

    Keep app.managed_chat_input(...) outside the reactive block when its props are static.

    line 629
  63. Distinctive:

    Use manual rendering when you need per-turn control, custom placeholders, explicit background orchestration, or you want to surface …

    line 742
  64. Very specific:

    Use chat_thread() and chat_message() when you want custom message composition but still want Violit's built-in chat styling.

    line 756
  65. Very specific:

    Start with cls, then use style, add_css(), configure_widget(), and part_cls when you need more control.

    line 818
  66. Very specific:

    Use these as intentional fine-tuning, not as a substitute for fixing the parent columns(...) / container(...) structure first.

    line 860
  67. Distinctive:

    Use these instead of inventing your own thread/timer scaffolding unless you have a strong reason not to.

    line 864
  68. Very specific:

    Use @app.dialog(...) when you need a modal.

    line 918
  69. Very specific:

    Only a random session ID is sent to the browser via ss_sid cookie (HttpOnly)

    line 1354
  70. Very specific:

    Prefer violit run ... in docs, examples, and generated instructions.

    line 1401
  71. Very specific:

    Use --localhost for local-only development.

    line 1402
  72. Very specific:

    Use --host 0.0.0.0 only when you intentionally want LAN or server access.

    line 1403
  73. Very specific:

    Use --lite when you want HTMX-based HTTP updates instead of the default WebSocket runtime.

    line 1404
  74. Very specific:

    Use --native, --debug, --on-top, and --nosplash for desktop app flows.

    line 1405

≈ “Common” is an estimate: at least 1 in 5 of the 3,225 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
llms.txt
# Violit — AI/LLM Code Generation Reference

> **Violit ≈ Streamlit-like authoring + fine-grained reactive updates + built-in app stack**
> If you know Streamlit, start there, but do not assume Violit is only a widget-compatible clone.
> Violit also has built-in ORM, auth, Tailwind-first styling hooks, background jobs, and interval timers.

---

## Quick Mental Model

```
Streamlit: st.xxx()        → Violit: app.xxx()
st.session_state            → app.state() / app.session_state()
Full script rerun on change → Only affected widgets update (Zero Rerun)
```

## High-Priority Rules For Code Generation

- Always create `app = vl.App(...)` before using widgets.
- Always end runnable examples with `app.run()`.
- Prefer showing run commands as `violit run app.py ...`; `python app.py ...` still works, but the CLI is now the recommended default.
- Prefer `app.state()` over ad-hoc globals or `session_state`-style dict code.
- Remember that `app.state()` is view-local by default. Use `session_state`, `app_state`, and `shared_state` when wider sharing is intentional.
- Pass `State` objects directly to widgets when you want reactivity.
- Input `on_change` callbacks receive the new decoded value directly, not a `State` object.
- `selectbox(...)` uses index-based initial selection semantics. Prefer `index=` for the initial choice; do not invent `value=` for it.
- Use `lambda:` when formatting `state.value` inside strings.
- For complex reactive blocks, prefer `@app.reactivity`.
- For reactive list or conditional rendering driven by changing state, prefer `app.For(...)` and `app.If(...)` before writing manual Python loops around `state.value`.
- Do not generate `@app.fragment`; `fragment` is deprecated.
- After defining a `@app.reactivity` function, call it once to register/render it.
- Buttons do not return booleans. Use `on_click=`.
- `State.subscribe(...)` and `@state.on_change` are for side effects such as logging or persistence, not for normal UI refreshes.
- Use `app.text_input(..., type="password")` for password fields; do not invent `app.password_input(...)`.
- For grouped submit UIs, prefer `with app.form(...):` + `app.form_submit_button(...)`.
- For navigation from callbacks, prefer `app.switch_page(...)`.
- For downloads and links, prefer `app.download_button(...)`, `app.link_button(...)`, and `app.page_link(...)` over hand-written HTML.
- For public examples that need API keys, never hardcode secrets. Prefer `app.text_input(..., type="password")` or environment loading only if the user explicitly asks for it.
- Sidebar content is reactive like main content. Use `State` and `lambda:` normally inside `with app.sidebar:`.
- For plain LLM chat apps, prefer `app.chat_history(...)` + `app.managed_chat_input(messages=..., on_submit=...)` over inventing a custom chat stack.
- For agent-oriented apps, prefer `app.agent_history(...)` + `app.managed_chat_input(messages=..., on_submit=...)`.
- For custom chat transcript layout, prefer `app.chat_thread(...)` + `with app.chat_message(...):` over raw HTML string assembly.
- For primitive custom chat transcripts, prefer `app.render_chat_message_body(...)`; for primitive agent transcripts, use `app.agent_turn(...)` and the shared agent message schema.
- For database apps, prefer `vl.App(db="./app.db")` and `app.db` CRUD.
- For auth, prefer `app.setup_auth(User)` instead of inventing a custom login stack.
- For styling, start with `cls`. Use `style`, `configure_widget()`, `add_css()`, and `part_cls` only as needed.
- For runtime theming and UX toggles, prefer `app.set_theme(...)`, `app.set_animation_mode(...)`, `app.set_primary_color(...)`, and `app.set_selection_mode(...)`.
- For long-running work, prefer `app.background(...)`.
- For polling and live tickers, prefer `app.interval(...)`.

## Common Hallucinations To Avoid

- Do not write `if app.button(...): ...`.
- Do not assume Streamlit's rerun model exists in Violit.
- Do not default to `st.session_state`-style patterns.
- Do not generate `@app.fragment` or `app.fragment(...)`; it is deprecated.
- Do not define `@app.reactivity` blocks and forget to call them.
- Do not use `@app.reactivity` when a direct `State` pass, `lambda:`, `app.If()`, or `app.For()` already solves the problem more simply.
- Do not initialize `app.selectbox(...)` with `value=`; use `index=` for the initial option.
- Do not treat styling as UnoCSS-first or invent framework-specific CSS DSLs.
- Do not assume ORM and auth require external integration first; Violit already has built-in paths.
- Do not generate `app.password_input(...)`; use `app.text_input(..., type="password")`.
- Do not assume `on_change` receives a `State`; it receives the current decoded value.
- Do not assume sidebar content is static or outside the reactive system.
- Do not hand-roll `<a download>` or custom JS download snippets when `app.download_button(...)` already solves it.
- Do not hand-roll modal HTML when `@app.dialog(...)` fits the task.
- Do not hardcode API keys or other secrets into examples intended for GitHub or docs.
- Do not generate `app.chat_messages(...)`; the high-level transcript renderers are `app.chat_history(...)` and `app.agent_history(...)`.
- Do not pass `messages=...` to primitive `app.chat_input(...)`; `messages=` belongs to `app.managed_chat_input(...)`.
- Do not put `app.chat_input(...)` inside a reactive block when its props are static.
- Do not manually append both user and assistant messages when using `app.managed_chat_input(messages=..., on_submit=...)`; return the assistant reply or stream instead.
- Do not mix manual `append_message(...)` / `replace_last_message(...)` transcript mutation with `managed_chat_input(...)` for the same turn.
- Do not invent alternate agent fields such as `steps`, `tool_calls`, or `reasoning_text` when Violit already understands `phase`, `status_text`, `summary`, `trace`, `artifacts`, and `error`.
- Do not forget that primitive attachment/audio submissions arrive as a dict with `text`, `files`, and `audio` keys when enabled.
- Do not manually reimplement streamed chunk rendering inside chat bubbles when `app.render_chat_message_body(...)` already handles text, chunks, and default attachment previews.

---

## 1. Boilerplate (Streamlit vs Violit)

```python
# ❌ Streamlit
import streamlit as st
st.title("Hello")
st.write("World")

# ✅ Violit
import violit as vl
app = vl.App(title="Hello", theme="ocean")
app.title("Hello")
app.write("World")
app.run()   # REQUIRED at the end
```

**Key differences:**
- `import violit as vl` (not `import streamlit as st`)
- Must create `app = vl.App(...)` instance first
- All widgets are called on `app` (e.g., `app.title()` not `st.title()`)
- Must call `app.run()` at the end of the script

**App constructor options:**
```python
app = vl.App(
    title="My App",             # Browser/window title
    theme="ocean",              # Theme preset (dark, light, ocean, cyberpunk, etc.)
    container_width="800px",    # Content max-width ("none" for full-width)
    mode="ws",                  # "ws" (WebSocket, default) or "lite" (HTMX; often better for higher concurrency)
)
```

**Running (recommended):**
```bash
violit run app.py                             # Web mode (default)
violit run app.py --reload --localhost        # Hot reload on localhost
violit run app.py --lite --localhost          # HTMX-based Lite mode; often better for higher concurrency
violit run app.py --native                    # Desktop app mode (pywebview)
violit run app.py --host 0.0.0.0 --port 8020 # LAN/server bind
violit run app.py --help                      # Show runtime flags
```

`python app.py [args]` still works, but `violit run ...` is the preferred default for generated docs and examples.

---

## 2. State — The Core Difference

Streamlit uses `st.session_state` dict + full rerun. Violit uses reactive `State` objects.

```python
# ❌ Streamlit
if "count" not in st.session_state:
    st.session_state.count = 0
if st.button("Click"):
    st.session_state.count += 1
st.write(st.session_state.count)

# ✅ Violit
count = app.state(0)
app.button("Click", on_click=lambda: count.set(count.value + 1))
app.write(count)  # Auto-updates when count changes
```

`app.state()` is the default, view-local state. Violit also supports wider scopes when you need sharing beyond the current browser view.

### State API

```python
# Create
count = app.state(0)                       # Default: current view only
name = app.state("", key="user_name")    # Explicit key

# Recommended explicit wrappers for wider sharing
theme = app.session_state("light", key="theme")
online = app.app_state(0, key="online_users")
messages = app.shared_state([], key="messages", namespace="room:lobby")

# Raw low-level form (same engine underneath)
draft = app.state("", key="draft", scope="view")
room = app.state([], key="messages", scope="shared", namespace="room:lobby")

# Read
count.value    # → 0
count()        # → 0  (shorthand)

# Write
count.set(5)       # Preferred in callbacks
count.value = 5    # Also works

# ⚠️ NEVER reassign the variable
count = 5  # ❌ WRONG — State object is lost!
```

### State Scopes

```python
draft = app.state("", key="draft")
theme = app.session_state("light", key="theme")
online_count = app.app_state(0, key="online_count")
room_messages = app.shared_state([], key="messages", namespace="room:lobby")
```

- `app.state(...)` / `app.view_state(...)`: current browser view only. Best for page-local UI state.
- `app.session_state(...)`: shared across tabs in the same browser session.
- `app.app_state(...)`: shared across all users in the current Violit process.
- `app.shared_state(..., namespace=...)`: shared across all users connected to the same named room / board / workspace.

Important scope notes:

- `app_state()` and `shared_state()` are process-local memory. They do not automatically sync across multiple workers or servers.
- `shared_state()` is a collaboration primitive, not an auth boundary. Your app still decides who may enter or write a namespace.
- App/shared values have a size safety limit, and inactive shared namespaces are cleaned up automatically.

### Passing State to Widgets (Reactivity)

```python
count = app.state(0)
name = app.state("World")

# ✅ Reactive — auto-updates when state changes
app.text(count)                            # State object directly
app.text(count * 2)                        # Operator overloading → ComputedState
app.text("Hello, " + name + "!")           # String concatenation
app.text(lambda: f"Count: {count.value}")  # Lambda for complex formatting
app.metric("Total", count)                 # State in any widget

# ❌ NOT reactive — value is frozen at call time
app.text(count.value)                      # Just passes int 0
app.text(f"Count: {count.value}")          # Just passes string "Count: 0"
```

**Rule of thumb:**
- Pass State **object** to widgets → reactive
- Pass `state.value` to widgets → frozen (not reactive)
- Use `lambda:` when you need f-string formatting or complex logic
- Use `.value` only in callbacks/calculations, not in widget arguments

### Reactive Lists and Conditional Blocks

When the number of rendered widgets changes with state, do not rely on a top-level Python `for` loop over `state.value`.
Use `app.For(...)`, `app.If(...)`, or `@app.reactivity` so Violit can rerender that block correctly.

```python
messages = app.shared_state([], key="messages", namespace="room:lobby")

app.For(
    lambda: messages.value,
    render=lambda item, index: app.text(f"{index + 1}. {item}"),
    empty=lambda: app.caption("No messages yet."),
)
```

### Complex Reactivity: `@app.reactivity`

Most reactivity in Violit does **not** need a decorator.
Use direct `State` passing, operators, `lambda:`, `app.If()`, or `app.For()` first.

Use `@app.reactivity` when you need a **reactive block with normal Python control flow**, especially `if` / `for` logic that should rerender as a unit.

```python
count = app.state(0)

@app.reactivity
def summary_block():
    if count.value > 5:
        app.success("Big!")
    else:
        app.info("Small")

summary_block()  # REQUIRED: call once to register and render
```

**Important rules:**
- Prefer `@app.reactivity` over deprecated `@app.fragment`
- The decorated function must be called once after definition
- Use it for grouped reactive control flow, not for every simple text binding
- For simple output, `app.text(state)` or `app.text(lambda: ...)` is usually better

**Decorator vs context manager:**
- `@app.reactivity`: preferred for a function-wrapped reactive block and partial rerender of that block
- `with app.reactivity():`: valid, but think of it as an inline reactive scope; use only when the inline form is clearly better

**Deprecated pattern:**

```python
@app.fragment
def old_block():
    app.text("Do not generate this")
```

Do not generate that pattern in new Violit code.

### State Side Effects

State already updates dependent widgets automatically. Use subscriptions only when you need side effects outside normal rendering.

```python
count = app.state(0)

def log_change(new_value, old_value):
    print(f"count changed: {old_value} -> {new_value}")

subscription = count.subscribe(log_change)

@count.on_change
def _(new_value):
    print("new count:", new_value)

# Later, if needed
subscription.cancel()
```

- `subscribe(callback)` supports `callback(new_value)` or `callback(new_value, old_value)`.
- `@state.on_change` is decorator sugar for `subscribe(...)`.
- Prefer subscriptions for logging, persistence, analytics, or background coordination, not for normal UI redraws.

---

## 3. Input Widgets — Return State, Not Values

In Streamlit, input widgets return the current value directly.
In Violit, input widgets return a **State object**.

```python
# ❌ Streamlit
name = st.text_input("Name")  # name is str
if st.button("Greet"):
    st.write(f"Hello {name}")

# ✅ Violit
name = app.text_input("Name")  # name is State[str]
app.button("Greet", on_click=lambda: app.toast(f"Hello {name.value}"))
app.text("Hello, " + name)     # Reactive display
```

### All Input Widgets (same names as Streamlit)

```python
name     = app.text_input("Name", value="default")
bio      = app.text_area("Bio", height=5)
age      = app.number_input("Age", value=0, min_value=0, max_value=120, step=1)
score    = app.slider("Score", min_value=0, max_value=100, value=50)
agree    = app.checkbox("I agree", value=False)
dark     = app.toggle("Dark mode", value=False)
size     = app.radio("Size", options=["S", "M", "L"], index=0)
lang     = app.selectbox("Language", options=["Python", "JS"], index=0)
tags     = app.multiselect("Tags", options=["A", "B", "C"], default=["A"])
color    = app.color_picker("Color", value="#ff0000")
date     = app.date_input("Date")
time     = app.time_input("Time")
file     = app.file_uploader("Upload", accept=".csv,.txt", multiple=False)
```

All return `State` objects. Access current value via `.value`.

### Important Input Details

```python
query = app.text_input(
    "Search",
    placeholder="Type and press Enter",
    on_change=lambda value: app.debug_print(value),
    on_submit=lambda value: app.toast(f"Searching: {value}"),
    help="Press Enter to submit",
)

secret = app.text_input("Password", type="password")
priority = app.select_slider("Priority", options=["Low", "Medium", "High"])
files = app.file_uploader("Upload files", multiple=True, accept=".csv,.txt")
```

- `on_change` callbacks receive the new value directly.
- `selectbox(...)` should be initialized with `index=...`, not `value=...`.
- `text_input(...)` supports `on_submit`, `type="password"`, `label_visibility`, and `help`.
- When `on_submit` is provided, `text_input(...)` submits on Enter by default.
- `slider(..., live_update=True)` updates continuously while dragging.
- `select_slider(...)` is for discrete options, not numeric ranges.
- `file_uploader(...)` returns `UploadedFile` objects with `.name`, `.type`, `.size`, and `.read()` support.

### Forms, Links, and File Actions

```python
with app.form("login", clear_on_submit=False, enter_to_submit=True):
    username = app.text_input("Username")
    password = app.text_input("Password", type="password")
    app.form_submit_button("Sign in", on_click=do_login)

app.download_button("Export CSV", data=csv_text, file_name="report.csv", mime="text/csv")
app.link_button("Open Docs", "https://doc.violit.cloud")
app.page_link("Settings", label="Go to Settings")
app.button("Back Home", on_click=lambda: app.switch_page("Home"))
```

- Use `app.form(...)` for grouped submit flows.
- Use `app.form_submit_button(...)` inside the form instead of a plain button when you want form submission semantics.
- `app.switch_page(...)` accepts a `Page` object, page function, page title, url path slug, or hash string.
- Use `download_button`, `link_button`, and `page_link` before inventing custom HTML anchors.

---

## 4. Button — on_click Pattern (NOT if-block)

```python
# ❌ Streamlit pattern (if-block triggers rerun)
if st.button("Save"):
    save_data()

# ✅ Violit pattern (on_click callback, no rerun)
app.button("Save", on_click=save_data)
app.button("Save", on_click=lambda: save_data())

# Button variants
app.button("OK", variant="primary")       # default
app.button("Cancel", variant="neutral")
app.button("Delete", variant="danger")
app.button("Large", size="large")
```

**Important:** Violit buttons do NOT return a boolean.
Use `on_click=` to define what happens when clicked.

---

## 5. Layout — Same as Streamlit

```python
# Columns
col1, col2 = app.columns(2)
with col1:
    app.text("Left")
with col2:
    app.text("Right")

# Columns with ratio
c1, c2, c3 = app.columns([2, 1, 1])

# Container
with app.container(border=True):
    app.text("Inside a card")

# Tabs
tab1, tab2 = app.tabs(["Tab A", "Tab B"])
with tab1:
    app.text("Content A")
with tab2:
    app.text("Content B")

# Expander
with app.expander("Details", expanded=False):
    app.text("Hidden content")
    app.button("Default button in expander")  # full width by default

# Reactive expander title
count = app.state(3)
with app.expander(lambda: f"Details ({count.value})", expanded=True):
    app.text(lambda: f"Current count: {count.value}")

# Sidebar
app.configure_sidebar(width=320, min_width=240, max_width=520, resizable=True)
theme_name = app.state("ocean")

with app.sidebar:
    app.text("Sidebar content")
    app.badge(lambda: f"{theme_name.value.title()} Theme")
```

- Sidebar content participates in normal reactive updates.
- Nested widgets inside sidebar `container(...)`, `columns(...)`, and `expander(...)` are supported and should be written like main-area widgets.

### Widget placement guidance

For simple layout, start with `columns(...)`, `container(...)`, and widget label options before reaching for custom CSS.

```python
# Vertical placement inside each column
left, right = app.columns([1, 2], gap="medium", equal_height=True, justify="center")

# Horizontal placement inside each column
left, right = app.columns([1, 2], gap="medium", align="center")

# Local override for one side only
left, right = app.columns([1, 2], gap="medium", equal_height=True)
with left:
    with app.container(fill_height=True, justify="bottom"):
        app.text("Bottom only on the left")

# Selectbox label placement without extra columns
app.selectbox("Language", ["Python", "JS"], label_position="left")
app.selectbox("Mode", ["A", "B"], label_visibility="collapsed")

# Button width: default full-width, explicit override when needed
app.button("Default button")  # full width by default
app.button("Fixed width button", style="width: 280px;")
app.button("Relative width button", style="width: 60%;")

with app.expander("Actions", expanded=True):
    app.button("Default button in expander")  # also full width by default
    app.button("Fixed width in expander", style="width: 280px;")
    app.button("Relative width in expander", style="width: 60%;")
```

**Placement rules of thumb:**
- Use `columns(..., justify=...)` for top/center/bottom placement of each column's child box.
- Use `columns(..., align=...)` for start/center/end placement across the horizontal axis inside each column.
- Use `equal_height=True` when vertical placement needs real extra height to be visible.
- Use `container(fill_height=True, justify=...)` when only one side of a row needs local vertical adjustment.
- Re-entering the same column or tab object multiple times in one render is a valid pattern. This is supported for layouts like `for item in items: with left: ...; with right: ...`.
- Use widget-native options like `label_position` and `label_visibility` before adding extra layout wrappers around forms.
- Default button placement should assume full-width behavior in normal vertical layout containers, including inside `app.expander(...)`.
- If a widget should be narrower than the container, prefer an explicit `style="width: ...;"` override instead of assuming special-case layout behavior inside expander or container blocks.
- When a widget label, title, or summary depends on state, pass the `State` object directly or use `lambda:`. Do not eagerly freeze it with `f"...{state.value}..."` unless a static snapshot is intentional.
- For reactive placement/content, prefer `app.text(state)` or `app.text(lambda: ...)` over `app.text(state.value)` or eager f-strings.
- For reactive expander headers, use `with app.expander(lambda: f"...{state.value}..."):`. The summary/title updates live while preserving the expander body content.
- `justify` aligns the outer child box, not the exact visual baseline of every widget. Different widgets can still look slightly off because their internal control heights, padding, or label areas differ.
- If exact visual alignment matters, add a shared inner frame with the same width/min-height on both sides, or apply small `cls` / `style` adjustments intentionally.

```python
items = app.session_state([{"text": "text"}, {"text": "another text"}], key="items")

@app.reactivity
def render_rows():
    left, right = app.columns(2)
    for item in items.value:
        with left:
            app.text(item["text"])
        with right:
            app.selectbox("value", ["test"])

render_rows()
```

---

## 6. Data & Charts — Same as Streamlit

```python
import pandas as pd
import plotly.express as px

df = pd.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})

app.dataframe(df)                            # AG Grid (interactive)
app.table(df)                                # Static HTML table
app.metric("Revenue", "$54,230", "+12%")     # Metric card
app.json({"key": "value"})                   # JSON viewer

# Filtered master-detail UIs should keep detail selection aligned with visible rows
visible_rows = df[df["y"] >= 20].reset_index(drop=True)
selected_row = app.state({}, key="selected_row")

if visible_rows.empty:
    selected_row.set({})
else:
    visible_records = visible_rows.to_dict("records")
    matching_row = next((row for row in visible_records if row.get("x") == selected_row.value.get("x")), None)
    selected_row.set(matching_row or visible_records[0])

# Charts
fig = px.line(df, x="x", y="y")
app.plotly_chart(fig)                        # Plotly chart

app.line_chart(df)                           # Quick charts
app.bar_chart(df)
app.area_chart(df)
app.scatter_chart(df)
```

---

## 7. Status & Feedback

```python
app.success("Saved!")
app.info("FYI")
app.warning("Caution")
app.error("Failed")
app.toast("Done!", variant="success", icon="circle-check")
app.spinner("Loading...")
app.progress(75)                             # Progress bar (0-100)
app.balloons()
app.snow()

app.callout("Use `app.text()` for escaped output.", variant="info")
app.callout_warning("This action cannot be undone.")
app.callout_success("Deployment finished.")
```

`app.callout(...)` and the shortcut helpers (`callout_info`, `callout_warning`, `callout_success`, etc.) are the preferred way to generate admonition-style documentation UI.

---

## 8. Chat Interface

```python
from typing import Any, cast

messages = app.state([
    {"role": "assistant", "content": "Hello. Ask anything."}
], key="chat_messages")
api_key = app.state("", key="chat_api_key")
mode = app.state("streaming", key="chat_mode")
reactivity = cast(Any, app.reactivity)

def _reply_non_streaming():
    return "Assistant reply"

def _reply_streaming():
    def stream():
        yield "Assistant "
        yield "reply"
    return stream()

def reply(_prompt: str):
    key = api_key.value.strip()
    if not key:
        raise RuntimeError("Paste your API key above.")

    if mode.value == "streaming":
        return _reply_streaming()
    return _reply_non_streaming()

app.text_input("API_KEY", value=api_key.value, key="chat_api_key", type="password")
app.selectbox(
    "Mode",
    ["streaming", "non-streaming"],
    index=0 if mode.value == "streaming" else 1,
    key="chat_mode",
)

@reactivity
def render_chat():
    app.chat_history(messages, height="60vh")

render_chat()
app.managed_chat_input(
    "Type a message...",
    messages=messages,
    on_submit=reply,
    pinned=False,
    auto_scroll="bottom",
    stream_speed="smooth",
)
```

**Preferred chat rules:**
- Use `messages = app.state([...])` and pass it to `app.chat_history(...)` or `app.agent_history(...)` plus `app.managed_chat_input(...)`.
- Let `app.managed_chat_input(messages=..., on_submit=...)` own the normal turn lifecycle: append the user turn, create the assistant placeholder, and apply returned text/events.
- `on_submit` may return a `str`, an iterable yielding `str` chunks, or agent event dicts when building an agent-oriented transcript.
- Keep `app.chat_history(...)` or `app.agent_history(...)` inside `@app.reactivity` when you want only the transcript to rerender.
- Keep `app.managed_chat_input(...)` outside the reactive block when its props are static.
- For provider-specific demos, splitting `Gemini` and `OpenAI` into separate files is often clearer than one large branching file.

**Choose the right chat surface:**
- Plain high-level chat: `app.chat_history(...)` + `app.managed_chat_input(...)`
- Agent high-level chat: `app.agent_history(...)` + `app.managed_chat_input(...)`
- Plain primitive chat: `app.chat_thread(...)` + `app.chat_message(...)` + `app.render_chat_message_body(...)` + `app.chat_input(...)`
- Agent primitive chat: `app.chat_thread(...)` + `app.agent_turn(...)` + `app.render_chat_message_body(...)` + `app.chat_input(...)`

**Primitive chat example:**

```python
from typing import Any, cast

messages = app.state([
    {"role": "assistant", "content": "Hello. Ask anything.", "content_format": "markdown"}
], key="primitive_chat_messages")
busy = app.state(False, key="primitive_chat_busy")
reactivity = cast(Any, app.reactivity)

def append_message(message: dict[str, Any]) -> None:
    messages.set([*messages.value, dict(message)])

def replace_last_message(message: dict[str, Any]) -> None:
    items = [dict(item) for item in messages.value]
    items[-1] = dict(message)
    messages.set(items)

def run_reply(prompt: str) -> None:
    chunks = ["Assistant ", "reply"]
    streamed: list[str] = []
    for chunk in chunks:
        streamed.append(chunk)
        replace_last_message({
            "role": "assistant",
            "content": "".join(streamed),
            "phase": "running",
            "content_format": "markdown",
        })
    replace_last_message({
        "role": "assistant",
        "content": "".join(streamed),
        "phase": "done",
        "content_format": "markdown",
    })

def submit_prompt(prompt: str) -> None:
    cleaned = str(prompt or "").strip()
    if not cleaned or busy.value:
        return

    append_message({"role": "user", "content": cleaned, "content_format": "text"})
    append_message({
        "role": "assistant",
        "content": "",
        "phase": "thinking",
        "thinking_label": "Thinking...",
        "content_format": "markdown",
    })
    busy.set(True)
    app.background(
        lambda prompt=cleaned: run_reply(prompt),
        on_complete=lambda: busy.set(False),
        on_error=lambda exc: busy.set(False),
    ).start()

@reactivity
def render_chat():
    with app.chat_thread(height="60vh"):
        for index, message in enumerate(messages.value):
            with app.chat_message(
                message.get("role", "assistant"),
                phase=message.get("phase") or None,
                thinking=(
                    message.get("role") == "assistant"
                    and not str(message.get("content", "") or "").strip()
                    and str(message.get("phase", "") or "") in {"thinking", "running"}
                ),
                thinking_label=message.get("thinking_label", "Thinking..."),
                status=message.get("status_text", ""),
                key=message.get("key") or f"msg_{index}",
            ):
                app.render_chat_message_body(message)

render_chat()
app.chat_input("Ask...", on_submit=submit_prompt, disabled=bool(busy.value), pinned=False)
```

**Agent message schema that Violit already understands:**

```python
{
    "role": "assistant",
    "content": "Final answer",
    "content_format": "markdown",   # optional: "markdown" or "text"
    "phase": "thinking",            # optional: thinking | running | done | error
    "status_text": "Searching docs",# optional short live status
    "summary": "Checked 3 files.",  # optional agent summary
    "trace": [                        # optional step list
        {"kind": "tool", "title": "search_workspace", "text": "Found 2 matches."}
    ],
    "artifacts": [                    # optional output cards
        {"kind": "file", "title": "doc/llms.txt", "text": "Updated prompt guidance."}
    ],
    "error": "",                     # optional agent error text
    "files": [],                      # optional uploaded file list
    "audio": None,                    # optional uploaded audio
}
```

`app.agent_history(...)` and primitive `app.agent_turn(...)` are designed around that schema. Reuse these field names instead of inventing a parallel protocol.

**When to use manual chat rendering instead:**
- Use manual rendering when you need per-turn control, custom placeholders, explicit background orchestration, or you want to surface attachments/audio before reply generation.
- Otherwise, prefer the built-in high-level chat widgets.

**Custom transcript widgets:**

```python
with app.chat_thread(height="60vh"):
    with app.chat_message("assistant"):
        app.markdown("Hello. Ask anything.")

    with app.chat_message("user"):
        app.markdown("Show me the latest report.")
```

Use `chat_thread()` and `chat_message()` when you want custom message composition but still want Violit's built-in chat styling. When rendering message dicts from state, prefer `app.render_chat_message_body(...)` so streamed chunks, attachments, and content format are handled consistently.

---

## 9. Multi-Page Navigation

```python
import violit as vl
app = vl.App(title="Multi-Page")

def home():
    app.title("Home")
    app.text("Welcome!")

def settings():
    app.title("Settings")
    theme = app.selectbox("Theme", ["light", "dark", "ocean"])
    app.button("Apply", on_click=lambda: app.set_theme(theme.value))

# Sidebar with optional content
with app.sidebar:
    app.markdown("## My App")
    app.divider()

app.navigation([
    vl.Page(home, title="Home", icon="house"),
    vl.Page(settings, title="Settings", icon="gear"),
])

app.button("Go Home", on_click=lambda: app.switch_page("Home"))
app.page_link("Settings", label="Open Settings")

app.run()
```

---

## 10. Theme

```python
# Set at init
app = vl.App(theme="cyberpunk")

# Change at runtime
app.set_theme("ocean")
app.set_animation_mode("hard")
app.set_selection_mode(True)
app.set_primary_color("#2563eb")

# Available themes:
# dark, light, ocean, sunset, forest, dracula, monokai, nord,
# cyberpunk, terminal, vaporwave, blueprint, neo_brutalism,
# soft_neu, hand_drawn, win95, bauhaus, editorial, glass,
# pastel, retro, ant, bootstrap, material,
# violit_light, violit_light_jewel, violit_dark, rgb_gamer, ...
```

---

## 11. Styling (Tailwind-first)

Violit styling is currently **Tailwind-first**.
Start with `cls`, then use `style`, `add_css()`, `configure_widget()`, and `part_cls` when you need more control.

```python
app.text("Big", style="font-size: 2rem; font-weight: 800;")
app.button("Wide", cls="w-full rounded-full bg-sky-500 text-white", variant="primary")
app.card(cls="glass", style="padding: 2rem;")

# Global CSS
app.add_css("""
    .glass {
        background: rgba(255,255,255,0.85);
        backdrop-filter: blur(16px);
    }
""")

# Widget type defaults
app.configure_widget("button", cls="font-semibold rounded-full")
app.configure_widget("card", style="border-radius: 1rem;")

# Shadow DOM part styling
app.button(
    "Save",
    part_cls={"base": "shadow-lg rounded-full"},
)
```

**Styling rules of thumb:**
- `cls`: first choice for utilities and layout
- `style`: direct CSS values or CSS variables
- `configure_widget()`: defaults for a widget family
- `add_css()`: global reusable classes and `::part()` selectors
- `part_cls`: advanced Shadow DOM part overrides

For micro-adjustments after the main layout is already correct, `cls` is the preferred final step.

```python
app.selectbox("Mode", ["A", "B"], cls="mt-4")
app.selectbox("Mode", ["A", "B"], cls="translate-y-[10px]")
app.selectbox("Mode", ["A", "B"], cls="relative top-[8px]")
app.selectbox("Mode", ["A", "B"], cls="ml-[8%] w-[82%]")
```

Use these as intentional fine-tuning, not as a substitute for fixing the parent `columns(...)` / `container(...)` structure first.

## Background Tasks & Interval Timers

Use these instead of inventing your own thread/timer scaffolding unless you have a strong reason not to.

```python
import time

progress = app.state(0)
status = app.state("idle")

def work():
    status.set("running")
    for step in range(1, 6):
        time.sleep(0.35)
        progress.set(step * 20)
    status.set("completed")

task = app.background(
    work,
    on_complete=lambda: app.toast("Finished", variant="success"),
    on_error=lambda exc: app.toast(f"Error: {exc}", variant="danger"),
    singleton=True,
)

timer = app.interval(
    lambda: app.toast("tick", duration=800),
    ms=5000,
    condition=lambda: status.value == "running",
    autostart=False,
)

app.button("Start job", on_click=task.start)
app.button("Pause timer", on_click=timer.pause)
app.button("Resume timer", on_click=timer.resume)
app.progress(progress)
```

**When to use which:**
- `app.background(...)`: long-running Python work
- `app.interval(...)`: periodic callbacks, polling, dashboards, timers
- `app.background(...)` supports `on_complete`, `on_error`, `singleton`, and returns a handle with `start()` / `cancel()` / `is_running`.
- `app.interval(...)` supports `condition=...`, `autostart=...`, and returns a handle with `pause()` / `resume()` / `stop()`.

---

## 12. Dialog

```python
@app.dialog("Confirm")
def confirm_dialog():
    app.text("Are you sure?")
    app.button("Yes", on_click=do_action)

app.button("Open Dialog", on_click=confirm_dialog)
```

Use `@app.dialog(...)` when you need a modal. Prefer it over hand-written modal HTML.

---

## 13. Conditional & Loop Rendering

```python
count = app.state(0)

# Conditional rendering (reactive)
app.If(
    lambda: count.value > 5,
    then=lambda: app.success("Big!"),
    else_=lambda: app.info("Small"),
)

# Loop rendering (reactive)
items = app.state(["Apple", "Banana"])
app.For(
    items,
    render=lambda item: app.text(f"- {item}"),
    empty=lambda: app.info("No items"),
)
```

---

## Common Mistakes to Avoid

```python
# ❌ WRONG: Using Streamlit's if-block pattern for buttons
if app.button("Click"):   # Buttons don't return bool in Violit
    do_something()

# ✅ CORRECT:
app.button("Click", on_click=do_something)

# ❌ WRONG: Forgetting app.run()
app.title("Hello")
# Script ends without app.run()

# ✅ CORRECT:
app.title("Hello")
app.run()

# ❌ WRONG: Passing .value to widgets (not reactive)
app.text(count.value)

# ✅ CORRECT: Pass State object directly
app.text(count)

# ❌ WRONG: Using f-string directly (not reactive)
app.text(f"Count: {count.value}")

# ✅ CORRECT: Use lambda or operator
app.text(lambda: f"Count: {count.value}")
app.text("Count: " + count)

# ❌ WRONG: Inventing a widget that does not exist
app.password_input("Password")

# ✅ CORRECT:
app.text_input("Password", type="password")

# ❌ WRONG: Assuming on_change gets a State object
app.text_input("Search", on_change=lambda s: print(s.value))

# ✅ CORRECT:
app.text_input("Search", on_change=lambda value: print(value))

# ❌ WRONG: Reassigning state variable
count = count.value + 1

# ✅ CORRECT:
count.set(count.value + 1)
```

---

## Complete Example: Dashboard

```python
import violit as vl
import pandas as pd
import numpy as np

app = vl.App(title="Dashboard", theme="ocean", container_width="1200px")

# State
data_size = app.state(50)

# Sidebar
with app.sidebar:
    app.markdown("## Dashboard")
    app.divider()
    theme = app.selectbox("Theme", ["ocean", "dark", "cyberpunk"])
    app.button("Apply", on_click=lambda: app.set_theme(theme.value))

# Main content
app.header("Sales Dashboard")

c1, c2, c3 = app.columns(3)
with c1: app.metric("Revenue", "$54,230", "+12%")
with c2: app.metric("Users", "1,234", "+5%")
with c3: app.metric("Uptime", "99.9%", "0%")

app.divider()

n = app.slider("Data points", 10, 200, 50)

app.line_chart(
    lambda: pd.DataFrame(
        np.random.randn(n.value, 3),
        columns=["A", "B", "C"]
    )
)

app.run()
```

---

## API Quick Reference

| Category | Streamlit | Violit | Note |
|----------|-----------|--------|------|
| Import | `import streamlit as st` | `import violit as vl` | |
| App init | (implicit) | `app = vl.App(...)` | Required |
| Run | `streamlit run app.py` | `violit run app.py` + `app.run()` | `python app.py` still works, but CLI is preferred |
| State | `st.session_state.x = 0` | `x = app.state(0)` | Returns reactive State |
| Read state | `st.session_state.x` | `x.value` or `x()` | |
| Write state | `st.session_state.x = 5` | `x.set(5)` or `x.value = 5` | |
| Button | `if st.button("X"):` | `app.button("X", on_click=fn)` | Callback, not if-block |
| Input return | `val = st.text_input()` (str) | `val = app.text_input()` (State) | |
| Form | `with st.form(...): ...` | `with app.form(...): ...` | Submit with `app.form_submit_button()` |
| Download | `st.download_button(...)` | `app.download_button(...)` | Built-in browser/native download flow |
| Programmatic nav | `st.switch_page(...)` | `app.switch_page(...)` | Page object, function, title, slug, or hash |
| Link widgets | Markdown / HTML links | `app.link_button(...)`, `app.page_link(...)` | Prefer helper widgets |
| Rerun | `st.rerun()` | Not needed | Auto partial update |
| Cache | `@st.cache_data` | Not needed | No full rerun |
| Reactive block | `@st.fragment` | `@app.reactivity` | `@app.fragment` is deprecated; call the decorated function once |
| Key | `st.button("X", key="k")` | Usually not needed | Auto-generated |
| Sidebar | `st.sidebar.xxx()` | `with app.sidebar: app.xxx()` | Context manager |
| Pages | `st.Page` in `st.navigation` | `vl.Page` in `app.navigation` | |
| Dialog | `@st.dialog` | `@app.dialog("Title")` | Decorator-based modal |
| Callout | `st.info/success/warning/error` | `app.callout(...)` + shortcuts | Rich admonition boxes |
| Theme | `config.toml` | `app = vl.App(theme="ocean")` | 30+ presets |
| Lite mode | Not available | `violit run app.py --lite` | HTMX-based; often better for higher concurrency |
| Desktop | Not available | `violit run app.py --native` | pywebview |
| Database | External ORM | `app = vl.App(db="./app.db")` | SQLModel built-in |
| Auth | External lib | `app.setup_auth(User)` | Session + bcrypt built-in |

---

## 14. Database (ORM)

Violit has built-in database support via **SQLModel** (SQLAlchemy 2.0 + Pydantic).
Models use standard SQLModel syntax — no custom wrapper classes needed.

### Setup

```python
from sqlmodel import SQLModel, Field
import violit as vl
from typing import Optional

# Define model (standard SQLModel)
class Task(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    done: bool = False
    priority: int = 0

# Pass db path to App — migration runs automatically on startup
app = vl.App(title="My App", db="./app.db")
# Default: migrate="auto" — creates tables + adds missing columns automatically
```

### Migration Modes

```python
app = vl.App(db="./app.db")                     # migrate="auto" (default)
app = vl.App(db="./app.db", migrate="auto")      # safe auto: add columns only
app = vl.App(db="./app.db", migrate="force")     # aggressive: also drops columns
app = vl.App(db="./app.db", migrate="files")     # Alembic file-based (production/teams)
app = vl.App(db="./app.db", migrate=False)       # disabled: manage DB yourself
```

**`auto` mode behavior (default):**
- Creates missing tables automatically
- Adds missing columns automatically (preserves existing data)
- Warns about deleted/changed columns — does NOT apply them (safe)

**File-based migration CLI** (when `migrate="files"`):
```bash
violit run main.py --make-migration "add priority column"  # generate Alembic file
violit run main.py --apply                                 # apply pending migrations
violit run main.py --rollback 1                            # roll back N steps
```

### CRUD API (`app.db`)

```python
# CREATE
task = app.db.add(Task(title="Buy milk"))       # INSERT + commit + refresh
task.id  # → auto-assigned int

# READ
task  = app.db.get(Task, 1)                     # by primary key → Task | None
task  = app.db.first(Task, Task.done == False)  # first match → Task | None
tasks = app.db.all(Task)                        # all rows → List[Task]
tasks = app.db.filter(
    Task,
    Task.done == False,
    order_by=Task.id.desc(),
    limit=20,
    offset=0,
)

# UPDATE
task.done = True
app.db.save(task)                               # UPDATE + commit

# DELETE
app.db.delete(task)                             # single row
count = app.db.delete_by(Task, Task.done == True)  # bulk delete

# AGGREGATES
total = app.db.count(Task)
done  = app.db.count(Task, Task.done == True)
found = app.db.exists(Task, Task.title == "Buy milk")  # → bool
```

### Raw / Complex Queries

```python
from sqlmodel import select
from sqlalchemy import func

with app.db.session() as session:
    results = session.exec(
        select(Task)
        .where(Task.done == False)
        .where(Task.title.contains("urgent"))
        .order_by(Task.priority.desc())
        .limit(10)
    ).all()

    # Aggregation
    count_by_done = session.exec(
        select(Task.done, func.count(Task.id)).group_by(Task.done)
    ).all()
```

### Supported Databases

```python
app = vl.App(db="./local.db")                                  # SQLite (default)
app = vl.App(db="sqlite:///./local.db")                        # SQLite explicit URL
app = vl.App(db="postgresql+psycopg2://user:pw@host/db")       # PostgreSQL
app = vl.App(db="mysql+pymysql://user:pw@host/db")             # MySQL
```

### Column Types

```python
from sqlmodel import SQLModel, Field
from typing import Optional
from datetime import datetime

class Article(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str                                           # NOT NULL text
    content: Optional[str] = None                        # nullable text
    view_count: int = 0                                  # integer with default
    rating: float = 0.0                                  # float
    is_published: bool = False                           # boolean
    email: str = Field(unique=True)                      # unique constraint
    created_at: datetime = Field(default_factory=datetime.now)
```

---

## 15. Auth (Authentication)

Violit has built-in session-based authentication using **bcrypt** password hashing.
The session is stored server-side in violit's existing `ss_sid` cookie store.

### Setup

```python
from sqlmodel import SQLModel, Field
import violit as vl
from typing import Optional

# 1. Define your own User model (standard SQLModel)
class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(unique=True)     # login identifier
    hashed_password: str                   # MUST use this field name (default)
    role: str = "user"                     # for role-based access control
    # Add any extra fields freely:
    email: Optional[str] = None
    display_name: Optional[str] = None

# 2. Create app with DB
app = vl.App(title="My App", db="./app.db")

# 3. Register auth — one line
app.setup_auth(User)
# Optional params:
# app.setup_auth(
#     User,
#     username_field="username",        # default
#     password_field="hashed_password", # default
#     role_field="role",                # default
#     login_page="로그인",               # page title to redirect on auth failure
#     require_auth=False,               # True = protect ALL pages automatically
# )
```

### Auth API (`app.auth`)

```python
# Login — returns True on success, False on failure
success = app.auth.login("admin", "password123")

# Logout — clears session
app.auth.logout()

# Get current user — returns User object or None if not logged in
user = app.auth.current_user()
if user:
    print(user.username)

# Check login status
if app.auth.is_authenticated():
    app.text("You are logged in")

# Check role (supports comma-separated multi-role: "admin,editor")
if app.auth.has_role("admin"):
    app.button("Admin Panel")

# Create user (password auto-hashed with bcrypt)
user = app.auth.create_user("john", "secure_pass", role="user", email="j@co.com")

# Change password
app.auth.change_password(user, "new_secure_pass")

# Delete user
app.auth.delete_user(user)
```

### Page Protection

```python
app.navigation([
    vl.Page(login_page,   title="Login"),                              # public
    vl.Page(home_page,    title="Home",    require_auth=True),         # login required
    vl.Page(admin_page,   title="Admin",   require_auth=True,
                                           require_role="admin"),       # admin only
    vl.Page(profile_page, title="Profile", require_auth=True),         # login required
])
# Non-authenticated users are automatically redirected to the login page.
# Users without the required role see an empty page (no redirect).
```

### Complete Auth Example

```python
from sqlmodel import SQLModel, Field
import violit as vl
from typing import Optional

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(unique=True)
    hashed_password: str
    role: str = "user"

app = vl.App(title="My App", db="./app.db")
app.setup_auth(User)

# Create initial admin on first run
def ensure_admin():
    if not app.db.exists(User, User.username == "admin"):
        app.auth.create_user("admin", "admin1234", role="admin")

def login_page():
    app.title("Login")
    username = app.state("")
    password = app.state("")

    with app.form("login"):
        app.text_input("Username", value=username)
        app.text_input("Password", value=password, type="password")

        def do_login():
            if app.auth.login(username.value, password.value):
                app.switch_page("Home")   # switch to Home page
            else:
                app.toast("Invalid credentials", "danger")

        app.form_submit_button("Login", on_click=do_login)

def home_page():
    user = app.auth.current_user()
    app.title(f"Welcome, {user.username}!")

    if app.auth.has_role("admin"):
        app.button("Admin Panel", on_click=lambda: app.switch_page("Admin"))

    app.button("Logout",
               on_click=lambda: (app.auth.logout(), app.switch_page("Login")),
               variant="secondary")

def admin_page():
    app.title("Admin Panel")
    users = app.db.all(User)
    app.text(f"Total users: {len(users)}")

app.navigation([
    vl.Page(login_page, title="Login"),
    vl.Page(home_page,  title="Home",  require_auth=True),
    vl.Page(admin_page, title="Admin", require_auth=True, require_role="admin"),
])

ensure_admin()
app.run()
```

### Security Notes

- Passwords are hashed with **bcrypt** (cost factor 12) — never stored as plaintext
- Login uses timing-safe comparison (bcrypt built-in) — prevents timing attacks
- Session is stored **server-side** in TTL cache (1800s expiry) — not in cookie payload
- Only a random session ID is sent to the browser via `ss_sid` cookie (HttpOnly)
- CSRF protection is already built into violit and covers auth actions automatically

---

## 16. CLI (Command Line Interface)

Violit provides a built-in CLI to manage your projects seamlessly, much like streamlit run or create-next-app.

### violit create
Scaffolds a new Violit project with the recommended boilerplate (`main.py`, `requirements.txt`, `.gitignore`, `README.md`).
```bash
violit create my-app
cd my-app
```

### violit run
Executes your Violit application. It passes any trailing arguments directly to `app.run()`.
```bash
# Basic run
violit run main.py

# Local development (recommended)
violit run main.py --reload --localhost

# HTMX-based Lite mode (often better for higher concurrency)
violit run main.py --lite --localhost

# Native desktop mode
violit run main.py --native
violit run main.py --native --debug
violit run main.py --native --on-top
violit run main.py --native --nosplash

# Custom bind / port
violit run main.py --host 0.0.0.0 --port 8010

# Database migrations (Alembic)
violit run main.py --make-migration "add_users"
violit run main.py --apply
violit run main.py --rollback 1

# Show supported runtime flags
violit run main.py --help
```

**CLI rules of thumb:**
- Prefer `violit run ...` in docs, examples, and generated instructions.
- Use `--localhost` for local-only development.
- Use `--host 0.0.0.0` only when you intentionally want LAN or server access.
- Use `--lite` when you want HTMX-based HTTP updates instead of the default WebSocket runtime.
- Use `--native`, `--debug`, `--on-top`, and `--nosplash` for desktop app flows.
- Migration flags require a database app and are most useful with `migrate="files"`.

*Note: You can still run `python main.py [args]`. `violit run` is simply the recommended wrapper and forwards the same runtime flags.*

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.