refactor: migrate and consolidate UI templates to compile-time Askama component macros
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# Detailed Project Architecture & Design System Reference Manual
|
||||
|
||||
This document provides a comprehensive technical overview of the **Stick** template, detailing its vertical structure, core dependencies, event delegation contracts, component reuse solutions, and our current implementation plan.
|
||||
|
||||
---
|
||||
|
||||
## 📁 1. Project Directory Layout & Vertical Architecture
|
||||
|
||||
Unlike traditional MVC applications that split code horizontally (e.g., separating all controllers, all models, and all views), Stick is organized by **vertical features (use-cases)**. Every business module contains its own Rust handler logic, database schemas, repository queries, and custom routing.
|
||||
|
||||
### Directory Mapping
|
||||
```text
|
||||
stick/
|
||||
├── Cargo.toml # Rust dependency management
|
||||
├── package.json # Node assets configuration (optional tooling)
|
||||
├── src/
|
||||
│ ├── main.rs # Core composition, shared state, and route merging
|
||||
│ ├── common/ # Shared utilities & configurations
|
||||
│ │ ├── config.rs # Configuration parsed from environment variables (.env)
|
||||
│ │ ├── database.rs # MongoDB initialization and connections
|
||||
│ │ └── errors.rs # AppError implementation wrapping custom responses
|
||||
│ ├── auth/ # AUTH USE-CASE (Users, Passwords, Session cookies)
|
||||
│ │ ├── extractors.rs # Native Axum extractors for authentication state
|
||||
│ │ ├── handlers.rs # Login and registration endpoint handlers
|
||||
│ │ ├── models.rs # User database schemas
|
||||
│ │ └── repository.rs # MongoDB operations for authentication
|
||||
│ ├── tasks/ # TASKS USE-CASE (CRUD tasks, dashboard view)
|
||||
│ │ ├── handlers.rs
|
||||
│ │ ├── models.rs
|
||||
│ │ └── repository.rs
|
||||
│ ├── developers/ # DEVELOPERS USE-CASE (HTMX-based assignee autocomplete query)
|
||||
│ │ ├── handlers.rs
|
||||
│ │ ├── models.rs
|
||||
│ │ └── repository.rs
|
||||
│ ├── main_view/ # STATIC VIEWS & GLOBAL ASSET ENDPOINTS
|
||||
│ │ └── mod.rs # Serves index.html, static css, and static javascript files
|
||||
│ ├── components/ # WIKI & INTERACTIVE DESIGN SYSTEM
|
||||
│ │ ├── mod.rs # Wiki routing table
|
||||
│ │ └── handlers.rs # Handlers serving reference pages
|
||||
│ └── input.css # Tailwind CSS v4 custom theme mappings and custom scrollbars
|
||||
├── static/ # Raw assets compiled/included in compilation
|
||||
│ ├── tailwind.css # Output/input css rules
|
||||
│ └── js/
|
||||
│ ├── components.js # Main component event delegation system
|
||||
│ └── combobox.js # HTMX-friendly autocomplete combobox scripts
|
||||
└── templates/ # Raw HTML template layout files (Askama templates)
|
||||
├── base.html # Global HTML layout wrapper (injects headers, navigation)
|
||||
├── auth/ # Login / registration markup
|
||||
├── tasks/ # Task management layouts
|
||||
├── main_view/ # Landing page layout
|
||||
└── components/ # Component Wiki pages (Buttons, Modals, Comboboxes, etc.)
|
||||
```
|
||||
|
||||
### Route Composition
|
||||
In [src/main.rs](file:///home/enciphered/Desktop/Code/stick/src/main.rs), routers from each vertical module are instantiated and merged using Axum's `.merge()` method:
|
||||
```rust
|
||||
let app = Router::new()
|
||||
.merge(main_view::router())
|
||||
.merge(components::router())
|
||||
.merge(auth::router())
|
||||
.merge(tasks::router())
|
||||
.merge(developers::router())
|
||||
.with_state(state);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 2. Core Technical Dependencies
|
||||
|
||||
* **Web Server**: `axum = "0.8.9"` — Native asynchronous routing with type-safe extractor traits.
|
||||
* **Database**: `mongodb = "3.7.0"` — Offical Rust driver, using `bson = "3.1.0"` with serde serialization.
|
||||
* **Template Rendering**: `askama = "0.16.0"` — Compile-time type safety; views are compiled directly into the binary.
|
||||
* **Styling Engine**: Tailwind CSS (v4) — Uses native CSS variables and `@import "tailwindcss"` rather than complex configuration JSONs.
|
||||
* **Security & JWT**: `jsonwebtoken = "10.4.0"` (via `rust_crypto` backend) + `bcrypt = "0.19.1"` for password hashing. JWTs are stored in secure `HttpOnly` cookie wrappers.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 3. JavaScript & HTML Event Delegation Contract
|
||||
|
||||
Rather than attaching individual event listeners to elements upon page load (which degrades performance and fails when elements are swapped dynamically via HTMX), Stick uses **document-level event delegation** via [static/js/components.js](file:///home/enciphered/Desktop/Code/stick/static/js/components.js) and [static/js/combobox.js](file:///home/enciphered/Desktop/Code/stick/static/js/combobox.js).
|
||||
|
||||
### Active Binding Attributes & Hooks
|
||||
To make components function, developers must preserve specific HTML attributes and class structures that JavaScript expects:
|
||||
|
||||
| Component Type | JavaScript Trigger / Hook | Expected Target / Functional Structure |
|
||||
| :--- | :--- | :--- |
|
||||
| **Modal / Dialog** | `[data-modal-target="id"]` | `.modal-dialog` (wrapper), `.modal-backdrop`, `.modal-close` |
|
||||
| **Sheets / Drawers** | `[data-sheet-target="id"]` | `.sheet-dialog` (wrapper), `.sheet-backdrop`, `.sheet-close` |
|
||||
| **Custom Dropdowns** | `.dropdown-trigger` | `.dropdown-menu` (container), `.dropdown-content` |
|
||||
| **Interactive Tabs**| `[data-tab-target="pane-id"]` | `[data-tab-group="group-name"]` on buttons & content wrappers |
|
||||
| **Accordions** | `.accordion-trigger` | `.accordion-item` (container), `.accordion-content`, `.accordion-chevron` |
|
||||
| **Custom Select** | `.select-trigger` | `.custom-select` (container), `.select-popover`, `.select-item`, `.select-value` |
|
||||
| **Autocomplete Combobox**| `.combobox-input` | `.autocomplete-combobox` (container), `.combobox-results`, `.combobox-value` |
|
||||
|
||||
*Any customized styling classes (colors, borders, rounded corners) can be freely added or changed. However, the core class naming hierarchy listed above must remain intact to preserve interaction animations and keyboard controls.*
|
||||
|
||||
---
|
||||
|
||||
## 🧩 4. Proposed Scalability & Code Reuse Solutions
|
||||
|
||||
### Solution 1: Askama Macros (Selected Demo)
|
||||
Consolidate UI components into a global macros registry template.
|
||||
* *Pros*: Safe compilation (missing variables or typos are checked at build time), zero performance cost.
|
||||
* *Cons*: Complex child structures (e.g. slots or dynamic SVG parameters) require passing strings/blocks, which can look verbose.
|
||||
|
||||
### Solution 2: CSS Component Classes
|
||||
Define custom components in [src/input.css](file:///home/enciphered/Desktop/Code/stick/src/input.css) using Tailwind CSS v4's components layer (e.g. `@apply btn btn-primary`).
|
||||
* *Pros*: Clear, descriptive markup syntax; easily consumable by pure front-end web developers.
|
||||
* *Cons*: Partially moves styling definitions out of HTML files and back into stylesheet files.
|
||||
|
||||
### Solution 3: Rust Struct Components
|
||||
Represent reusable buttons, grids, or select widgets as Rust structs that implement Askama's `Template` trait.
|
||||
* *Pros*: Advanced data-driven structures and type constraints managed entirely in Rust code.
|
||||
* *Cons*: Visual updates require recompiling Rust binaries, which limits swift CSS experimentation.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 5. Action Plan: Askama Macros Implementation
|
||||
We are implementing a demo showcasing Askama Macros in a new git branch:
|
||||
|
||||
1. **Branch Setup**: Create `demo/askama-macros`.
|
||||
2. **Define Macros**: Create `templates/components/macros.html` to define reusable snippets for:
|
||||
* **Button**: Primary, secondary, outline, destructive, status indicators.
|
||||
* **Modal**: Accessibility markup, background overlays, closing controls.
|
||||
* **Autocomplete Combobox**: Dynamic HTMX data queries and search hooks.
|
||||
3. **Refactor Views**: Replace manual HTML blocks in [dashboard.html](file:///home/enciphered/Desktop/Code/stick/templates/tasks/dashboard.html) with:
|
||||
```html
|
||||
{% import "components/macros.html" as ui %}
|
||||
{{ ui::button("Submit", "primary") }}
|
||||
```
|
||||
@@ -185,7 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (input) {
|
||||
input.value = name;
|
||||
input.focus();
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Sign In - Stick{% endblock %}
|
||||
|
||||
@@ -22,20 +23,12 @@
|
||||
{% endif %}
|
||||
|
||||
<form class="space-y-5" action="/auth/login" method="post">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-400 mb-1.5">Username</label>
|
||||
<input id="username" name="username" type="text" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="Enter username">
|
||||
</div>
|
||||
{{ ui::text_input(id="username", name="username", label="Username", type="text", placeholder="Enter username", required=true) }}
|
||||
|
||||
{{ ui::text_input(id="password", name="password", label="Password", type="password", placeholder="••••••••", required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-400 mb-1.5">Password</label>
|
||||
<input id="password" name="password" type="password" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="group relative w-full flex justify-center py-3.5 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 focus:ring-offset-[#0f172a] shadow-lg shadow-sky-500/10">
|
||||
Sign In
|
||||
</button>
|
||||
{{ ui::button(label="Sign In", variant="indigo", type="submit", extra_class="w-full py-3.5 shadow-lg shadow-sky-500/10") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Sign Up - Stick{% endblock %}
|
||||
|
||||
@@ -31,20 +32,12 @@
|
||||
{% endif %}
|
||||
|
||||
<form class="space-y-5" action="/auth/register" method="post">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-400 mb-1.5">Username</label>
|
||||
<input id="username" name="username" type="text" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition duration-200 text-sm" placeholder="Choose a username">
|
||||
</div>
|
||||
{{ ui::text_input(id="username", name="username", label="Username", type="text", placeholder="Choose a username", required=true) }}
|
||||
|
||||
{{ ui::text_input(id="password", name="password", label="Password", type="password", placeholder="••••••••", required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-400 mb-1.5">Password</label>
|
||||
<input id="password" name="password" type="password" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition duration-200 text-sm" placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="group relative w-full flex justify-center py-3.5 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-emerald-500 to-teal-600 hover:opacity-95 transition focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 focus:ring-offset-[#0f172a] shadow-lg shadow-emerald-500/10">
|
||||
Sign Up
|
||||
</button>
|
||||
{{ ui::button(label="Sign Up", variant="primary", type="submit", extra_class="w-full py-3.5 bg-gradient-to-r from-emerald-500 to-teal-600 hover:opacity-95 text-white shadow-lg shadow-emerald-500/10 focus:ring-emerald-500") }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Buttons - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,82 +34,44 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="btn-sandbox" class="wiki-pane flex flex-wrap gap-4 py-2">
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background bg-primary text-primary-foreground hover:opacity-90 px-4 py-2.5 shadow-md shadow-slate-950/20 active:scale-95">
|
||||
Primary
|
||||
</button>
|
||||
{{ ui::button(label="Primary", variant="primary") }}
|
||||
{{ ui::button(label="Secondary", variant="secondary") }}
|
||||
{{ ui::button(label="Outline", variant="outline") }}
|
||||
{{ ui::button(label="Destructive", variant="destructive") }}
|
||||
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background bg-secondary text-secondary-foreground hover:bg-secondary/80 px-4 py-2.5 active:scale-95">
|
||||
Secondary
|
||||
</button>
|
||||
<!-- Icon Button using safe SVG markup inside label -->
|
||||
{{ ui::button(label="<svg class='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M12 4v16m8-8H4'/></svg> Create Task", variant="indigo", extra_class="gap-2") }}
|
||||
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background border border-border bg-transparent hover:bg-secondary text-slate-200 px-4 py-2.5 active:scale-95">
|
||||
Outline
|
||||
</button>
|
||||
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 focus:ring-offset-background bg-destructive text-destructive-foreground hover:opacity-90 px-4 py-2.5 shadow-md active:scale-95">
|
||||
Destructive
|
||||
</button>
|
||||
|
||||
<!-- Icon Button -->
|
||||
<button class="inline-flex items-center gap-2 justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2.5 active:scale-95">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Create Task
|
||||
</button>
|
||||
|
||||
<!-- Loading State -->
|
||||
<button disabled class="inline-flex items-center gap-2 justify-center rounded-xl text-xs font-bold bg-secondary text-slate-500 cursor-not-allowed px-4 py-2.5">
|
||||
<svg class="animate-spin h-3.5 w-3.5 text-slate-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Processing...
|
||||
</button>
|
||||
<!-- Loading / Disabled state -->
|
||||
{{ ui::button(label="<svg class='animate-spin h-3.5 w-3.5 text-slate-500' fill='none' viewBox='0 0 24 24'><circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4'></circle><path class='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path></svg> Processing...", variant="secondary", disabled=true, extra_class="gap-2 text-slate-500 cursor-not-allowed") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
<div id="btn-code" class="wiki-pane hidden space-y-4">
|
||||
<div class="relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 rounded-lg border border-border bg-popover/80 backdrop-blur text-[10px] font-semibold text-muted-foreground/90 hover:text-white hover:bg-secondary opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5" onclick="copyCodeSnippet(this)">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Primary Button -->
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background bg-primary text-primary-foreground hover:opacity-90 px-4 py-2.5 shadow-md shadow-slate-950/20 active:scale-95">
|
||||
Primary
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Primary Button -->
|
||||
{{ "{{" }} ui::button(label="Primary", variant="primary") {{ "}}" }}
|
||||
|
||||
<!-- Secondary Button -->
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 bg-secondary text-secondary-foreground hover:bg-secondary/80 px-4 py-2.5 active:scale-95">
|
||||
Secondary
|
||||
</button>
|
||||
{{ "{{" }} ui::button(label="Secondary", variant="secondary") {{ "}}" }}
|
||||
|
||||
<!-- Outline Button -->
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 border border-border bg-transparent hover:bg-secondary text-slate-200 px-4 py-2.5 active:scale-95">
|
||||
Outline
|
||||
</button>
|
||||
{{ "{{" }} ui::button(label="Outline", variant="outline") {{ "}}" }}
|
||||
|
||||
<!-- Destructive Button -->
|
||||
<button class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-rose-500 bg-destructive text-destructive-foreground hover:opacity-90 px-4 py-2.5 active:scale-95">
|
||||
Destructive
|
||||
</button>
|
||||
{{ "{{" }} ui::button(label="Destructive", variant="destructive") {{ "}}" }}
|
||||
|
||||
<!-- Create Button (Icon + Text) -->
|
||||
<button class="inline-flex items-center gap-2 justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 bg-indigo-650 hover:bg-indigo-600 text-white px-4 py-2.5 active:scale-95">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Create Task
|
||||
</button>
|
||||
{{ "{{" }} ui::button(label="<svg class='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M12 4v16m8-8H4'/></svg> Create Task", variant="indigo", extra_class="gap-2") {{ "}}" }}
|
||||
|
||||
<!-- Spinner Loading Button -->
|
||||
<button disabled class="inline-flex items-center gap-2 justify-center rounded-xl text-xs font-bold bg-secondary text-slate-500 cursor-not-allowed px-4 py-2.5">
|
||||
<svg class="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></svg>
|
||||
Processing...
|
||||
</button></code></pre>
|
||||
{{ "{{" }} ui::button(label="<svg class='animate-spin h-3.5 w-3.5 text-slate-500' fill='none' viewBox='0 0 24 24'><circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4'></circle><path class='opacity-75' fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'></path></svg> Processing...", variant="secondary", disabled=true, extra_class="gap-2 text-slate-500 cursor-not-allowed") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Autocomplete (Combobox) - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -56,14 +57,7 @@
|
||||
<span class="text-[10px] font-bold text-emerald-400 uppercase tracking-wider block mb-1">Server-Side HTMX Search</span>
|
||||
|
||||
{% if authenticated %}
|
||||
<div class="autocomplete-combobox relative">
|
||||
<label class="block text-xs font-semibold text-muted-foreground mb-1.5">Live MongoDB Query</label>
|
||||
<input type="hidden" name="assignee_id" class="combobox-value">
|
||||
<input type="text" name="q" placeholder="Query developers database..." autocomplete="off"
|
||||
class="combobox-input block w-full px-4 py-2 bg-background border border-border text-white rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
hx-get="/developers/search" hx-trigger="input changed delay:200ms" hx-target="next .combobox-results">
|
||||
<div class="combobox-results absolute z-10 w-full mt-2 bg-popover border border-border rounded-xl p-1 shadow-xl hidden"></div>
|
||||
</div>
|
||||
{{ ui::search_combobox(name="assignee_id", label="Live MongoDB Query", placeholder="Query developers database...", search_url="/developers/search", input_id="wiki-db-search", value_id="wiki-db-value") }}
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-border bg-[#09090b]/40 p-4 space-y-2 text-xs">
|
||||
<div class="flex items-center gap-1.5 text-amber-500 font-bold">
|
||||
@@ -115,19 +109,10 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Server-Side Combobox Container (Asynchronous Query via HTMX) -->
|
||||
<div class="autocomplete-combobox relative">
|
||||
<!-- Holds the final value submitted to forms -->
|
||||
<input type="hidden" name="assignee_id" class="combobox-value">
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Input box triggers search query. Submits with parameter 'q' -->
|
||||
<input type="text" name="q" placeholder="Search developers..." autocomplete="off"
|
||||
class="combobox-input block w-full px-4 py-2 bg-background border border-border rounded-xl text-sm focus:ring-2 focus:ring-sky-500"
|
||||
hx-get="/developers/search" hx-trigger="input changed delay:250ms" hx-target="next .combobox-results">
|
||||
|
||||
<!-- Dropdown container receives swapped HTML markup from server -->
|
||||
<div class="combobox-results absolute z-10 w-full mt-2 bg-popover border border-border rounded-xl p-1 shadow-xl hidden"></div>
|
||||
</div></code></pre>
|
||||
<!-- Server-Side Combobox using macro (Asynchronous Query via HTMX) -->
|
||||
{{ "{{" }} ui::search_combobox(name="assignee_id", label="Live MongoDB Query", placeholder="Query developers database...", search_url="/developers/search", input_id="wiki-db-search", value_id="wiki-db-value") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Date & Time Pickers - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,183 +34,42 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="picker-sandbox" class="wiki-pane grid grid-cols-1 sm:grid-cols-2 gap-6 max-w-lg py-2">
|
||||
<!-- Date Picker -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-405">Date Picker</label>
|
||||
<div class="custom-datepicker relative inline-block w-full" id="wiki-datepicker" data-year="2026" data-month="4">
|
||||
<input type="hidden" name="wiki_date" class="datepicker-value" value="2026-05-30">
|
||||
<button type="button" class="datepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="datepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span class="datepicker-text">May 30, 2026</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-[270px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex items-center justify-between mb-3.5">
|
||||
<button type="button" class="datepicker-prev p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span class="datepicker-month-year text-xs font-bold text-slate-200">May 2026</span>
|
||||
<button type="button" class="datepicker-next p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 text-center text-[10px] font-bold text-slate-500 mb-2">
|
||||
<span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
|
||||
</div>
|
||||
<div class="datepicker-days grid grid-cols-7 gap-1 text-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Date Picker using macro -->
|
||||
{{ ui::datepicker(id="wiki-datepicker", name="wiki_date", label="Date Picker", value="2026-05-30", display_text="May 30, 2026", data_year="2026", data_month="4") }}
|
||||
|
||||
<!-- Time Picker -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-405">Time Picker</label>
|
||||
<div class="custom-timepicker relative inline-block w-full" id="wiki-timepicker">
|
||||
<input type="hidden" name="wiki_time" class="timepicker-value" value="12:00 PM">
|
||||
<button type="button" class="timepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="timepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="timepicker-text">12:00 PM</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="timepicker-popover absolute right-0 sm:left-0 z-20 mt-2 w-[230px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Hr</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-hours"></div>
|
||||
</div>
|
||||
<span class="text-slate-500 font-bold self-end mb-12">:</span>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Min</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-minutes"></div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center ml-1">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Am/Pm</span>
|
||||
<div class="flex flex-col gap-1 w-12">
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">AM</button>
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">PM</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Time Picker using macro -->
|
||||
{{ ui::timepicker(id="wiki-timepicker", name="wiki_time", label="Time Picker", value="12:00 PM") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
<div id="picker-code" class="wiki-pane hidden space-y-4">
|
||||
<!-- Date Picker Code -->
|
||||
<!-- Date Picker Macro -->
|
||||
<div class="space-y-2">
|
||||
<span class="text-xs font-bold text-muted-foreground block">Date Picker HTML Structure</span>
|
||||
<span class="text-xs font-bold text-muted-foreground block">Date Picker Macro</span>
|
||||
<div class="relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 rounded-lg border border-border bg-popover/80 backdrop-blur text-[10px] font-semibold text-muted-foreground/90 hover:text-white hover:bg-secondary opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5" onclick="copyCodeSnippet(this)">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Date Picker Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Custom Date Picker Container
|
||||
- class "custom-datepicker": required for JavaScript target binding
|
||||
- data-year / data-month: initializes the calendar viewport (month is 0-indexed: 4 = May) -->
|
||||
<div class="custom-datepicker relative inline-block w-full" id="unique-datepicker-id" data-year="2026" data-month="4">
|
||||
<!-- Hidden input that holds the actual selected date (YYYY-MM-DD) to submit with the form -->
|
||||
<input type="hidden" name="date_value" class="datepicker-value" value="2026-05-30">
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Trigger Button: opens/closes the dropdown calendar popover -->
|
||||
<button type="button" class="datepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200">
|
||||
<span class="datepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<!-- text-label: will be dynamically updated by components.js when a day is selected -->
|
||||
<span class="datepicker-text">Pick a date</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Popover: holds month controls and calendar grids -->
|
||||
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-[270px] p-3 rounded-2xl border border-border bg-popover shadow-2xl hidden">
|
||||
<!-- Navigation Controls -->
|
||||
<div class="flex items-center justify-between mb-3.5">
|
||||
<button type="button" class="datepicker-prev p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<!-- Current visible Month/Year label -->
|
||||
<span class="datepicker-month-year text-xs font-bold text-slate-200"></span>
|
||||
<button type="button" class="datepicker-next p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Weekday column labels -->
|
||||
<div class="grid grid-cols-7 gap-1 text-center text-[10px] font-bold text-slate-500 mb-2">
|
||||
<span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
|
||||
</div>
|
||||
<!-- Day Grid (filled dynamically with days by components.js on load) -->
|
||||
<div class="datepicker-days grid grid-cols-7 gap-1 text-center"></div>
|
||||
</div>
|
||||
</div></code></pre>
|
||||
<!-- Custom Date Picker using macro -->
|
||||
{{ "{{" }} ui::datepicker(id="my-datepicker", name="my_date", label="Date Picker", value="2026-05-30", display_text="May 30, 2026", data_year="2026", data_month="4") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Picker Code -->
|
||||
<!-- Time Picker Macro -->
|
||||
<div class="space-y-2">
|
||||
<span class="text-xs font-bold text-muted-foreground block">Time Picker HTML Structure</span>
|
||||
<span class="text-xs font-bold text-muted-foreground block">Time Picker Macro</span>
|
||||
<div class="relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 rounded-lg border border-border bg-popover/80 backdrop-blur text-[10px] font-semibold text-slate-455 hover:text-white hover:bg-secondary opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5" onclick="copyCodeSnippet(this)">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Time Picker Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Custom Time Picker Container
|
||||
- class "custom-timepicker": required for JavaScript target binding -->
|
||||
<div class="custom-timepicker relative inline-block w-full" id="unique-timepicker-id">
|
||||
<!-- Hidden input holds selected value (e.g., "12:00 PM") for form submission -->
|
||||
<input type="hidden" name="time_value" class="timepicker-value" value="12:00 PM">
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Trigger Button -->
|
||||
<button type="button" class="timepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200">
|
||||
<span class="timepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<!-- Time text label updated dynamically on pick -->
|
||||
<span class="timepicker-text">12:00 PM</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Time Picker Dropdown Menu -->
|
||||
<div class="timepicker-popover absolute left-0 z-20 mt-2 w-[230px] p-3 rounded-2xl border border-border bg-popover shadow-2xl hidden">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<!-- Hours Column (filled with <button>s 1 to 12 dynamically by components.js) -->
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Hr</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-hours"></div>
|
||||
</div>
|
||||
|
||||
<span class="text-slate-500 font-bold self-end mb-12">:</span>
|
||||
|
||||
<!-- Minutes Column (filled with <button>s 00 to 55 in 5m steps dynamically) -->
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Min</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-minutes"></div>
|
||||
</div>
|
||||
|
||||
<!-- AM/PM Selector -->
|
||||
<div class="flex flex-col items-center ml-1">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Am/Pm</span>
|
||||
<div class="flex flex-col gap-1 w-12">
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">AM</button>
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">PM</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></code></pre>
|
||||
<!-- Custom Time Picker using macro -->
|
||||
{{ "{{" }} ui::timepicker(id="my-timepicker", name="my_time", label="Time Picker", value="12:00 PM") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Form Fields & Select - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,45 +34,22 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="input-sandbox" class="wiki-pane space-y-4 max-w-md py-2">
|
||||
<!-- Text field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Username Input</label>
|
||||
<input type="text" placeholder="e.g. dev_alice" class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200">
|
||||
</div>
|
||||
<!-- Text field using macro -->
|
||||
{{ ui::text_input(id="demo-username", name="username", label="Username Input", type="text", placeholder="e.g. dev_alice") }}
|
||||
|
||||
<!-- Password field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Security Password</label>
|
||||
<input type="password" placeholder="••••••••" class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200">
|
||||
</div>
|
||||
<!-- Password field using macro -->
|
||||
{{ ui::text_input(id="demo-password", name="password", label="Security Password", type="password", placeholder="••••••••") }}
|
||||
|
||||
<!-- Textarea -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Task Details</label>
|
||||
<textarea rows="3" placeholder="Provide a detailed description of the task requirements..." class="block w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200 resize-none"></textarea>
|
||||
</div>
|
||||
<!-- Textarea using macro -->
|
||||
{{ ui::textarea(id="demo-details", name="details", label="Task Details", placeholder="Provide a detailed description of the task requirements...") }}
|
||||
|
||||
<!-- Custom Styled Select Dropdown -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Developer Specialization</label>
|
||||
<div class="custom-select relative inline-block w-full">
|
||||
<input type="hidden" name="specialization" class="select-value" value="Senior Rust Engineer">
|
||||
<button type="button" class="select-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary/50 transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="select-text">Senior Rust Engineer</span>
|
||||
<svg class="h-4 w-4 text-slate-500 transition-transform duration-200 select-chevron" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="select-popover absolute left-0 z-20 mt-2 w-full p-1 rounded-2xl border border-border bg-popover shadow-2xl hidden animate-in fade-in slide-in-from-top-2 duration-150">
|
||||
<div class="max-h-60 overflow-y-auto p-0.5 space-y-0.5">
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs bg-accent text-accent-foreground font-semibold focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-205 cursor-pointer select-none text-left" data-value="Senior Rust Engineer">Senior Rust Engineer</button>
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-205 cursor-pointer select-none text-left" data-value="Frontend Architect">Frontend Architect</button>
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-205 cursor-pointer select-none text-left" data-value="DevOps Lead">DevOps Lead</button>
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-205 cursor-pointer select-none text-left" data-value="Database Administrator">Database Administrator</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Custom Styled Select Dropdown using paired macros -->
|
||||
{{ ui::select_open(name="specialization", label="Developer Specialization", current_value="Senior Rust Engineer", current_text="Senior Rust Engineer") }}
|
||||
{{ ui::select_item(value="Senior Rust Engineer", label="Senior Rust Engineer", is_selected=true) }}
|
||||
{{ ui::select_item(value="Frontend Architect", label="Frontend Architect") }}
|
||||
{{ ui::select_item(value="DevOps Lead", label="DevOps Lead") }}
|
||||
{{ ui::select_item(value="Database Administrator", label="Database Administrator") }}
|
||||
{{ ui::select_close() }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
@@ -81,44 +59,23 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Styled Text Input -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Username Input</label>
|
||||
<input type="text" placeholder="e.g. dev_alice"
|
||||
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200">
|
||||
</div>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Styled Text Input -->
|
||||
{{ "{{" }} ui::text_input(id="demo-username", name="username", label="Username Input", type="text", placeholder="e.g. dev_alice") {{ "}}" }}
|
||||
|
||||
<!-- Styled Password Input -->
|
||||
{{ "{{" }} ui::text_input(id="demo-password", name="password", label="Security Password", type="password", placeholder="••••••••") {{ "}}" }}
|
||||
|
||||
<!-- Styled Textarea -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Task Details</label>
|
||||
<textarea rows="3" placeholder="Provide a detailed description..."
|
||||
class="block w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200 resize-none"></textarea>
|
||||
</div>
|
||||
{{ "{{" }} ui::textarea(id="demo-details", name="details", label="Task Details", placeholder="Provide a detailed description...") {{ "}}" }}
|
||||
|
||||
<!-- Custom Styled Select Dropdown (Chevron and Popover managed globally in components.js) -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Developer Specialization</label>
|
||||
<div class="custom-select relative inline-block w-full">
|
||||
<!-- Hidden input holds the actual value for form submissions -->
|
||||
<input type="hidden" name="specialization" class="select-value" value="Senior Rust Engineer">
|
||||
|
||||
<!-- Toggle trigger button -->
|
||||
<button type="button" class="select-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200">
|
||||
<span class="select-text">Senior Rust Engineer</span>
|
||||
<svg class="h-4 w-4 text-slate-500 transition-transform duration-200 select-chevron" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Options Popover -->
|
||||
<div class="select-popover absolute left-0 z-20 mt-2 w-full p-1 rounded-2xl border border-border bg-popover shadow-2xl hidden">
|
||||
<div class="max-h-60 overflow-y-auto p-0.5 space-y-0.5">
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs bg-accent text-accent-foreground font-semibold text-slate-200 text-left" data-value="Senior Rust Engineer">Senior Rust Engineer</button>
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs hover:bg-accent hover:text-accent-foreground text-slate-200 text-left" data-value="Frontend Architect">Frontend Architect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></code></pre>
|
||||
<!-- Custom Styled Select Dropdown -->
|
||||
{{ "{{" }} ui::select_open(name="specialization", label="Developer Specialization", current_value="Senior Rust Engineer", current_text="Senior Rust Engineer") {{ "}}" }}
|
||||
{{ "{{" }} ui::select_item(value="Senior Rust Engineer", label="Senior Rust Engineer", is_selected=true) {{ "}}" }}
|
||||
{{ "{{" }} ui::select_item(value="Frontend Architect", label="Frontend Architect") {{ "}}" }}
|
||||
{{ "{{" }} ui::select_item(value="DevOps Lead", label="DevOps Lead") {{ "}}" }}
|
||||
{{ "{{" }} ui::select_close() {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
{% macro button(label, variant="primary", type="button", extra_class="", disabled=false) %}
|
||||
<button
|
||||
type="{{ type }}"
|
||||
{% if disabled %}disabled{% endif %}
|
||||
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
|
||||
{% if variant == "primary" %}
|
||||
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
|
||||
{% elif variant == "secondary" %}
|
||||
bg-secondary text-secondary-foreground hover:bg-secondary/80
|
||||
{% elif variant == "outline" %}
|
||||
border border-border bg-transparent hover:bg-secondary text-slate-200
|
||||
{% elif variant == "destructive" %}
|
||||
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
|
||||
{% elif variant == "indigo" %}
|
||||
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
|
||||
{% endif %}
|
||||
{% if disabled %}opacity-50 cursor-not-allowed active:scale-100{% endif %}
|
||||
{{ extra_class }}"
|
||||
>
|
||||
{{ label|safe }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal_trigger(target_id, label, variant="primary", extra_class="") %}
|
||||
<button
|
||||
data-modal-target="{{ target_id }}"
|
||||
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
|
||||
{% if variant == "primary" %}
|
||||
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
|
||||
{% elif variant == "secondary" %}
|
||||
bg-secondary text-secondary-foreground hover:bg-secondary/80
|
||||
{% elif variant == "outline" %}
|
||||
border border-border bg-transparent hover:bg-secondary text-slate-200
|
||||
{% elif variant == "destructive" %}
|
||||
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
|
||||
{% elif variant == "indigo" %}
|
||||
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
|
||||
{% endif %}
|
||||
{{ extra_class }}"
|
||||
>
|
||||
{{ label|safe }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro sheet_trigger(target_id, label, variant="primary", extra_class="") %}
|
||||
<button
|
||||
data-sheet-target="{{ target_id }}"
|
||||
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
|
||||
{% if variant == "primary" %}
|
||||
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
|
||||
{% elif variant == "secondary" %}
|
||||
bg-secondary text-secondary-foreground hover:bg-secondary/80
|
||||
{% elif variant == "outline" %}
|
||||
border border-border bg-transparent hover:bg-secondary text-slate-200
|
||||
{% elif variant == "destructive" %}
|
||||
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
|
||||
{% elif variant == "indigo" %}
|
||||
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
|
||||
{% endif %}
|
||||
{{ extra_class }}"
|
||||
>
|
||||
{{ label|safe }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal(id, title, content, close_label="Close") %}
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-100">{{ title }}</h3>
|
||||
<div class="text-xs text-slate-400 mt-2 leading-relaxed">{{ content|safe }}</div>
|
||||
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border hover:bg-secondary transition text-xs font-semibold text-slate-200">{{ close_label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal_open(id, title) %}
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-100 mb-2">{{ title }}</h3>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal_close(close_label="Dismiss Modal") %}
|
||||
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border hover:bg-secondary transition text-xs font-semibold text-slate-200">{{ close_label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro sheet_open(id, title, max_width_class="max-w-sm") %}
|
||||
<div id="{{ id }}" class="sheet-dialog fixed inset-0 z-50 overflow-hidden hidden" role="dialog" aria-modal="true">
|
||||
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm opacity-0 transition-opacity duration-300 animate-fade-in"></div>
|
||||
<div class="absolute inset-y-0 right-0 max-w-full flex pl-10">
|
||||
<div class="sheet-content w-screen {{ max_width_class }} translate-x-full transition-transform duration-300 ease-in-out border-l border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl flex flex-col justify-between">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-border">
|
||||
<h3 class="text-sm font-bold text-slate-100">{{ title }}</h3>
|
||||
<button class="sheet-close text-slate-500 hover:text-white rounded-lg p-1.5 hover:bg-secondary transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro sheet_close(save_label="Save") %}
|
||||
</div>
|
||||
{% if !save_label.is_empty() %}
|
||||
<button class="sheet-close w-full py-2.5 rounded-xl bg-indigo-650 hover:bg-indigo-600 transition text-xs font-bold text-white shadow-lg shadow-indigo-650/10">{{ save_label }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro search_combobox(name, label, placeholder="Search...", search_url="/developers/search", input_id="combobox-search", value_id="combobox-value") %}
|
||||
<div class="autocomplete-combobox relative">
|
||||
{% if !label.is_empty() %}
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1.5">{{ label }}</label>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hidden input holding the actual selected ID to submit in the form -->
|
||||
<input type="hidden" id="{{ value_id }}" name="{{ name }}" class="combobox-value">
|
||||
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
id="{{ input_id }}"
|
||||
name="q"
|
||||
placeholder="{{ placeholder }}"
|
||||
autocomplete="off"
|
||||
hx-get="{{ search_url }}"
|
||||
hx-trigger="input changed delay:250ms, search"
|
||||
hx-target="next .combobox-results"
|
||||
hx-indicator="next .combobox-indicator"
|
||||
class="combobox-input appearance-none rounded-xl relative block w-full pl-9 pr-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
|
||||
|
||||
<!-- Search Icon & Loading Indicator -->
|
||||
<div class="absolute left-3 top-3 text-slate-500">
|
||||
<svg class="combobox-indicator htmx-indicator animate-spin h-4 w-4 text-sky-500 hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results Dropdown Popover -->
|
||||
<div class="combobox-results absolute z-10 w-full mt-1.5 bg-slate-900 border border-slate-800 rounded-xl shadow-2xl overflow-hidden hidden">
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro text_input(id, name, label, type="text", placeholder="", value="", required=false, extra_class="") %}
|
||||
<div class="space-y-2 {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<label for="{{ id }}" class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
|
||||
{% endif %}
|
||||
<input
|
||||
id="{{ id }}"
|
||||
name="{{ name }}"
|
||||
type="{{ type }}"
|
||||
value="{{ value }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
{% if required %}required{% endif %}
|
||||
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200"
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro textarea(id, name, label, placeholder="", value="", rows="3", required=false, extra_class="") %}
|
||||
<div class="space-y-2 {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<label for="{{ id }}" class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
|
||||
{% endif %}
|
||||
<textarea
|
||||
id="{{ id }}"
|
||||
name="{{ name }}"
|
||||
rows="{{ rows }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
{% if required %}required{% endif %}
|
||||
class="block w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200 resize-none"
|
||||
>{{ value }}</textarea>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro select_open(name, label, current_value, current_text) %}
|
||||
<div class="space-y-2">
|
||||
{% if !label.is_empty() %}
|
||||
<label class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="custom-select relative inline-block w-full">
|
||||
<input type="hidden" name="{{ name }}" class="select-value" value="{{ current_value }}">
|
||||
<button type="button" class="select-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary/50 transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="select-text">{{ current_text }}</span>
|
||||
<svg class="h-4 w-4 text-slate-500 transition-transform duration-200 select-chevron" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="select-popover absolute left-0 z-20 mt-2 w-full p-1 rounded-2xl border border-border bg-popover shadow-2xl hidden animate-in fade-in slide-in-from-top-2 duration-150">
|
||||
<div class="max-h-60 overflow-y-auto p-0.5 space-y-0.5">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro select_item(value, label, is_selected=false) %}
|
||||
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs {% if is_selected %}bg-accent text-accent-foreground font-semibold{% else %}hover:bg-accent hover:text-accent-foreground{% endif %} focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-200 cursor-pointer select-none text-left" data-value="{{ value }}">{{ label }}</button>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro select_close() %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro toggle_switch(name, label, checked=false, extra_class="") %}
|
||||
<div class="flex items-center justify-between {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<span class="text-xs text-muted-foreground font-medium">{{ label }}</span>
|
||||
{% endif %}
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="{{ name }}" class="sr-only peer" {% if checked %}checked{% endif %}>
|
||||
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-slate-400 after:rounded-full after:h-[14px] after:w-[14px] after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
|
||||
</label>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro checkbox(name, label, checked=false, extra_class="") %}
|
||||
<label class="flex items-center gap-3 cursor-pointer group {{ extra_class }}">
|
||||
<input type="checkbox" name="{{ name }}" class="sr-only peer" {% if checked %}checked{% endif %}>
|
||||
<div class="w-4 h-4 rounded bg-popover border border-border flex items-center justify-center peer-checked:bg-indigo-600 peer-checked:border-indigo-500 peer-checked:[&_svg]:opacity-100 transition">
|
||||
<svg class="w-2.5 h-2.5 text-white opacity-0 transition-opacity" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="4"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||
</div>
|
||||
{% if !label.is_empty() %}
|
||||
<span class="text-xs text-muted-foreground peer-checked:text-slate-200 select-none">{{ label }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro range_slider(name, label, min="0", max="100", value="50", extra_class="") %}
|
||||
<div class="space-y-2 {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<label class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="range" name="{{ name }}" min="{{ min }}" max="{{ max }}" value="{{ value }}" class="grow h-1 bg-secondary rounded-lg appearance-none cursor-pointer accent-indigo-600" oninput="this.nextElementSibling.textContent = this.value + '%'">
|
||||
<span class="text-xs font-mono font-bold text-sky-400 w-10 text-right">{{ value }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro datepicker(id, name, label, value="2026-05-30", display_text="May 30, 2026", data_year="2026", data_month="4", extra_class="") %}
|
||||
<div class="space-y-2 {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<label class="block text-xs font-semibold text-slate-400">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="custom-datepicker relative inline-block w-full" id="{{ id }}" data-year="{{ data_year }}" data-month="{{ data_month }}">
|
||||
<input type="hidden" name="{{ name }}" class="datepicker-value" value="{{ value }}">
|
||||
<button type="button" class="datepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="datepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span class="datepicker-text">{{ display_text }}</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-[270px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex items-center justify-between mb-3.5">
|
||||
<button type="button" class="datepicker-prev p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span class="datepicker-month-year text-xs font-bold text-slate-200"></span>
|
||||
<button type="button" class="datepicker-next p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-1 text-center text-[10px] font-bold text-slate-500 mb-2">
|
||||
<span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
|
||||
</div>
|
||||
<div class="datepicker-days grid grid-cols-7 gap-1 text-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timepicker(id, name, label, value="12:00 PM", extra_class="") %}
|
||||
<div class="space-y-2 {{ extra_class }}">
|
||||
{% if !label.is_empty() %}
|
||||
<label class="block text-xs font-semibold text-slate-405">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="custom-timepicker relative inline-block w-full" id="{{ id }}">
|
||||
<input type="hidden" name="{{ name }}" class="timepicker-value" value="{{ value }}">
|
||||
<button type="button" class="timepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="timepicker-label flex items-center gap-2">
|
||||
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="timepicker-text">{{ value }}</span>
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="timepicker-popover absolute right-0 sm:left-0 z-20 mt-2 w-[230px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Hr</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-hours"></div>
|
||||
</div>
|
||||
<span class="text-slate-500 font-bold self-end mb-12">:</span>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Min</span>
|
||||
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-minutes"></div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center ml-1">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Am/Pm</span>
|
||||
<div class="flex flex-col gap-1 w-12">
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">AM</button>
|
||||
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">PM</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tabs_header_open() %}
|
||||
<div class="flex border-b border-border">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tab_trigger(group, target_id, label, is_active=false) %}
|
||||
<button
|
||||
type="button"
|
||||
data-tab-group="{{ group }}"
|
||||
data-tab-target="{{ target_id }}"
|
||||
class="px-3 py-1.5 text-xs font-semibold border-b-2 focus:outline-none transition-all
|
||||
{% if is_active %}border-sky-500 text-sky-400{% else %}border-transparent text-muted-foreground hover:text-slate-200{% endif %}"
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tabs_header_close() %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tabs_content_open() %}
|
||||
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs text-muted-foreground min-h-[5rem]">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tab_pane_open(group, id, is_active=true) %}
|
||||
<div id="{{ id }}" data-tab-content-group="{{ group }}" {% if !is_active %}class="hidden"{% endif %}>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tab_pane_close() %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tabs_content_close() %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro accordion_open(title, is_open=false) %}
|
||||
<div class="accordion-item border border-border rounded-xl overflow-hidden bg-card/30">
|
||||
<button type="button" class="accordion-trigger w-full flex items-center justify-between px-4 py-3 text-xs font-bold text-slate-200 hover:bg-secondary/50 transition focus:outline-none">
|
||||
<span>{{ title }}</span>
|
||||
<svg class="accordion-chevron h-3 w-3 text-slate-500 transition-transform duration-200 {% if is_open %}rotate-180{% endif %}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="accordion-content px-4 pb-3 pt-1 text-xs text-muted-foreground {% if !is_open %}hidden{% endif %} border-t border-border leading-relaxed">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro accordion_close() %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Dialog Modals - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,9 +34,7 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="modal-sandbox" class="wiki-pane py-2">
|
||||
<button data-modal-target="wiki-demo-modal" class="inline-flex items-center justify-center rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2.5 transition active:scale-95">
|
||||
Open Modal Dialog
|
||||
</button>
|
||||
{{ ui::modal_trigger(target_id="wiki-demo-modal", label="Open Modal Dialog", variant="indigo") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
@@ -45,27 +44,18 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Trigger Button (points to modal element ID) -->
|
||||
<button data-modal-target="my-modal-id" class="px-4 py-2 bg-indigo-600 text-white text-xs font-bold rounded-xl">
|
||||
Open Modal
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Modal Overlay Element -->
|
||||
<div id="my-modal-id" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<!-- Backdrop shadow with blur -->
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<!-- Trigger Button (points to modal element ID) -->
|
||||
{{ "{{" }} ui::modal_trigger(target_id="my-modal-id", label="Open Modal Dialog", variant="indigo") {{ "}}" }}
|
||||
|
||||
<!-- Modal Panel with scale / opacity transition -->
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl">
|
||||
<h3 class="text-sm font-bold text-slate-100">Confirmation Title</h3>
|
||||
<p class="text-xs text-muted-foreground mt-2 leading-relaxed">Are you sure you want to proceed?</p>
|
||||
<!-- Option A: Simple Modal (Self-contained with text body) -->
|
||||
{{ "{{" }} ui::modal(id="my-modal-id", title="Confirmation Title", content="Are you sure you want to proceed?", close_label="Cancel") {{ "}}" }}
|
||||
|
||||
<!-- Close triggers require class 'modal-close' -->
|
||||
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border text-slate-200 text-xs font-semibold">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div></code></pre>
|
||||
<!-- Option B: Paired Macros (For custom markup, inputs, or headers) -->
|
||||
{{ "{{" }} ui::modal_open(id="my-modal-id", title="Confirmation Title") {{ "}}" }}
|
||||
<p class="text-xs text-muted-foreground mt-2 leading-relaxed">Are you sure you want to proceed?</p>
|
||||
{{ "{{" }} ui::modal_close(close_label="Cancel") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,20 +126,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Modal Sandbox Element -->
|
||||
<div id="wiki-demo-modal" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-100">Wiki Sandbox Modal</h3>
|
||||
<p class="text-xs text-slate-405 mt-2 leading-relaxed">This modal is animated and handled globally by <code>components.js</code>. Pressing escape or clicking outside closes it instantly.</p>
|
||||
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border hover:bg-secondary transition text-xs font-semibold text-slate-200">Dismiss Modal</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Interactive Modal Sandbox Element using paired macros -->
|
||||
{{ ui::modal_open(id="wiki-demo-modal", title="Wiki Sandbox Modal") }}
|
||||
<p class="text-xs text-slate-405 mt-2 leading-relaxed">This modal is animated and handled globally by <code>components.js</code>. Pressing escape or clicking outside closes it instantly.</p>
|
||||
{{ ui::modal_close(close_label="Dismiss Modal") }}
|
||||
|
||||
<script>
|
||||
function toggleWikiTabs(btn, paneId) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Slide-over Drawers (Sheets) - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,9 +34,7 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="sheet-sandbox" class="wiki-pane py-2">
|
||||
<button data-sheet-target="wiki-demo-sheet" class="inline-flex items-center justify-center rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2.5 transition active:scale-95">
|
||||
Open Right Drawer
|
||||
</button>
|
||||
{{ ui::sheet_trigger(target_id="wiki-demo-sheet", label="Open Right Drawer", variant="indigo") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
@@ -45,34 +44,15 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- Trigger Button (points to sheet element ID) -->
|
||||
<button data-sheet-target="my-sheet-id" class="px-4 py-2 bg-indigo-650 text-white text-xs font-bold rounded-xl">
|
||||
Open Drawer
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Slide Drawer Sheet Element -->
|
||||
<div id="my-sheet-id" class="sheet-dialog fixed inset-0 z-50 overflow-hidden hidden" role="dialog" aria-modal="true">
|
||||
<!-- Backdrop -->
|
||||
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm opacity-0 transition-opacity duration-300"></div>
|
||||
<!-- Trigger Button (points to sheet element ID) -->
|
||||
{{ "{{" }} ui::sheet_trigger(target_id="my-sheet-id", label="Open Drawer", variant="indigo") {{ "}}" }}
|
||||
|
||||
<div class="absolute inset-y-0 right-0 max-w-full flex pl-10">
|
||||
<!-- Panel with transition translate-x-full -->
|
||||
<div class="sheet-content w-screen max-w-sm translate-x-full transition-transform duration-300 bg-popover/95 border-l border-border p-6 flex flex-col justify-between">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-border">
|
||||
<h3 class="text-sm font-bold text-slate-100">Settings</h3>
|
||||
<button class="sheet-close text-slate-500 hover:text-white">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Drawer Body Content</p>
|
||||
</div>
|
||||
<button class="sheet-close w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-xl">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div></code></pre>
|
||||
<!-- Slide Drawer Sheet Element using paired macros -->
|
||||
{{ "{{" }} ui::sheet_open(id="my-sheet-id", title="Settings", max_width_class="max-w-sm") {{ "}}" }}
|
||||
<p class="text-xs text-muted-foreground">Drawer Body Content</p>
|
||||
{{ "{{" }} ui::sheet_close(save_label="Save") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,24 +123,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Sheet Sandbox Element -->
|
||||
<div id="wiki-demo-sheet" class="sheet-dialog fixed inset-0 z-50 overflow-hidden hidden" role="dialog" aria-modal="true">
|
||||
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm opacity-0 transition-opacity duration-300 animate-fade-in"></div>
|
||||
<div class="absolute inset-y-0 right-0 max-w-full flex pl-10">
|
||||
<div class="sheet-content w-screen max-w-sm translate-x-full transition-transform duration-300 ease-in-out border-l border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl flex flex-col justify-between">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-border">
|
||||
<h3 class="text-sm font-bold text-slate-100">Drawer Panel Properties</h3>
|
||||
<button class="sheet-close text-slate-500 hover:text-white rounded-lg p-1.5 hover:bg-secondary transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-405 leading-relaxed">This slide-over panel demonstrates real-time sidebar parameter adjustments, sliding in from the right edge with hardware transitions.</p>
|
||||
</div>
|
||||
<button class="sheet-close w-full py-2.5 rounded-xl bg-indigo-650 hover:bg-indigo-600 transition text-xs font-bold text-white shadow-lg shadow-indigo-650/10">Save Properties</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Interactive Sheet Sandbox Element using paired macros -->
|
||||
{{ ui::sheet_open(id="wiki-demo-sheet", title="Drawer Panel Properties", max_width_class="max-w-sm") }}
|
||||
<p class="text-xs text-slate-405 leading-relaxed">This slide-over panel demonstrates real-time sidebar parameter adjustments, sliding in from the right edge with hardware transitions.</p>
|
||||
{{ ui::sheet_close(save_label="Save Properties") }}
|
||||
|
||||
<script>
|
||||
function toggleWikiTabs(btn, paneId) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Tabs & Accordions - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,36 +34,32 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="tabs-sandbox" class="wiki-pane space-y-6 max-w-md py-2">
|
||||
<!-- Tabs Showcase -->
|
||||
<!-- Tabs Showcase using macros -->
|
||||
<div class="space-y-2">
|
||||
<span class="block text-xs font-semibold text-muted-foreground">Switch Tabs</span>
|
||||
<div class="flex border-b border-border">
|
||||
<button type="button" data-tab-group="wiki-tabs" data-tab-target="wiki-pane-1" class="px-4 py-2 text-xs font-semibold border-b-2 border-sky-500 text-sky-400 focus:outline-none">Overview</button>
|
||||
<button type="button" data-tab-group="wiki-tabs" data-tab-target="wiki-pane-2" class="px-4 py-2 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-slate-250 focus:outline-none">Config Settings</button>
|
||||
</div>
|
||||
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs text-muted-foreground min-h-[5rem]">
|
||||
<div id="wiki-pane-1" data-tab-content-group="wiki-tabs">
|
||||
{{ ui::tabs_header_open() }}
|
||||
{{ ui::tab_trigger(group="wiki-tabs", target_id="wiki-pane-1", label="Overview", is_active=true) }}
|
||||
{{ ui::tab_trigger(group="wiki-tabs", target_id="wiki-pane-2", label="Config Settings") }}
|
||||
{{ ui::tabs_header_close() }}
|
||||
|
||||
{{ ui::tabs_content_open() }}
|
||||
{{ ui::tab_pane_open(group="wiki-tabs", id="wiki-pane-1", is_active=true) }}
|
||||
Overview parameters content pane. You can place statistics, graphs, or summary tables here.
|
||||
</div>
|
||||
<div id="wiki-pane-2" data-tab-content-group="wiki-tabs" class="hidden">
|
||||
{{ ui::tab_pane_close() }}
|
||||
|
||||
{{ ui::tab_pane_open(group="wiki-tabs", id="wiki-pane-2", is_active=false) }}
|
||||
Settings updates pane. Configuration toggles, environment variables, or webhook URLs live here.
|
||||
</div>
|
||||
</div>
|
||||
{{ ui::tab_pane_close() }}
|
||||
{{ ui::tabs_content_close() }}
|
||||
</div>
|
||||
|
||||
<!-- Accordion Showcase -->
|
||||
<!-- Accordion Showcase using paired macros -->
|
||||
<div class="space-y-2">
|
||||
<span class="block text-xs font-semibold text-muted-foreground">Accordion Collapsible</span>
|
||||
<div class="space-y-2">
|
||||
<div class="accordion-item border border-border rounded-xl overflow-hidden bg-card/30">
|
||||
<button type="button" class="accordion-trigger w-full flex items-center justify-between px-4 py-3 text-xs font-bold text-slate-200 hover:bg-secondary/50 transition focus:outline-none">
|
||||
<span>Is this library dependency-free?</span>
|
||||
<svg class="accordion-chevron h-3 w-3 text-slate-500 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="accordion-content px-4 pb-3 pt-1 text-xs text-muted-foreground hidden border-t border-border leading-relaxed">
|
||||
Yes! The template does not rely on Alpine.js or React. JavaScript toggles run via vanilla click listeners listening on specific class selectors.
|
||||
</div>
|
||||
</div>
|
||||
{{ ui::accordion_open(title="Is this library dependency-free?", is_open=false) }}
|
||||
Yes! The template does not rely on Alpine.js or React. JavaScript toggles run via vanilla click listeners listening on specific class selectors.
|
||||
{{ ui::accordion_close() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,45 +71,28 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- 1. Tabs Layout Markup -->
|
||||
<div class="space-y-2">
|
||||
<!-- Tabs Trigger Header -->
|
||||
<div class="flex border-b border-border">
|
||||
<!-- Include class 'border-sky-500 text-sky-400' on the active trigger -->
|
||||
<button data-tab-group="my-tabs-group" data-tab-target="my-pane-1" class="px-4 py-2 border-b-2 border-sky-500 text-sky-400 text-xs font-semibold">
|
||||
Tab 1
|
||||
</button>
|
||||
<button data-tab-group="my-tabs-group" data-tab-target="my-pane-2" class="px-4 py-2 border-b-2 border-transparent text-muted-foreground hover:text-slate-200 text-xs font-semibold">
|
||||
Tab 2
|
||||
</button>
|
||||
</div>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- Content Panes -->
|
||||
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs">
|
||||
<div id="my-pane-1" data-tab-content-group="my-tabs-group">
|
||||
Overview content...
|
||||
</div>
|
||||
<div id="my-pane-2" data-tab-content-group="my-tabs-group" class="hidden">
|
||||
Settings content...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 1. Tabs Layout using paired macros -->
|
||||
{{ "{{" }} ui::tabs_header_open() {{ "}}" }}
|
||||
{{ "{{" }} ui::tab_trigger(group="my-tabs-group", target_id="my-pane-1", label="Tab 1", is_active=true) {{ "}}" }}
|
||||
{{ "{{" }} ui::tab_trigger(group="my-tabs-group", target_id="my-pane-2", label="Tab 2") {{ "}}" }}
|
||||
{{ "{{" }} ui::tabs_header_close() {{ "}}" }}
|
||||
|
||||
<!-- 2. Accordion Markup -->
|
||||
<div class="accordion-item border border-border rounded-xl bg-card/30 overflow-hidden">
|
||||
<!-- Accordion Button Trigger -->
|
||||
<button class="accordion-trigger w-full flex items-center justify-between px-4 py-3 text-xs font-bold text-slate-200 hover:bg-secondary/50">
|
||||
<span>Section Title</span>
|
||||
<svg class="accordion-chevron h-3 w-3 text-slate-500 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{ "{{" }} ui::tabs_content_open() {{ "}}" }}
|
||||
{{ "{{" }} ui::tab_pane_open(group="my-tabs-group", id="my-pane-1", is_active=true) {{ "}}" }}
|
||||
Overview content...
|
||||
{{ "{{" }} ui::tab_pane_close() {{ "}}" }}
|
||||
|
||||
<!-- Content Panel (Hidden by default) -->
|
||||
<div class="accordion-content px-4 pb-3 pt-1 text-xs text-muted-foreground hidden border-t border-border">
|
||||
Collapsible description content.
|
||||
</div>
|
||||
</div></code></pre>
|
||||
{{ "{{" }} ui::tab_pane_open(group="my-tabs-group", id="my-pane-2", is_active=false) {{ "}}" }}
|
||||
Settings content...
|
||||
{{ "{{" }} ui::tab_pane_close() {{ "}}" }}
|
||||
{{ "{{" }} ui::tabs_content_close() {{ "}}" }}
|
||||
|
||||
<!-- 2. Accordion using paired macros -->
|
||||
{{ "{{" }} ui::accordion_open(title="Section Title", is_open=false) {{ "}}" }}
|
||||
Collapsible description content.
|
||||
{{ "{{" }} ui::accordion_close() {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Switches & Checkboxes - Design System Wiki{% endblock %}
|
||||
|
||||
@@ -33,34 +34,16 @@
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="toggle-sandbox" class="wiki-pane space-y-5 max-w-xs py-2">
|
||||
<!-- Toggle Switch -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-muted-foreground font-medium">Toggle Status</span>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer">
|
||||
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-slate-400 after:rounded-full after:h-[14px] after:w-[14px] after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
|
||||
</label>
|
||||
</div>
|
||||
<!-- Toggle Switch using macro -->
|
||||
{{ ui::toggle_switch(name="demo_toggle", label="Toggle Status", checked=false) }}
|
||||
|
||||
<!-- Custom Checkbox -->
|
||||
<!-- Custom Checkbox using macro -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<input type="checkbox" class="sr-only peer" checked>
|
||||
<div class="w-4 h-4 rounded bg-popover border border-border flex items-center justify-center peer-checked:bg-indigo-600 peer-checked:border-indigo-500 peer-checked:[&_svg]:opacity-100 transition">
|
||||
<svg class="w-2.5 h-2.5 text-white opacity-0 transition-opacity" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="4"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground peer-checked:text-slate-200">Enable Email notifications</span>
|
||||
</label>
|
||||
{{ ui::checkbox(name="demo_checkbox", label="Enable Email notifications", checked=true) }}
|
||||
</div>
|
||||
|
||||
<!-- Range Slider -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-muted-foreground">Range Slider (0-100%)</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="range" min="0" max="100" value="50" class="grow h-1 bg-secondary rounded-lg appearance-none cursor-pointer accent-indigo-600" oninput="this.nextElementSibling.textContent = this.value + '%'">
|
||||
<span class="text-xs font-mono font-bold text-sky-400 w-10 text-right">50%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Range Slider using macro -->
|
||||
{{ ui::range_slider(name="demo_slider", label="Range Slider (0-100%)", min="0", max="100", value="50") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
@@ -70,34 +53,16 @@
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code><!-- 1. Toggle Switch Variant -->
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<!-- sr-only: visually hides native input; peer: lets siblings styled with 'peer-checked:' react to its state -->
|
||||
<input type="checkbox" class="sr-only peer">
|
||||
<!-- bg-secondary: base slider bg; peer-checked:bg-indigo-600: checked slider bg;
|
||||
after: absolute round knob dot; peer-checked:after:translate-x-4: moves knob when checked -->
|
||||
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-slate-400 after:rounded-full after:h-[14px] after:w-[14px] after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
|
||||
</label>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
<!-- 1. Toggle Switch Variant -->
|
||||
{{ "{{" }} ui::toggle_switch(name="demo_toggle", label="Toggle Status", checked=false) {{ "}}" }}
|
||||
|
||||
<!-- 2. Custom Checkbox -->
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<!-- sr-only peer hides checkbox input globally but exposes its state -->
|
||||
<input type="checkbox" class="sr-only peer">
|
||||
<!-- peer-checked:[&_svg]:opacity-100: uses a descendant selector to show the checkmark when checked -->
|
||||
<div class="w-4 h-4 rounded bg-popover border border-border flex items-center justify-center peer-checked:bg-indigo-600 peer-checked:border-indigo-500 peer-checked:[&_svg]:opacity-100 transition">
|
||||
<svg class="w-2.5 h-2.5 text-white opacity-0 transition-opacity" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground peer-checked:text-slate-200 select-none">Label Option</span>
|
||||
</label>
|
||||
{{ "{{" }} ui::checkbox(name="demo_checkbox", label="Enable Email notifications", checked=true) {{ "}}" }}
|
||||
|
||||
<!-- 3. Custom Range Slider (accent-indigo-600 styles input thumb in modern browsers) -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- oninput: updates text element sibling content to display slider value -->
|
||||
<input type="range" min="0" max="100" value="50" class="grow h-1 bg-secondary rounded-lg appearance-none cursor-pointer accent-indigo-600" oninput="this.nextElementSibling.textContent = this.value + '%'">
|
||||
<span class="text-xs font-mono font-bold text-sky-400 w-10 text-right">50%</span>
|
||||
</div></code></pre>
|
||||
<!-- 3. Custom Range Slider -->
|
||||
{{ "{{" }} ui::range_slider(name="demo_slider", label="Range Slider (0-100%)", min="0", max="100", value="50") {{ "}}" }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Edit Developer - Stick{% endblock %}
|
||||
|
||||
@@ -13,28 +14,17 @@
|
||||
</div>
|
||||
|
||||
<form action="/developers/{{ developer.id.unwrap().to_hex() }}/edit" method="post" class="space-y-5">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-slate-400 mb-1.5">Name</label>
|
||||
<input id="name" name="name" type="text" value="{{ developer.name }}" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
|
||||
</div>
|
||||
{{ ui::text_input(id="name", name="name", label="Name", type="text", placeholder="Name", value=developer.name, required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-slate-400 mb-1.5">Email</label>
|
||||
<input id="email" name="email" type="email" value="{{ developer.email }}" required class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
|
||||
</div>
|
||||
{{ ui::text_input(id="email", name="email", label="Email", type="email", placeholder="Email", value=developer.email, required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="skills" class="block text-sm font-medium text-slate-400 mb-1.5">Skills (Comma-separated)</label>
|
||||
<input id="skills" name="skills" type="text" value='{{ developer.skills.join(", ") }}' class="appearance-none rounded-xl relative block w-full px-4 py-3 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
|
||||
</div>
|
||||
{{ ui::text_input(id="skills", name="skills", label="Skills (Comma-separated)", type="text", placeholder="Skills", value=developer.skills.join(", ")) }}
|
||||
|
||||
<div class="flex gap-4 pt-2">
|
||||
<a href="/developers" class="flex-1 py-3 px-4 text-center text-sm font-semibold rounded-xl text-slate-300 bg-slate-900 border border-slate-800 hover:border-slate-700 transition">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="flex-1 py-3 px-4 text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-lg shadow-sky-500/10">
|
||||
Save Changes
|
||||
</button>
|
||||
{{ ui::button(label="Save Changes", variant="indigo", type="submit", extra_class="flex-1 py-3 bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-lg shadow-sky-500/10") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Developers - Stick{% endblock %}
|
||||
|
||||
@@ -30,24 +31,13 @@
|
||||
Add Developer
|
||||
</h3>
|
||||
<form action="/developers" method="post" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-xs font-semibold text-slate-400 mb-1.5">Name</label>
|
||||
<input id="name" name="name" type="text" required class="appearance-none rounded-xl relative block w-full px-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="e.g. Alice Smith">
|
||||
</div>
|
||||
{{ ui::text_input(id="name", name="name", label="Name", type="text", placeholder="e.g. Alice Smith", required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-xs font-semibold text-slate-400 mb-1.5">Email</label>
|
||||
<input id="email" name="email" type="email" required class="appearance-none rounded-xl relative block w-full px-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="e.g. alice@company.com">
|
||||
</div>
|
||||
{{ ui::text_input(id="email", name="email", label="Email", type="email", placeholder="e.g. alice@company.com", required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="skills" class="block text-xs font-semibold text-slate-400 mb-1.5">Skills (Comma-separated)</label>
|
||||
<input id="skills" name="skills" type="text" class="appearance-none rounded-xl relative block w-full px-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="e.g. Rust, Axum, MongoDB">
|
||||
</div>
|
||||
{{ ui::text_input(id="skills", name="skills", label="Skills (Comma-separated)", type="text", placeholder="e.g. Rust, Axum, MongoDB") }}
|
||||
|
||||
<button type="submit" class="w-full py-3 px-4 text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-md shadow-sky-500/10">
|
||||
Create Developer
|
||||
</button>
|
||||
{{ ui::button(label="Create Developer", variant="indigo", type="submit", extra_class="w-full py-3 bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-md shadow-sky-500/10") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Tasks Dashboard - Stick{% endblock %}
|
||||
|
||||
@@ -15,6 +16,8 @@
|
||||
<span class="text-xs font-semibold px-3 py-1.5 rounded-xl bg-slate-900 border border-slate-800 text-slate-300">
|
||||
Total Tasks: {{ tasks.len() }}
|
||||
</span>
|
||||
<!-- Info trigger button -->
|
||||
{{ ui::modal_trigger(target_id="task-info-modal", label="<svg class='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'/></svg>", variant="outline", extra_class="px-2.5 py-2.5") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,58 +33,17 @@
|
||||
New Task
|
||||
</h3>
|
||||
<form action="/tasks/create" method="post" class="space-y-4">
|
||||
<div>
|
||||
<label for="title" class="block text-xs font-semibold text-slate-400 mb-1.5">Title</label>
|
||||
<input id="title" name="title" type="text" required class="appearance-none rounded-xl relative block w-full px-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="Task name">
|
||||
</div>
|
||||
<!-- Task Title Input using macro -->
|
||||
{{ ui::text_input(id="title", name="title", label="Title", type="text", placeholder="Task name", required=true) }}
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-xs font-semibold text-slate-400 mb-1.5">Description (Optional)</label>
|
||||
<textarea id="description" name="description" rows="3" class="appearance-none rounded-xl relative block w-full px-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm" placeholder="Add some context..."></textarea>
|
||||
</div>
|
||||
<!-- Task Description TextArea using macro -->
|
||||
{{ ui::textarea(id="description", name="description", label="Description (Optional)", placeholder="Add some context...") }}
|
||||
|
||||
<!-- Interactive Assignee Search -->
|
||||
<div class="autocomplete-combobox relative">
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1.5">Assignee (Optional)</label>
|
||||
<!-- Interactive Assignee Search using macro -->
|
||||
{{ ui::search_combobox(name="assignee_id", label="Assignee (Optional)", placeholder="Search developer...", search_url="/developers/search", input_id="assignee-search", value_id="assignee-id") }}
|
||||
|
||||
<!-- Hidden input holding the actual developer ID to submit -->
|
||||
<input type="hidden" id="assignee-id" name="assignee_id" class="combobox-value">
|
||||
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
id="assignee-search"
|
||||
name="q"
|
||||
placeholder="Search developer..."
|
||||
autocomplete="off"
|
||||
|
||||
hx-get="/developers/search"
|
||||
hx-trigger="input changed delay:250ms, search"
|
||||
hx-target="next .combobox-results"
|
||||
hx-indicator="next .combobox-indicator"
|
||||
|
||||
class="combobox-input appearance-none rounded-xl relative block w-full pl-9 pr-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
|
||||
|
||||
<!-- Search Icon & Loading Indicator -->
|
||||
<div class="absolute left-3 top-3 text-slate-500">
|
||||
<svg class="combobox-indicator htmx-indicator animate-spin h-4 w-4 text-sky-500 hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results Dropdown Popover -->
|
||||
<div id="search-results"
|
||||
class="combobox-results absolute z-10 w-full mt-1.5 bg-slate-900 border border-slate-800 rounded-xl shadow-2xl overflow-hidden hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-3 px-4 text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-md shadow-sky-500/10">
|
||||
Create Task
|
||||
</button>
|
||||
<!-- Create Task submit button using macro -->
|
||||
{{ ui::button(label="Create Task", variant="indigo", type="submit", extra_class="w-full py-3 bg-gradient-to-r from-sky-500 to-indigo-600 hover:opacity-95 transition shadow-md shadow-sky-500/10") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,4 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reusable Task Info Modal defined via macro -->
|
||||
{{ ui::modal(id="task-info-modal", title="Tasks Guide", content="Welcome to the task board! You can add new tasks, assign them to registered developers, mark them complete, or delete them. This board is dynamic and updates automatically.", close_label="Got it") }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user