feat: implement audit logs system, request extractor, admin log panel, and dedicated documentation

- Added an enterprise-grade, request-scoped AuditLogger extractor in Axum.
- Configured MongoDB persistence for structured, replayable audit logs (capturing timestamp, user, action, type, payload snapshot, client IP with proxy header support, and User-Agent).
- Created a live Administrator console at /auth/audit to filter and inspect log events.
- Re-architected documentation by moving Design Wiki pages out of /components into a dedicated /docs route.
- Published logging architecture documentation at /docs/logging.
This commit is contained in:
2026-05-30 18:23:49 +05:00
parent f6ea8a99d9
commit 4c98dd93ad
34 changed files with 1389 additions and 134 deletions
+98
View File
@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Buttons - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Actions / Navigation</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Buttons</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Standard button variants including primary, secondary, outlines, and statuses, completed with focus ring outlines, scale transformations on hover, and loading spinner designs.
</p>
</div>
<!-- Section Buttons -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Button Variants & Interactive Demos</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'btn-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'btn-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="btn-sandbox" class="wiki-pane flex flex-wrap gap-4 py-2">
{{ ui::button(label="Primary", variant="primary") }}
{{ ui::button(label="Secondary", variant="secondary") }}
{{ ui::button(label="Outline", variant="outline") }}
{{ ui::button(label="Destructive", variant="destructive") }}
<!-- 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") }}
<!-- 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 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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- Primary Button --&gt;
{{ "{{" }} ui::button(label="Primary", variant="primary") {{ "}}" }}
&lt;!-- Secondary Button --&gt;
{{ "{{" }} ui::button(label="Secondary", variant="secondary") {{ "}}" }}
&lt;!-- Outline Button --&gt;
{{ "{{" }} ui::button(label="Outline", variant="outline") {{ "}}" }}
&lt;!-- Destructive Button --&gt;
{{ "{{" }} ui::button(label="Destructive", variant="destructive") {{ "}}" }}
&lt;!-- Create Button (Icon + Text) --&gt;
{{ "{{" }} ui::button(label="&lt;svg class='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'&gt;&lt;path stroke-linecap='round' stroke-linejoin='round' d='M12 4v16m8-8H4'/&gt;&lt;/svg&gt; Create Task", variant="indigo", extra_class="gap-2") {{ "}}" }}
&lt;!-- Spinner Loading Button --&gt;
{{ "{{" }} ui::button(label="&lt;svg class='animate-spin h-3.5 w-3.5 text-slate-500' fill='none' viewBox='0 0 24 24'&gt;&lt;circle class='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' stroke-width='4'&gt;&lt;/circle&gt;&lt;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'&gt;&lt;/path&gt;&lt;/svg&gt; Processing...", variant="secondary", disabled=true, extra_class="gap-2 text-slate-500 cursor-not-allowed") {{ "}}" }}</code></pre>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+201
View File
@@ -0,0 +1,201 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Autocomplete (Combobox) - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Forms & Inputs</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Autocomplete (Combobox)</h1>
<p class="text-slate-405 text-sm mt-2 leading-relaxed">
Asynchronous search dropdown inputs driven by HTMX requests with fully integrated keyboard navigation, focus overlays, selection, and hidden inputs for form validation.
</p>
</div>
<!-- Section Combobox -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Autocomplete Showcase & Integration</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'combo-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'combo-client-code')">Client-Side HTML</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'combo-server-code')">Server-Side HTML</button>
</div>
<!-- Demo Viewport -->
<div id="combo-sandbox" class="wiki-pane grid grid-cols-1 md:grid-cols-2 gap-8 py-2">
<!-- Left: Client-side Demo -->
<div class="space-y-2 max-w-xs w-full">
<span class="text-[10px] font-bold text-indigo-400 uppercase tracking-wider block mb-1">Client-Side Filtering</span>
<div class="autocomplete-combobox relative">
<label class="block text-xs font-semibold text-muted-foreground mb-1.5">Local Developer List</label>
<input type="hidden" id="wiki-assignee-id" class="combobox-value">
<input type="text" id="wiki-assignee-search" placeholder="Type name e.g. Bob..." 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">
<div class="combobox-results absolute z-10 w-full mt-2 bg-popover border border-border rounded-xl p-1 shadow-xl hidden">
<div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="1" data-name="Alice Vance" tabindex="0">Alice Vance (Lead)</div>
<div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="2" data-name="Bob Carter" tabindex="0">Bob Carter (Senior)</div>
<div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="3" data-name="Charlie Smith" tabindex="0">Charlie Smith (Junior)</div>
</div>
</div>
</div>
<!-- Right: Server-side Demo -->
<div class="space-y-2 max-w-xs w-full">
<span class="text-[10px] font-bold text-emerald-400 uppercase tracking-wider block mb-1">Server-Side HTMX Search</span>
{% if authenticated %}
{{ 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">
<svg class="h-4.5 w-4.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
Authentication Required
</div>
<p class="text-muted-foreground leading-normal">
To query the MongoDB database on the server, you must sign in to an active session first.
</p>
<a href="/auth/login" class="inline-flex items-center text-xs font-semibold text-sky-400 hover:text-sky-350 gap-1 mt-1">
Log in to query database
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>
</a>
</div>
{% endif %}
</div>
</div>
<!-- Client-Side Code Snippet Area -->
<div id="combo-client-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-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 Code
</button>
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>&lt;!-- Client-Side Combobox Container (Filtered Locally) --&gt;
&lt;div class="autocomplete-combobox relative"&gt;
&lt;!-- Holds the final value submitted to forms --&gt;
&lt;input type="hidden" name="developer_id" class="combobox-value"&gt;
&lt;!-- Input box handles local filtering --&gt;
&lt;input type="text" placeholder="Type name e.g. Bob..." 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"&gt;
&lt;!-- Dropdown lists all options, filtered on input/focus dynamically --&gt;
&lt;div class="combobox-results absolute z-10 w-full mt-2 bg-popover border border-border rounded-xl p-1 shadow-xl hidden"&gt;
&lt;div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="1" data-name="Alice Vance" tabindex="0"&gt;Alice Vance (Lead)&lt;/div&gt;
&lt;div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="2" data-name="Bob Carter" tabindex="0"&gt;Bob Carter (Senior)&lt;/div&gt;
&lt;div class="combobox-item flex items-center w-full h-8 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-200 cursor-pointer select-none" data-id="3" data-name="Charlie Smith" tabindex="0"&gt;Charlie Smith (Junior)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</code></pre>
</div>
</div>
<!-- Server-Side Code Snippet Area -->
<div id="combo-server-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-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 Code
</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 {{ "%}" }}
&lt;!-- Server-Side Combobox using macro (Asynchronous Query via HTMX) --&gt;
{{ "{{" }} 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>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">How the Global Combobox JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
The global script <code>combobox.js</code> runs automatically on page load and hooks onto the class names listed below. Ensure your markup uses these selectors to integrate keyboard selection and local/remote filtering:
</p>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/4">Selector / Class</th>
<th class="pb-2.5 w-1/4">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.autocomplete-combobox</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Wraps all input elements, hidden inputs, and search result list containers.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.combobox-value</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to the <code>&lt;input type="hidden"&gt;</code> element that holds the final selection key (e.g. database ID) for form validation and submission.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.combobox-input</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to the visible <code>&lt;input type="text"&gt;</code> element. Captures typing, trigger clicks, and keyboard arrow navigation events.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.combobox-results</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The overlay list container. Starts with the class <code>hidden</code>. Toggled open on focus, click, or when query results return.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.combobox-item</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to option elements. Must contain <code>tabindex="0"</code> (for keyboard focus), <code>data-id="..."</code> (database value), and <code>data-name="..."</code> (display value).
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
You can customize the visual appearance of the options list <code>.combobox-results</code>, option heights, text alignment, input styles, borders, icons, and colors. The functional classes (like <code>.combobox-input</code>, <code>.combobox-value</code>, <code>.combobox-item</code>) must remain intact, and each item must include the <code>data-id</code>, <code>data-name</code>, and <code>tabindex="0"</code> attributes so they participate in keyboard selection cycling and updates.
</p>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+252
View File
@@ -0,0 +1,252 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Date & Time Pickers - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Pickers</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Date & Time Pickers</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Custom popup calendars and hour/minute scroll menus constructed using DOM components to prevent relying on native browser-system calendar windows. Designed for optimal styling consistency.
</p>
</div>
<!-- Section Pickers -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Date & Time Popovers</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'picker-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'picker-code')">HTML Markup</button>
</div>
<!-- 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 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 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 Macro -->
<div class="space-y-2">
<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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- Custom Date Picker using macro --&gt;
{{ "{{" }} 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 Macro -->
<div class="space-y-2">
<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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- Custom Time Picker using macro --&gt;
{{ "{{" }} ui::timepicker(id="my-timepicker", name="my_time", label="Time Picker", value="12:00 PM") {{ "}}" }}</code></pre>
</div>
</div>
</div>
</div>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-6 border-t border-border/60 pt-6">
<div>
<h2 class="text-lg font-bold text-slate-200">How the Global Date Picker JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed mt-1">
The custom date picker relies on the following classes and datasets to manage month navigation and update values:
</p>
</div>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/3">Selector / Class</th>
<th class="pb-2.5 w-1/6">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.custom-datepicker</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Wraps the date picker component. Must declare <code>data-year</code> (e.g. 2026) and <code>data-month</code> (0-indexed, 0 = Jan, 4 = May) to initialize the viewport.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-value</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Hidden input field (<code>&lt;input type="hidden"&gt;</code>) storing the ISO date value (YYYY-MM-DD) for form submission.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-trigger</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Button clicked by user to open or close the calendar popover dropdown.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-text</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Target text element updated dynamically to display the formatted selected date (e.g., "May 30, 2026").
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-popover</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Floating container containing the month navigation buttons and days grids. Starts as <code>hidden</code>.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-prev / .datepicker-next</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Navigational buttons to decrement or increment the active month.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-month-year</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Clickable header button. Toggles view grid modes between days selection, months selection, and years selection.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.datepicker-days</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Calendar day grid container dynamically populated with clickable day buttons by the JS engine.
</td>
</tr>
</tbody>
</table>
</div>
<div>
<h2 class="text-lg font-bold text-slate-200 mt-4">How the Global Time Picker JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed mt-1">
The custom time picker scopes dynamic lists of hours and minutes inside scrollable blocks using the following bindings:
</p>
</div>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/3">Selector / Class</th>
<th class="pb-2.5 w-1/6">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.custom-timepicker</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Wraps the time picker markup structure.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-value</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Hidden input field storing the active text time (e.g. "12:00 PM") for forms.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-trigger</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Clickable button to open the scroll dropdown.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-text</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Text node inside trigger displaying the formatted time.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-popover</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Dropdown overlay panel displaying columns for hour list, minute list, and AM/PM buttons.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-col-hours / .timepicker-col-minutes</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Containers populated automatically with hourly buttons (1-12) and minute buttons (00-55).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.timepicker-ampm-btn</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Buttons representing "AM" and "PM" choices toggled on click.
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
You can customize the styling of the trigger buttons, chevrons, icons, popover cards, borders, shadows, backgrounds, and the active option buttons (which receive <code>.bg-indigo-600</code>). Ensure you preserve the classes and attributes listed above so the DOM-generation functions and trigger listeners in <code>components.js</code> execute without error.
</p>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+130
View File
@@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block title %}Toasts & Alerts - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-405">Feedback</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Toasts & Alerts</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Dynamic toast stack notifications triggered from JavaScript, combined with static SVG alert boxes for validation errors or warnings.
</p>
</div>
<!-- Section Feedback -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Interactive Toast & Alert Showcase</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'feedback-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'feedback-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="feedback-sandbox" class="wiki-pane space-y-4 max-w-md py-2">
<!-- Toast triggers -->
<div class="space-y-2">
<span class="block text-xs font-semibold text-muted-foreground">Toast Notifications</span>
<div class="flex gap-2">
<button type="button" class="inline-flex items-center justify-center rounded-xl text-xs font-bold bg-secondary border border-border text-slate-200 px-4 py-2 hover:bg-secondary active:scale-95 transition" onclick="showToast('Operation finished successfully!')">
Trigger Toast
</button>
</div>
</div>
<!-- Alert callouts -->
<div class="space-y-2.5">
<span class="block text-xs font-semibold text-muted-foreground">Static Callout Banners</span>
<!-- Info Banner -->
<div class="rounded-2xl border border-sky-500/20 bg-sky-500/5 p-4 flex gap-3 text-xs text-sky-400">
<svg class="h-4.5 w-4.5 shrink-0" 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>
<span class="font-bold text-slate-100 block">General Information</span>
<span class="text-muted-foreground leading-relaxed block mt-0.5">Please remember to assign the task to an active workspace developer.</span>
</div>
</div>
<!-- Warning Banner -->
<div class="rounded-2xl border border-amber-500/20 bg-amber-500/5 p-4 flex gap-3 text-xs text-amber-400">
<svg class="h-4.5 w-4.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<span class="font-bold text-slate-100 block">Database Sync Issues</span>
<span class="text-muted-foreground leading-relaxed block mt-0.5">Local connections to MongoDB might be interrupted temporarily.</span>
</div>
</div>
</div>
</div>
<!-- Code Snippet Area -->
<div id="feedback-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-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 Code
</button>
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>&lt;!-- 1. Dynamic Toast Trigger from JS --&gt;
&lt;button onclick="showToast('Action Completed Successfully!')"&gt;
Trigger Success Toast
&lt;/button&gt;
&lt;!-- 2. Static Info Banner Alert --&gt;
&lt;div class="rounded-2xl border border-sky-500/20 bg-sky-500/5 p-4 flex gap-3 text-xs text-sky-400"&gt;
&lt;svg class="h-4.5 w-4.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"&gt;
&lt;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"/&gt;
&lt;/svg&gt;
&lt;div&gt;
&lt;span class="font-bold text-slate-100 block"&gt;Alert Title&lt;/span&gt;
&lt;span class="text-muted-foreground block mt-0.5"&gt;Description of alert information.&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 3. Static Warning Banner Alert --&gt;
&lt;div class="rounded-2xl border border-amber-500/20 bg-amber-500/5 p-4 flex gap-3 text-xs text-amber-400"&gt;
&lt;svg class="h-4.5 w-4.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"&gt;
&lt;path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/&gt;
&lt;/svg&gt;
&lt;div&gt;
&lt;span class="font-bold text-slate-100 block"&gt;Warning Title&lt;/span&gt;
&lt;span class="text-muted-foreground block mt-0.5"&gt;Warning context content details.&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;</code></pre>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+133
View File
@@ -0,0 +1,133 @@
{% extends "base.html" %}
{% block title %}Design System Wiki - Stick{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Floating Sidebar Navigation -->
{% include "docs/sidebar.html" %}
<!-- Main Content Area -->
<div class="flex-1 space-y-10">
<!-- Intro Header -->
<div class="pb-6 border-b border-border">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 mb-3 animate-pulse">
Stick Design System Wiki
</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight">Component Reference Manual</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Welcome to the Stick design system Wiki. This documentation serves as a living blueprint detailing HTML structures, Tailwind CSS custom variables, and JavaScript trigger hooks required to build premium, high-fidelity interactive elements using the Shadcn aesthetic.
</p>
</div>
<!-- Architecture & Concept -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="border border-border bg-secondary/10 rounded-2xl p-6 space-y-3">
<div class="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center">
<svg class="h-5 w-5 text-sky-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
</div>
<h3 class="text-sm font-bold text-slate-200">Vertical Feature Architecture</h3>
<p class="text-xs text-muted-foreground/90 leading-relaxed">
The backend routes and documentation handlers are located under <code>src/docs/</code>. Shared components reside in <code>templates/components/</code>. The rest of the application is packaged inside clean vertical feature directories (e.g. <code>src/auth/</code>, <code>src/tasks/</code>, <code>src/audit/</code>).
</p>
</div>
<div class="border border-border bg-secondary/10 rounded-2xl p-6 space-y-3">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<svg class="h-5 w-5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="text-sm font-bold text-slate-200">Lightweight & Dependency-Free</h3>
<p class="text-xs text-muted-foreground/90 leading-relaxed">
Designed to minimize heavy JS bundles. Using vanilla JavaScript with document-level event delegation (e.g. for Modals, Sheets, Accordions, and Tabs) keeping the interactive shell fast and responsive.
</p>
</div>
</div>
<!-- Custom Styling tokens -->
<div class="space-y-4">
<h2 class="text-lg font-bold text-slate-250">Global HSL Tokens</h2>
<p class="text-xs text-muted-foreground">
The layout relies on standard Tailwind themes mapped onto raw HSL variables, allowing instant utility customization.
</p>
<div class="border border-border bg-card/50 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5">Variable</th>
<th class="pb-2.5">Raw HSL Mapping</th>
<th class="pb-2.5">Render Example</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground font-mono text-[11px]">
<tr>
<td class="py-2 text-muted-foreground font-sans font-semibold">--background</td>
<td class="py-2">224 71% 4%</td>
<td class="py-2">
<div class="w-4 h-4 rounded bg-[#030712] border border-border"></div>
</td>
</tr>
<tr>
<td class="py-2 text-muted-foreground font-sans font-semibold">--primary</td>
<td class="py-2">210 40% 98%</td>
<td class="py-2">
<div class="w-4 h-4 rounded bg-[#f8fafc]"></div>
</td>
</tr>
<tr>
<td class="py-2 text-muted-foreground font-sans font-semibold">--border / --input</td>
<td class="py-2">216 34% 17%</td>
<td class="py-2">
<div class="w-4 h-4 rounded bg-[#1e293b]"></div>
</td>
</tr>
<tr>
<td class="py-2 text-muted-foreground font-sans font-semibold">--ring</td>
<td class="py-2">216 12.2% 83.9%</td>
<td class="py-2">
<div class="w-4 h-4 rounded ring-2 ring-slate-400"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Architecture & Contract Philosophy -->
<div class="space-y-4 border-t border-border/60 pt-8">
<h2 class="text-lg font-bold text-slate-200">Understanding the JS Binding Architecture</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
Rather than attaching active event listeners to every individual DOM node, the design system utilizes <strong>document-level event delegation</strong> and <strong>global query selector hooks</strong>. This keeps page loads incredibly fast and handles dynamically rendered components automatically.
</p>
<div class="rounded-2xl border border-indigo-500/20 bg-indigo-500/5 p-5 text-xs space-y-4">
<div>
<span class="font-bold text-indigo-400 block mb-1">🔗 The Core Binding Contract</span>
<p class="text-slate-400 leading-normal">
To make components interactive, they must contain specific functional CSS classes or attributes (e.g., <code>.modal-dialog</code>, <code>[data-modal-target]</code>, <code>.custom-select</code>) that the Javascript file uses to query the DOM. If these functional hooks are missing or renamed, the interactivity will fail.
</p>
</div>
<div>
<span class="font-bold text-indigo-400 block mb-1">🎨 What is Customizable vs. Fixed?</span>
<ul class="list-disc pl-5 space-y-1.5 text-slate-400 leading-normal">
<li>
<strong class="text-slate-200">Fixed (Functional) Hooks:</strong> Standard semantic class names (like <code>.modal-dialog</code>, <code>.modal-content</code>, <code>.modal-backdrop</code>, <code>.modal-close</code>, <code>.select-trigger</code>, and <code>.select-item</code>) are required structure hooks. These must remain unchanged because our JavaScript code expects them to animate opacity, visibility, translation transform stages, and handle keydown events.
</li>
<li>
<strong class="text-slate-200">Customizable (Styling) Rules:</strong> Any Tailwind visual classes (like colors e.g. <code>bg-slate-900</code>, padding/margin <code>p-6 mt-2</code>, border-radius <code>rounded-3xl</code>, drop shadows <code>shadow-xl</code>, borders <code>border-border</code>, and custom fonts) can be modified, replaced, or completely restyled. As long as you maintain the core HTML nesting hierarchy and functional selector names, the component will work perfectly.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+178
View File
@@ -0,0 +1,178 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Form Fields & Select - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Forms & Inputs</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Form Fields & Select</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Standard text inputs, passwords, multi-line textareas, and custom styled select dropdown elements overdrawn by vector SVG chevron arrows.
</p>
</div>
<!-- Section Fields -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Form Inputs & Select Menu Showcase</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'input-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'input-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="input-sandbox" class="wiki-pane space-y-4 max-w-md py-2">
<!-- Text field using macro -->
{{ ui::text_input(id="demo-username", name="username", label="Username Input", type="text", placeholder="e.g. dev_alice") }}
<!-- Password field using macro -->
{{ ui::text_input(id="demo-password", name="password", label="Security Password", type="password", placeholder="••••••••") }}
<!-- 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 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 -->
<div id="input-code" class="wiki-pane hidden">
<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 Code
</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 {{ "%}" }}
&lt;!-- Styled Text Input --&gt;
{{ "{{" }} ui::text_input(id="demo-username", name="username", label="Username Input", type="text", placeholder="e.g. dev_alice") {{ "}}" }}
&lt;!-- Styled Password Input --&gt;
{{ "{{" }} ui::text_input(id="demo-password", name="password", label="Security Password", type="password", placeholder="••••••••") {{ "}}" }}
&lt;!-- Styled Textarea --&gt;
{{ "{{" }} ui::textarea(id="demo-details", name="details", label="Task Details", placeholder="Provide a detailed description...") {{ "}}" }}
&lt;!-- Custom Styled Select Dropdown --&gt;
{{ "{{" }} 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>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">How the Global Custom Select JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
The global script <code>components.js</code> monitors specific class names to run the premium custom select elements. Ensure your markup uses these selectors to integrate selection, triggers, and keyboard arrow controls:
</p>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/4">Selector / Class</th>
<th class="pb-2.5 w-1/4">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.custom-select</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Outer wrapper for the entire select component. Scopes and isolates input values and popovers.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-value</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to the <code>&lt;input type="hidden"&gt;</code> that stores the raw value submitted to forms.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-trigger</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The visible button that user clicks. Toggles popover visibility and chevron rotation on click.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-text</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Inner text element inside trigger button that dynamically changes its text content to match the selected option.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-chevron</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Arrow SVG icon. Rotates 180 degrees (adds <code>rotate-180</code>) when the menu opens.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-popover</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Floating list container. Starts with the class <code>hidden</code>. Positioned absolutely.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.select-item</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to choices inside popover (usually buttons). Must have <code>data-value="..."</code> containing the raw option value.
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
You can customize the button trigger layout (paddings, chevrons, fonts, sizing), borders, shadows, options listing alignment, and checkmark icons. Ensure you preserve the classes <code>.custom-select</code>, <code>.select-value</code>, <code>.select-trigger</code>, <code>.select-popover</code>, and <code>.select-item</code> with its <code>data-value</code> attribute so that mouse selections and keyboard arrows function correctly.
</p>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+240
View File
@@ -0,0 +1,240 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Audit Logging - Documentation{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Architecture / Operations</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Audit Logging Architecture</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
A first-class, request-scoped auditing framework built to log and replay critical user mutations (Create, Read, Update, Delete, Search) transparently. Powered by Axum extractors, MongoDB, and JSON payloads.
</p>
</div>
<!-- Section: Design Philosophy -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">1. Architectural Philosophy</h2>
<p class="text-sm text-muted-foreground leading-relaxed">
In enterprise-grade software, logs are not secondary diagnostics. They are an <strong>immutable audit trail</strong> that ensures accountability and replayability. Every critical event records the state of the entity at the time of modification, who performed it, and their network metadata.
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
<span class="text-xs font-bold text-sky-400">Request-Scoped Context</span>
<p class="text-xs text-muted-foreground leading-relaxed">
No manual extraction of IP addresses, headers, or cookies. The system resolves all metadata implicitly via request parts.
</p>
</div>
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
<span class="text-xs font-bold text-emerald-400">State Snapshotting</span>
<p class="text-xs text-muted-foreground leading-relaxed">
Snapshots are preserved as raw JSON payloads, making historical state transitions fully auditable and replayable.
</p>
</div>
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
<span class="text-xs font-bold text-indigo-400">Administrator Console</span>
<p class="text-xs text-muted-foreground leading-relaxed">
A real-time search interface available to authorized administrators, supporting filtering by user, action, type, and timeline.
</p>
</div>
</div>
</section>
<!-- Section: How it Works (Extractor) -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">2. The AuditLogger Extractor</h2>
<p class="text-sm text-muted-foreground leading-relaxed">
Rather than cluttering handlers with boilerplate code to parse headers and look up user accounts, developers can leverage the custom Axum <code>AuditLogger</code> extractor. This struct implements Axum's <code>FromRequestParts</code> trait.
</p>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'logger-impl')">Usage in Handlers</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'logger-struct')">Extractor Source</button>
</div>
<!-- Handler Usage Pane -->
<div id="logger-impl" class="wiki-pane space-y-4">
<p class="text-xs text-muted-foreground">
Adding <code>logger: AuditLogger</code> to any Axum handler automatically intercepts the request context. Writing a log requires just a single asynchronous call.
</p>
<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 Code
</button>
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10.5px] text-sky-450 font-mono"><code class="text-sky-400">use crate::audit::AuditLogger;
use axum::{response::IntoResponse, extract::State};
use serde_json::json;
pub async fn update_task_handler(
State(repo): State<MongoTaskRepository>,
logger: AuditLogger, // <-- Extractor automatically fetches DB, User, IP, UA
axum::Form(input): axum::Form<UpdateTaskForm>,
) -> Result<impl IntoResponse, AppError> {
// 1. Perform database operation
let updated_task = repo.update(&input.id, &input).await?;
// 2. Audit the event (single line, fully request-aware)
logger.log(
"Update", // action_type
"Task", // entity_type
Some(updated_task.id), // entity_id
Some("Updated task details".into()), // details
Some(json!(updated_task)), // payload for replayability
).await;
Ok(Redirect::to("/tasks"))
}</code></pre>
</div>
</div>
<!-- Extractor Code Pane -->
<div id="logger-struct" class="wiki-pane hidden space-y-4">
<p class="text-xs text-muted-foreground">
Below is the underlying implementation of the extractor. It queries the JWT cookie, standardizes client IP resolution, and manages the database handle.
</p>
<div class="relative group">
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>pub struct AuditLogger {
pub db: mongodb::Database,
pub user: Option<AuthenticatedUser>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
impl&lt;S&gt; axum::extract::FromRequestParts&lt;S&gt; for AuditLogger
where
mongodb::Database: axum::extract::FromRef&lt;S&gt;,
Config: axum::extract::FromRef&lt;S&gt;,
S: Send + Sync,
{
type Rejection = crate::common::errors::AppError;
async fn from_request_parts(parts: &amp;mut Parts, state: &amp;S) -&gt; Result&lt;Self, Self::Rejection&gt; {
let db = mongodb::Database::from_ref(state);
let user = AuthenticatedUser::from_request_parts(parts, state).await.ok();
let user_agent = parts.headers.get(USER_AGENT)
.and_then(|h| h.to_str().ok())
.map(String::from);
let ip_address = parts.headers.get("x-forwarded-for")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
.or_else(|| {
parts.extensions.get::&lt;ConnectInfo&lt;SocketAddr&gt;&gt;()
.map(|ConnectInfo(addr)| addr.ip().to_string())
});
Ok(AuditLogger { db, user, ip_address, user_agent })
}
}</code></pre>
</div>
</div>
</div>
</section>
<!-- Section: Audit Log Model Schema -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">3. Schema Design</h2>
<p class="text-sm text-muted-foreground leading-relaxed">
Audit logs are persisted in the <code>audit_logs</code> collection in MongoDB. The model captures details on the actor, action, target entity, network details, and the structural payload.
</p>
<div class="border border-border bg-card/50 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5">Field</th>
<th class="pb-2.5">Data Type</th>
<th class="pb-2.5">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground/90 font-sans text-xs">
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">timestamp</td>
<td class="py-2.5 font-mono text-[11px]">DateTime&lt;Utc&gt;</td>
<td class="py-2.5">Chronological timestamp of when the action occurred in UTC.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">user_id</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;ObjectId&gt;</td>
<td class="py-2.5">Reference identifier of the actor. <code>None</code> for guest actions.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">username</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;String&gt;</td>
<td class="py-2.5">Cached username of the user for immediate visual queries.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">action_type</td>
<td class="py-2.5 font-mono text-[11px]">String</td>
<td class="py-2.5">The command category (e.g. <code>Create</code>, <code>Update</code>, <code>Delete</code>, <code>View</code>, <code>Login</code>, <code>Search</code>).</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">entity_type</td>
<td class="py-2.5 font-mono text-[11px]">String</td>
<td class="py-2.5">The resource kind (e.g. <code>Task</code>, <code>User</code>, <code>Developer</code>, <code>Auth</code>).</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">entity_id</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;ObjectId&gt;</td>
<td class="py-2.5">The target resource primary key identifier.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">details</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;String&gt;</td>
<td class="py-2.5">Human readable description of the event.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">payload</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;serde_json::Value&gt;</td>
<td class="py-2.5 text-sky-400">A snapshot of the affected model state or diff data for complete audit replayability.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">ip_address</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;String&gt;</td>
<td class="py-2.5">Origin client IP address resolved via proxy headers or TCP socket.</td>
</tr>
<tr>
<td class="py-2.5 font-mono text-[11px] text-indigo-400">user_agent</td>
<td class="py-2.5 font-mono text-[11px]">Option&lt;String&gt;</td>
<td class="py-2.5">Browser details used for authentication forensics.</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Section: Best Practices for Junior Developers -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">4. Guidelines for Developers</h2>
<div class="rounded-2xl border border-indigo-500/20 bg-indigo-500/5 p-5 text-xs space-y-4">
<div>
<span class="font-bold text-indigo-400 block mb-1">💡 When to write audit logs?</span>
<p class="text-slate-400 leading-normal">
Audit logs should always be populated during state changes. Whenever writing an endpoint that uses <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code> operations, inject the <code>AuditLogger</code> and record the event after a successful database execution.
</p>
</div>
<div>
<span class="font-bold text-indigo-400 block mb-1">🔄 Replayability Principle</span>
<p class="text-slate-400 leading-normal">
The `payload` field is crucial. By storing the JSON serialization of the model BEFORE or AFTER the action, system administrators can reconstruct state history. For deletions, always record the serial snapshot of the deleted model in the payload so it is never permanently lost to audit inquiries.
</p>
</div>
</div>
</section>
</div>
</div>
{% endblock %}
+268
View File
@@ -0,0 +1,268 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Dialog Modals - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Overlays</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Dialog Modals</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Overlay window popups centering inside the viewport with scale animations, hardware blur filters, and document-level listeners for escape-key/backdrop dismissals.
</p>
</div>
<!-- Section Modals -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Modal Showcase & Integration</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'modal-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'modal-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="modal-sandbox" class="wiki-pane py-2">
{{ ui::modal_trigger(target_id="wiki-demo-modal", label="Open Modal Dialog", variant="indigo") }}
</div>
<!-- Code Snippet Area -->
<div id="modal-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-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 Code
</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 {{ "%}" }}
&lt;!-- Trigger Button (points to modal element ID) --&gt;
{{ "{{" }} ui::modal_trigger(target_id="my-modal-id", label="Open Modal Dialog", variant="indigo") {{ "}}" }}
&lt;!-- Option A: Simple Modal (Self-contained with text body) --&gt;
{{ "{{" }} ui::modal(id="my-modal-id", title="Confirmation Title", content="Are you sure you want to proceed?", close_label="Cancel") {{ "}}" }}
&lt;!-- Option B: Paired Macros (For custom markup, inputs, or headers) --&gt;
{{ "{{" }} ui::modal_open(id="my-modal-id", title="Confirmation Title") {{ "}}" }}
&lt;p class="text-xs text-muted-foreground mt-2 leading-relaxed"&gt;Are you sure you want to proceed?&lt;/p&gt;
{{ "{{" }} ui::modal_close(close_label="Cancel") {{ "}}" }}</code></pre>
</div>
</div>
</div>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">How the Global Modal JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
The global script <code>components.js</code> runs automatically on page load and monitors specific attributes and classes. Here is what makes the modal operational:
</p>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/4">Selector / Class</th>
<th class="pb-2.5 w-1/4">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">[data-modal-target]</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Attribute</td>
<td class="py-3 text-slate-400">
Applied to buttons or triggers. The value must match the ID of the modal dialog (e.g., <code>data-modal-target="my-modal"</code>). Clicking it triggers <code>openModal("my-modal")</code>.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.modal-dialog</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The outer wrapper container. Starts with the class <code>hidden</code>. The JS removes/adds <code>hidden</code> to show/hide the popup.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.modal-content</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The inner dialog panel card. The JS looks for this class to toggle transitions (adds <code>scale-100 opacity-100</code> on show; resets to <code>scale-95 opacity-0</code> on hide).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.modal-backdrop</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The dark blur overlay. The JS toggles its opacity and registers clicks on it to automatically close the modal.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.modal-close</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to any close button (e.g., "Cancel", "Confirm", or an "X" icon). Clicking any element containing this class triggers modal closure.
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
You can customize the styling, colors, layout, and sizing of the <code>.modal-content</code> card freely. You must preserve the classes listed above so the Javascript code can target them and apply animations correctly.
</p>
</div>
</section>
<!-- Confirmation Action Pattern -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">Confirmation Actions (e.g., Deletes)</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
To prevent accidental actions, wrap destructive or state-changing actions (such as deletions) in a confirmation dialog modal. Since modals are populated inside the DOM structure, you can either wrap the action inside a standard HTML form or use HTMX for AJAX deletions.
</p>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'confirm-demo-pane')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'confirm-markup-form')">Form POST (Standard)</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'confirm-markup-htmx')">HTMX Delete (AJAX)</button>
</div>
<!-- Interactive Demo Pane -->
<div id="confirm-demo-pane" class="wiki-pane py-2">
<div class="flex items-center gap-4">
<span class="text-xs text-slate-400 font-medium">Demo Project Entity:</span>
<div class="flex items-center gap-3 px-3 py-2 bg-secondary/20 border border-border rounded-xl">
<span class="text-xs font-semibold text-slate-200">Important Draft.docx</span>
{{ ui::modal_trigger(target_id="wiki-confirm-delete-modal", label="Delete Draft", variant="destructive", extra_class="!px-2.5 !py-1 text-[10px]") }}
</div>
</div>
</div>
<!-- Form POST Code Markup -->
<div id="confirm-markup-form" 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-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-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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- 1. The Trigger Button (usually part of a table or list) --&gt;
{{ "{{" }} ui::modal_trigger(target_id="delete-confirm-modal-123", label="Delete Entity", variant="destructive") {{ "}}" }}
&lt;!-- 2. The Confirmation Dialog Modal with embedded Form --&gt;
{{ "{%" }} call ui::modal_open(id="delete-confirm-modal-123", title="Confirm Deletion") {{ "%}" }}
&lt;form action="/entities/123/delete" method="POST" class="space-y-4 mt-2"&gt;
&lt;p class="text-xs text-muted-foreground leading-relaxed text-center"&gt;
Are you sure you want to permanently delete this entity? This action cannot be undone.
&lt;/p&gt;
&lt;div class="flex gap-3 mt-4"&gt;
&lt;!-- Cancel Button (closes the modal via .modal-close) --&gt;
&lt;button type="button" class="modal-close flex-1 py-2 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200"&gt;
Cancel
&lt;/button&gt;
&lt;!-- Submit Button (performs POST redirect) --&gt;
&lt;button type="submit" class="flex-1 py-2 rounded-xl bg-destructive text-destructive-foreground hover:opacity-90 shadow-md transition text-xs font-bold"&gt;
Confirm Delete
&lt;/button&gt;
&lt;/div&gt;
&lt;/form&gt;
{{ "{%" }} call ui::modal_close_only() {{ "%}" }}</code></pre>
</div>
</div>
<!-- HTMX AJAX Code Markup -->
<div id="confirm-markup-htmx" 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-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-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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- 1. Trigger Button --&gt;
{{ "{{" }} ui::modal_trigger(target_id="delete-htmx-modal-123", label="Delete Entity", variant="destructive") {{ "}}" }}
&lt;!-- 2. The Confirmation Dialog Modal with HTMX triggers --&gt;
{{ "{%" }} call ui::modal_open(id="delete-htmx-modal-123", title="Confirm Deletion") {{ "%}" }}
&lt;div class="space-y-4 mt-2"&gt;
&lt;p class="text-xs text-muted-foreground leading-relaxed text-center"&gt;
Are you sure you want to delete this entity? This triggers an async HTMX request and removes the element.
&lt;/p&gt;
&lt;div class="flex gap-3 mt-4"&gt;
&lt;!-- Cancel Button --&gt;
&lt;button type="button" class="modal-close flex-1 py-2 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200"&gt;
Cancel
&lt;/button&gt;
&lt;!-- HTMX Action button. Adding .modal-close closes the modal animation instantly as the request goes out --&gt;
&lt;button type="button"
hx-delete="/entities/123"
hx-target="#entity-row-123"
hx-swap="outerHTML"
class="modal-close flex-1 py-2 rounded-xl bg-destructive text-destructive-foreground hover:opacity-90 shadow-md transition text-xs font-bold"&gt;
Confirm Delete
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
{{ "{%" }} call ui::modal_close_only() {{ "%}" }}</code></pre>
</div>
</div>
</div>
</section>
</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") }}
<!-- Interactive Confirm Delete Modal for the Demo -->
{{ ui::modal_open(id="wiki-confirm-delete-modal", title="Confirm Deletion") }}
<div class="space-y-4 mt-2">
<p class="text-xs text-slate-400 leading-relaxed text-center">
Are you sure you want to permanently delete <strong class="text-slate-200">"Important Draft.docx"</strong>? This action cannot be undone.
</p>
<div class="flex gap-3 mt-4">
<button class="modal-close flex-1 py-2 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200">
Cancel
</button>
<button class="modal-close flex-1 py-2 rounded-xl bg-rose-600 hover:bg-rose-500 text-white transition text-xs font-bold shadow-md" onclick="showToast('Entity &ldquo;Important Draft.docx&rdquo; deleted successfully!')">
Confirm Delete
</button>
</div>
</div>
{{ ui::modal_close_only() }}
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+105
View File
@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Custom Scrollbars - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Styles</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Custom Scrollbars</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Thin custom scrollbars replacing chunky default native OS scrollbars using modern cross-browser CSS rules.
</p>
</div>
<!-- Section Scrollbars -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Scrollbar Customization Showcase</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'scroll-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'scroll-code')">CSS Rules</button>
</div>
<!-- Demo Viewport -->
<div id="scroll-sandbox" class="wiki-pane max-w-sm py-2">
<div class="max-h-28 overflow-y-auto pr-1.5 border border-border rounded-xl p-2 bg-card/50 space-y-1.5">
<div class="p-2 rounded-lg bg-secondary text-xs text-muted-foreground">Scroll item 1</div>
<div class="p-2 rounded-lg bg-secondary text-xs text-muted-foreground">Scroll item 2</div>
<div class="p-2 rounded-lg bg-secondary text-xs text-muted-foreground">Scroll item 3</div>
<div class="p-2 rounded-lg bg-secondary text-xs text-muted-foreground">Scroll item 4</div>
<div class="p-2 rounded-lg bg-secondary text-xs text-muted-foreground">Scroll item 5</div>
</div>
</div>
<!-- Code Snippet Area -->
<div id="scroll-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-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 CSS Rules
</button>
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>/* Custom Webkit scrollbar rules (applied in input.css) */
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 9999px;
border: 1px solid transparent;
background-clip: padding-box;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground));
}</code></pre>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+272
View File
@@ -0,0 +1,272 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Slide-over Drawers (Sheets) - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Overlays</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Slide-over Drawers (Sheets)</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Right-anchored slide-out sheets designed for detail inspections, metadata lists, settings panels, and form workflows.
</p>
</div>
<!-- Section Sheets -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Drawer Showcase & Integration</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'sheet-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'sheet-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="sheet-sandbox" class="wiki-pane py-2">
{{ ui::sheet_trigger(target_id="wiki-demo-sheet", label="Open Right Drawer", variant="indigo") }}
</div>
<!-- Code Snippet Area -->
<div id="sheet-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-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 Code
</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 {{ "%}" }}
&lt;!-- Trigger Button (points to sheet element ID) --&gt;
{{ "{{" }} ui::sheet_trigger(target_id="my-sheet-id", label="Open Drawer", variant="indigo") {{ "}}" }}
&lt;!-- Slide Drawer Sheet Element using paired macros --&gt;
{{ "{{" }} ui::sheet_open(id="my-sheet-id", title="Settings", max_width_class="max-w-sm") {{ "}}" }}
&lt;p class="text-xs text-muted-foreground"&gt;Drawer Body Content&lt;/p&gt;
{{ "{{" }} ui::sheet_close(save_label="Save") {{ "}}" }}</code></pre>
</div>
</div>
</div>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">How the Global Sheet JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
The global script <code>components.js</code> monitors specific attributes and classes on page load. Here is the operational contract for sheets:
</p>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/4">Selector / Class</th>
<th class="pb-2.5 w-1/4">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">[data-sheet-target]</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Attribute</td>
<td class="py-3 text-slate-400">
Applied to buttons or triggers. The value must match the ID of the sheet container (e.g., <code>data-sheet-target="my-sheet"</code>). Clicking it triggers <code>openSheet("my-sheet")</code>.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.sheet-dialog</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The outer wrapper sheet container. Starts with the class <code>hidden</code>. Toggled by the script.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.sheet-content</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The inner slide-over panel card. The JS toggles transition translation classes (removes <code>translate-x-full</code> and adds <code>translate-x-0</code> on show).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.sheet-backdrop</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
The backdrop overlay. The JS toggles its opacity and registers clicks on it to automatically close the sheet.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.sheet-close</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Applied to close trigger buttons. Clicking any element with this class closes the sheet.
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
You can freely style the placement, width, colors, borders, and contents of the <code>.sheet-content</code> slide-out panel. The functional slide classes (<code>translate-x-full</code> and <code>translate-x-0</code>) and semantic structure class names must be preserved so the script can locate and slide the sheets dynamically.
</p>
</div>
</section>
<!-- Confirmation Action Pattern -->
<section class="space-y-4 border-t border-border/60 pt-6">
<h2 class="text-lg font-bold text-slate-200">Confirmation Actions (e.g., Deletes)</h2>
<p class="text-xs text-muted-foreground leading-relaxed">
Slide-over sheets are highly effective for confirmation dialogs when you want to provide more context, metadata details, or descriptive warnings about the entity being impacted before executing the action.
</p>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'confirm-sheet-demo-pane')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'confirm-sheet-markup-form')">Form POST (Standard)</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'confirm-sheet-markup-htmx')">HTMX Delete (AJAX)</button>
</div>
<!-- Interactive Demo Pane -->
<div id="confirm-sheet-demo-pane" class="wiki-pane py-2">
<div class="flex items-center gap-4">
<span class="text-xs text-slate-400 font-medium">Demo Database Entity:</span>
<div class="flex items-center gap-3 px-3 py-2 bg-secondary/20 border border-border rounded-xl">
<span class="text-xs font-semibold text-slate-200">User Account: ahmed_shaamil</span>
{{ ui::sheet_trigger(target_id="wiki-confirm-delete-sheet", label="Delete User", variant="destructive", extra_class="!px-2.5 !py-1 text-[10px]") }}
</div>
</div>
</div>
<!-- Form POST Code Markup -->
<div id="confirm-sheet-markup-form" 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-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-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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- 1. Trigger Button --&gt;
{{ "{{" }} ui::sheet_trigger(target_id="delete-sheet-confirm-123", label="Delete Account", variant="destructive") {{ "}}" }}
&lt;!-- 2. Slide Drawer with Form --&gt;
{{ "{%" }} call ui::sheet_open(id="delete-sheet-confirm-123", title="Confirm Account Deletion", max_width_class="max-w-md") {{ "%}" }}
&lt;form action="/accounts/123/delete" method="POST" class="flex flex-col justify-between h-full space-y-6"&gt;
&lt;div class="space-y-4 mt-2"&gt;
&lt;p class="text-xs text-muted-foreground leading-relaxed"&gt;
You are about to delete this account. This will invalidate all associated access tokens and permanently purge database profiles.
&lt;/p&gt;
&lt;div class="p-3.5 rounded-2xl bg-destructive/10 border border-destructive/20 text-[11px] text-destructive-foreground leading-normal"&gt;
Warning: This action is irreversible. Enter confirmation parameters if required.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="flex gap-3 pt-4 border-t border-border mt-auto"&gt;
&lt;button type="button" class="sheet-close flex-1 py-2.5 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200"&gt;
Cancel
&lt;/button&gt;
&lt;button type="submit" class="flex-1 py-2.5 rounded-xl bg-destructive text-destructive-foreground hover:opacity-90 shadow-md transition text-xs font-bold"&gt;
Confirm Delete
&lt;/button&gt;
&lt;/div&gt;
&lt;/form&gt;
{{ "{%" }} call ui::sheet_close(save_label="") {{ "%}" }}</code></pre>
</div>
</div>
<!-- HTMX AJAX Code Markup -->
<div id="confirm-sheet-markup-htmx" 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-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-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>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
&lt;!-- 1. Trigger Button --&gt;
{{ "{{" }} ui::sheet_trigger(target_id="delete-sheet-htmx-123", label="Delete Account", variant="destructive") {{ "}}" }}
&lt;!-- 2. Slide Drawer with HTMX --&gt;
{{ "{%" }} call ui::sheet_open(id="delete-sheet-htmx-123", title="Confirm Account Deletion", max_width_class="max-w-md") {{ "%}" }}
&lt;div class="flex flex-col justify-between h-full space-y-6"&gt;
&lt;div class="space-y-4 mt-2"&gt;
&lt;p class="text-xs text-muted-foreground leading-relaxed"&gt;
Are you sure you want to delete this account? This triggers an async HTMX request and removes the element.
&lt;/p&gt;
&lt;/div&gt;
&lt;div class="flex gap-3 pt-4 border-t border-border mt-auto"&gt;
&lt;button type="button" class="sheet-close flex-1 py-2.5 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200"&gt;
Cancel
&lt;/button&gt;
&lt;button type="button"
hx-delete="/accounts/123"
hx-target="#account-row-123"
hx-swap="outerHTML"
class="sheet-close flex-1 py-2.5 rounded-xl bg-destructive text-destructive-foreground hover:opacity-90 shadow-md transition text-xs font-bold"&gt;
Confirm Delete
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
{{ "{%" }} call ui::sheet_close(save_label="") {{ "%}" }}</code></pre>
</div>
</div>
</div>
</section>
</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") }}
<!-- Interactive Confirm Delete Sheet Element -->
{{ ui::sheet_open(id="wiki-confirm-delete-sheet", title="Confirm Account Deletion", max_width_class="max-w-sm") }}
<p class="text-xs text-slate-400 leading-relaxed mt-2">
Are you sure you want to permanently delete user <strong class="text-slate-200">ahmed_shaamil</strong>? All database records and linked files will be removed.
</p>
</div> <!-- Closes .space-y-4 -->
<!-- Custom Footer Buttons -->
<div class="flex gap-3 pt-4 border-t border-border mt-auto">
<button class="sheet-close flex-1 py-2.5 rounded-xl bg-secondary border border-border hover:bg-secondary/80 transition text-xs font-semibold text-slate-200">
Cancel
</button>
<button class="sheet-close flex-1 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-white transition text-xs font-bold shadow-lg shadow-rose-650/10" onclick="showToast('User account ahmed_shaamil deleted successfully!')">
Confirm Delete
</button>
</div>
</div> <!-- Closes .sheet-content -->
</div> <!-- Closes .absolute -->
</div> <!-- Closes .sheet-dialog -->
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
<aside class="lg:w-64 shrink-0">
<div class="sticky top-24 space-y-1.5 p-4 rounded-3xl border border-border bg-card/50 backdrop-blur-xl" id="wiki-sidebar">
<span class="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-2">Wiki Navigation</span>
<a href="/docs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs">Introduction</a>
<div class="h-px bg-secondary my-1 mx-2"></div>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Architecture</span>
<a href="/docs/logging" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/logging">Audit Logging</a>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Actions</span>
<a href="/docs/buttons" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/buttons">Buttons</a>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Forms & Inputs</span>
<a href="/docs/inputs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/inputs">Form Fields & Select</a>
<a href="/docs/date-time" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/date-time">Date & Time Pickers</a>
<a href="/docs/combobox" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/combobox">Autocomplete (Combobox)</a>
<a href="/docs/toggles" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/toggles">Switches & Checkboxes</a>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Overlays</span>
<a href="/docs/modals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/modals">Dialog Modals</a>
<a href="/docs/sheets" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/sheets">Slide-over Drawers</a>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Layout & Navigation</span>
<a href="/docs/tabs-accordion" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/tabs-accordion">Tabs & Accordions</a>
<a href="/docs/scrollbars" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/scrollbars">Custom Scrollbars</a>
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Visuals & Feedback</span>
<a href="/docs/visuals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/visuals">Avatars & Badges</a>
<a href="/docs/feedback" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/feedback">Toasts & Alerts</a>
</div>
</aside>
+218
View File
@@ -0,0 +1,218 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Tabs & Accordions - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Layout & Navigation</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Tabs & Accordions</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Horizontal tab groups and collapsible vertical headers with animated rotation chevrons, powered by lightweight document-level event delegation.
</p>
</div>
<!-- Section Tabs/Accordion -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Interactive Navigation Elements</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'tabs-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'tabs-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="tabs-sandbox" class="wiki-pane space-y-6 max-w-md py-2">
<!-- Tabs Showcase using macros -->
<div class="space-y-2">
<span class="block text-xs font-semibold text-muted-foreground">Switch Tabs</span>
{{ 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.
{{ 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.
{{ ui::tab_pane_close() }}
{{ ui::tabs_content_close() }}
</div>
<!-- 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">
{{ 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>
<!-- Code Snippet Area -->
<div id="tabs-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-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 Code
</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 {{ "%}" }}
&lt;!-- 1. Tabs Layout using paired macros --&gt;
{{ "{{" }} 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() {{ "}}" }}
{{ "{{" }} 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() {{ "}}" }}
{{ "{{" }} 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() {{ "}}" }}
&lt;!-- 2. Accordion using paired macros --&gt;
{{ "{{" }} ui::accordion_open(title="Section Title", is_open=false) {{ "}}" }}
Collapsible description content.
{{ "{{" }} ui::accordion_close() {{ "}}" }}</code></pre>
</div>
</div>
</div>
</section>
<!-- Global JS Bindings Documentation -->
<section class="space-y-6 border-t border-border/60 pt-6">
<div>
<h2 class="text-lg font-bold text-slate-200">How the Global Tabs JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed mt-1">
The global script <code>components.js</code> handles click switches for tabs via specific dataset variables. Here is the contract:
</p>
</div>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/3">Attribute</th>
<th class="pb-2.5 w-1/6">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">data-tab-group</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Attribute</td>
<td class="py-3 text-slate-400">
Applied to buttons and triggers. Isolates tab groups on the same page. Tab buttons in the same group toggle together (e.g., <code>data-tab-group="group1"</code>).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">data-tab-target</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Attribute</td>
<td class="py-3 text-slate-400">
Specifies the ID of the content element pane that should be revealed when this tab is clicked (e.g., <code>data-tab-target="my-pane-1"</code>).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">data-tab-content-group</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Attribute</td>
<td class="py-3 text-slate-400">
Applied to the content elements. Must match the <code>data-tab-group</code> string. Click triggers hide all content elements in this group and show the target.
</td>
</tr>
</tbody>
</table>
</div>
<div>
<h2 class="text-lg font-bold text-slate-200 mt-4">How the Global Accordion JS Works</h2>
<p class="text-xs text-muted-foreground leading-relaxed mt-1">
Accordions are toggled through click delegation looking for the following class tree:
</p>
</div>
<div class="border border-border bg-card/40 rounded-2xl p-5 overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-b border-border text-muted-foreground font-bold">
<th class="pb-2.5 w-1/3">Selector / Class</th>
<th class="pb-2.5 w-1/6">Type</th>
<th class="pb-2.5">Behavior & Purpose</th>
</tr>
</thead>
<tbody class="divide-y divide-border/40 text-muted-foreground leading-relaxed text-[11px] font-sans">
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.accordion-item</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Surrounds the single collapsible item container (trigger header + content block).
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.accordion-trigger</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Clickable header button. Toggles display classes on the sibling content element.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.accordion-content</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Contained collapsible item body text. Starts with the class <code>hidden</code>. Toggled by the script.
</td>
</tr>
<tr>
<td class="py-3 font-mono text-indigo-400 font-semibold">.accordion-chevron</td>
<td class="py-3 font-semibold text-slate-300 text-[10px] uppercase">Class</td>
<td class="py-3 text-slate-400">
Optional vector arrow icon inside trigger. The JS applies <code>rotate-180</code> animation classes on show.
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-xl border border-indigo-500/20 bg-indigo-500/5 p-4 text-xs space-y-1">
<span class="font-bold text-indigo-400 block">💡 What is customizable?</span>
<p class="text-slate-400 leading-normal">
For <strong>Tabs</strong>, you can style the tab list buttons (direction, active borders, colors) and content layout freely. Just ensure <code>data-tab-group</code> matches between triggers and active panes, and <code>data-tab-target</code> matches the pane ID.
For <strong>Accordions</strong>, you can design the headers, chevron SVGs, background panels, and borders. You must maintain the <code>.accordion-item</code>, <code>.accordion-trigger</code>, and <code>.accordion-content</code> class selectors so the script can toggle the collapsed state.
</p>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Switches & Checkboxes - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-405">Toggles</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Switches & Checkboxes</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Premium animated toggle elements, range sliders, and custom styled checkboxes that mask standard hidden inputs with custom animations.
</p>
</div>
<!-- Section Toggles -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Interactive Toggles & Checkbox Demos</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'toggle-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'toggle-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="toggle-sandbox" class="wiki-pane space-y-5 max-w-xs py-2">
<!-- Toggle Switch using macro -->
{{ ui::toggle_switch(name="demo_toggle", label="Toggle Status", checked=false) }}
<!-- Custom Checkbox using macro -->
<div class="flex flex-col gap-3">
{{ ui::checkbox(name="demo_checkbox", label="Enable Email notifications", checked=true) }}
</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 -->
<div id="toggle-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-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 Code
</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 {{ "%}" }}
&lt;!-- 1. Toggle Switch Variant --&gt;
{{ "{{" }} ui::toggle_switch(name="demo_toggle", label="Toggle Status", checked=false) {{ "}}" }}
&lt;!-- 2. Custom Checkbox --&gt;
{{ "{{" }} ui::checkbox(name="demo_checkbox", label="Enable Email notifications", checked=true) {{ "}}" }}
&lt;!-- 3. Custom Range Slider --&gt;
{{ "{{" }} ui::range_slider(name="demo_slider", label="Range Slider (0-100%)", min="0", max="100", value="50") {{ "}}" }}</code></pre>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}
+104
View File
@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block title %}Avatars & Badges - Design System Wiki{% endblock %}
{% block content %}
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
<!-- Left Navigation Sidebar -->
{% include "docs/sidebar.html" %}
<!-- Main Content -->
<div class="flex-1 space-y-8">
<!-- Header -->
<div class="pb-6 border-b border-border">
<span class="text-xs font-semibold text-indigo-400">Visuals</span>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Avatars & Badges</h1>
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
Circular user avatars featuring fallback initials text, combined with status badges in sky, emerald, amber, and rose variants.
</p>
</div>
<!-- Section Visuals -->
<section class="space-y-4">
<h2 class="text-lg font-bold text-slate-200">Avatars & Badges Showcase</h2>
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
<!-- Tab Headers -->
<div class="flex border-b border-border/60 pb-1.5">
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'visual-sandbox')">Interactive Demo</button>
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'visual-code')">HTML Markup</button>
</div>
<!-- Demo Viewport -->
<div id="visual-sandbox" class="wiki-pane space-y-4 py-2">
<!-- Avatars -->
<div class="space-y-2">
<span class="block text-xs font-semibold text-muted-foreground">Fallback Avatars</span>
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-secondary border border-border flex items-center justify-center text-xs font-bold text-sky-400 leading-none">AV</div>
<div class="w-9 h-9 rounded-full bg-secondary border border-border flex items-center justify-center text-xs font-bold text-emerald-400 leading-none">BC</div>
<div class="w-9 h-9 rounded-full bg-secondary border border-border flex items-center justify-center text-xs font-bold text-indigo-400 leading-none">JD</div>
<span class="text-xs text-muted-foreground/90">Textual initials fallback inside boundaries</span>
</div>
</div>
<!-- Badges -->
<div class="space-y-2">
<span class="block text-xs font-semibold text-muted-foreground">Status Badges</span>
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-sky-500/10 text-sky-400 border border-sky-500/20 leading-none">In Progress</span>
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 leading-none">Completed</span>
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 leading-none">Reviewing</span>
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/20 leading-none">Blocked</span>
</div>
</div>
</div>
<!-- Code Snippet Area -->
<div id="visual-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-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 Code
</button>
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>&lt;!-- Initials Fallback Avatar --&gt;
&lt;div class="w-9 h-9 rounded-full bg-secondary border border-border flex items-center justify-center text-xs font-bold text-sky-400 leading-none"&gt;AV&lt;/div&gt;
&lt;!-- Status Badges --&gt;
&lt;!-- Info / Sky --&gt;
&lt;span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-sky-500/10 text-sky-400 border border-sky-500/20 leading-none"&gt;In Progress&lt;/span&gt;
&lt;!-- Success / Emerald --&gt;
&lt;span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 leading-none"&gt;Completed&lt;/span&gt;
&lt;!-- Warning / Amber --&gt;
&lt;span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 leading-none"&gt;Reviewing&lt;/span&gt;
&lt;!-- Destructive / Rose --&gt;
&lt;span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/20 leading-none"&gt;Blocked&lt;/span&gt;</code></pre>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
function toggleWikiTabs(btn, paneId) {
const card = btn.closest('.border-border');
if (!card) return;
card.querySelectorAll('button').forEach(b => {
b.classList.remove('border-sky-500', 'text-sky-400');
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
});
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-muted-foreground');
btn.classList.add('border-sky-500', 'text-sky-400');
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
const target = card.querySelector('#' + paneId);
if (target) target.classList.remove('hidden');
}
</script>
{% endblock %}