Add inactive UIUX V2 shared components
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
{#
|
||||||
|
UI/UX V2 Phase 1A — Breadcrumb component (INERT UNTIL INCLUDED)
|
||||||
|
|
||||||
|
Expected optional context:
|
||||||
|
breadcrumbs_v2: [{"label": "Clients", "url": "/clients"}, ...]
|
||||||
|
breadcrumbs_aria_label_v2: default "Breadcrumb"
|
||||||
|
|
||||||
|
The last item is rendered as the current page even if a URL is supplied.
|
||||||
|
#}
|
||||||
|
{% set _crumbs = breadcrumbs_v2|default([], true) %}
|
||||||
|
{% set _crumb_label = breadcrumbs_aria_label_v2|default('Breadcrumb', true) %}
|
||||||
|
|
||||||
|
{% if _crumbs %}
|
||||||
|
<nav aria-label="{{ _crumb_label }}" class="min-w-0" data-uiux-v2="breadcrumbs">
|
||||||
|
<ol class="flex min-w-0 items-center gap-1.5 overflow-x-auto whitespace-nowrap text-sm text-slate-500" role="list">
|
||||||
|
{% for crumb in _crumbs %}
|
||||||
|
{% set _is_last = loop.last %}
|
||||||
|
<li class="flex min-w-0 items-center gap-1.5">
|
||||||
|
{% if not loop.first %}
|
||||||
|
<svg class="h-4 w-4 shrink-0 text-slate-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 0 1 .02-1.06L11.168 10 7.23 6.29a.75.75 0 1 1 1.04-1.08l4.51 4.25a.75.75 0 0 1 0 1.08l-4.51 4.25a.75.75 0 0 1-1.06-.02Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not _is_last and crumb.get('url') %}
|
||||||
|
<a href="{{ crumb.get('url') }}" class="max-w-56 truncate rounded px-1 py-0.5 font-medium text-slate-600 hover:text-brand-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500">
|
||||||
|
{{ crumb.get('label', '') }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="max-w-64 truncate px-1 py-0.5 font-semibold text-slate-900" {% if _is_last %}aria-current="page"{% endif %}>
|
||||||
|
{{ crumb.get('label', '') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{#
|
||||||
|
UI/UX V2 Phase 1A — Optional page context bar (INERT UNTIL INCLUDED)
|
||||||
|
|
||||||
|
Combines breadcrumbs and page header without touching the current layout.
|
||||||
|
A later phase can include this wrapper on selected pages after route-level
|
||||||
|
breadcrumb/back/action context has been supplied explicitly.
|
||||||
|
#}
|
||||||
|
{% set _has_breadcrumbs = breadcrumbs_v2|default([], true) %}
|
||||||
|
{% set _has_page_header = page_title_v2|default('', true) or page_description_v2|default('', true) or back_url_v2|default('', true) or page_actions_v2|default([], true) %}
|
||||||
|
|
||||||
|
{% if _has_breadcrumbs or _has_page_header %}
|
||||||
|
<section class="mx-auto w-full max-w-[1500px] space-y-4 px-4 py-5 sm:px-6 lg:px-8" data-uiux-v2="context-bar">
|
||||||
|
{% include "ui/templates/components/breadcrumbs_v2.html" %}
|
||||||
|
{% include "ui/templates/components/page_header_v2.html" %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{#
|
||||||
|
UI/UX V2 Phase 1A — Mobile navigation component (INERT UNTIL INCLUDED)
|
||||||
|
|
||||||
|
Uses native <details>, so it requires no JavaScript and cannot interfere with
|
||||||
|
the current application shell. It consumes the same navigation_items_v2 data
|
||||||
|
as navigation_v2.html.
|
||||||
|
#}
|
||||||
|
{% set _mobile_items = navigation_items_v2|default([], true) %}
|
||||||
|
{% set _mobile_label = navigation_aria_label_v2|default('Mobile navigation', true) %}
|
||||||
|
|
||||||
|
{% if _mobile_items %}
|
||||||
|
<div class="lg:hidden" data-uiux-v2="mobile-navigation">
|
||||||
|
<details class="group border-b border-slate-200 bg-white">
|
||||||
|
<summary class="mx-auto flex min-h-12 max-w-[1500px] cursor-pointer list-none items-center justify-between gap-3 px-4 py-2 text-sm font-semibold text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500 sm:px-6">
|
||||||
|
<span class="inline-flex items-center gap-2">
|
||||||
|
<svg class="h-5 w-5 group-open:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
<svg class="hidden h-5 w-5 group-open:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" d="M6 6l12 12M18 6 6 18" />
|
||||||
|
</svg>
|
||||||
|
<span>Menu</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs font-medium text-slate-500">{{ _mobile_label }}</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<nav aria-label="{{ _mobile_label }}" class="border-t border-slate-100 bg-slate-50/80 px-4 py-3 sm:px-6">
|
||||||
|
<ul class="mx-auto max-w-[1500px] space-y-1" role="list">
|
||||||
|
{% for item in _mobile_items %}
|
||||||
|
{% if item.get('visible', true) %}
|
||||||
|
{% set _children = item.get('children', []) or [] %}
|
||||||
|
{% set _active = item.get('active', false) %}
|
||||||
|
<li>
|
||||||
|
{% if _children %}
|
||||||
|
<details class="group/sub rounded-xl border {{ 'border-brand-200 bg-brand-50/60' if _active else 'border-transparent bg-white' }}">
|
||||||
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold {{ 'text-brand-700' if _active else 'text-slate-800' }}">
|
||||||
|
<span class="inline-flex items-center gap-2">
|
||||||
|
{{ item.get('label', '') }}
|
||||||
|
{% if item.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-brand-100 px-2 py-0.5 text-[11px] font-bold text-brand-700">{{ item.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<svg class="h-4 w-4 transition-transform group-open/sub:rotate-180" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.51a.75.75 0 0 1-1.08 0l-4.25-4.51a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="space-y-1 border-t border-slate-100 p-2">
|
||||||
|
{% for child in _children %}
|
||||||
|
{% if child.get('visible', true) %}
|
||||||
|
{% set _child_active = child.get('active', false) %}
|
||||||
|
<a
|
||||||
|
href="{{ child.get('url', '#') }}"
|
||||||
|
class="flex items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm {{ 'bg-brand-100 font-semibold text-brand-800' if _child_active else 'text-slate-700 hover:bg-slate-100' }}"
|
||||||
|
{% if _child_active %}aria-current="page"{% endif %}
|
||||||
|
{% if child.get('external', false) %}target="_blank" rel="noopener noreferrer"{% endif %}
|
||||||
|
>
|
||||||
|
<span>{{ child.get('label', '') }}</span>
|
||||||
|
{% if child.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-white px-2 py-0.5 text-[11px] font-bold text-slate-600">{{ child.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% else %}
|
||||||
|
<a
|
||||||
|
href="{{ item.get('url', '#') }}"
|
||||||
|
class="flex items-center justify-between gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold {{ 'bg-brand-50 text-brand-700' if _active else 'bg-white text-slate-800 hover:bg-slate-100' }}"
|
||||||
|
{% if _active %}aria-current="page"{% endif %}
|
||||||
|
{% if item.get('external', false) %}target="_blank" rel="noopener noreferrer"{% endif %}
|
||||||
|
>
|
||||||
|
<span>{{ item.get('label', '') }}</span>
|
||||||
|
{% if item.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-brand-100 px-2 py-0.5 text-[11px] font-bold text-brand-700">{{ item.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{#
|
||||||
|
UI/UX V2 Phase 1A — Desktop navigation component (INERT UNTIL INCLUDED)
|
||||||
|
|
||||||
|
Expected optional context:
|
||||||
|
navigation_items_v2: list of mappings with:
|
||||||
|
label (required), url, active, visible, badge, children
|
||||||
|
navigation_aria_label_v2: accessible label (default: "Primary navigation")
|
||||||
|
|
||||||
|
The component is deliberately data-driven and contains no role or permission
|
||||||
|
assumptions. A later role pilot will build navigation_items_v2 from the
|
||||||
|
existing permission helpers without changing current access rules.
|
||||||
|
#}
|
||||||
|
{% set _nav_items = navigation_items_v2|default([], true) %}
|
||||||
|
{% set _nav_label = navigation_aria_label_v2|default('Primary navigation', true) %}
|
||||||
|
|
||||||
|
{% if _nav_items %}
|
||||||
|
<nav aria-label="{{ _nav_label }}" class="hidden border-b border-slate-200 bg-white lg:block" data-uiux-v2="desktop-navigation">
|
||||||
|
<div class="mx-auto max-w-[1500px] px-4 sm:px-6 lg:px-8">
|
||||||
|
<ul class="flex min-h-12 items-stretch gap-1" role="list">
|
||||||
|
{% for item in _nav_items %}
|
||||||
|
{% set _visible = item.get('visible', true) %}
|
||||||
|
{% set _children = item.get('children', []) or [] %}
|
||||||
|
{% set _active = item.get('active', false) %}
|
||||||
|
{% if _visible %}
|
||||||
|
<li class="relative flex items-stretch">
|
||||||
|
{% if _children %}
|
||||||
|
<details class="group relative flex items-stretch">
|
||||||
|
<summary
|
||||||
|
class="flex cursor-pointer list-none items-center gap-2 border-b-2 px-3 py-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 {{ 'border-brand-600 text-brand-700' if _active else 'border-transparent text-slate-700 hover:border-slate-300 hover:text-slate-950' }}"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
>
|
||||||
|
<span>{{ item.get('label', '') }}</span>
|
||||||
|
{% if item.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-brand-50 px-2 py-0.5 text-[11px] font-bold text-brand-700">{{ item.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.51a.75.75 0 0 1-1.08 0l-4.25-4.51a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="absolute left-0 top-full z-50 mt-1 min-w-64 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-card" role="menu">
|
||||||
|
{% for child in _children %}
|
||||||
|
{% if child.get('visible', true) %}
|
||||||
|
{% set _child_active = child.get('active', false) %}
|
||||||
|
<a
|
||||||
|
href="{{ child.get('url', '#') }}"
|
||||||
|
class="flex items-center justify-between gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 {{ 'bg-brand-50 text-brand-700' if _child_active else 'text-slate-700 hover:bg-slate-50 hover:text-slate-950' }}"
|
||||||
|
role="menuitem"
|
||||||
|
{% if _child_active %}aria-current="page"{% endif %}
|
||||||
|
{% if child.get('external', false) %}target="_blank" rel="noopener noreferrer"{% endif %}
|
||||||
|
>
|
||||||
|
<span>{{ child.get('label', '') }}</span>
|
||||||
|
{% if child.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-bold text-slate-600">{{ child.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% else %}
|
||||||
|
<a
|
||||||
|
href="{{ item.get('url', '#') }}"
|
||||||
|
class="flex items-center gap-2 border-b-2 px-3 py-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 {{ 'border-brand-600 text-brand-700' if _active else 'border-transparent text-slate-700 hover:border-slate-300 hover:text-slate-950' }}"
|
||||||
|
{% if _active %}aria-current="page"{% endif %}
|
||||||
|
{% if item.get('external', false) %}target="_blank" rel="noopener noreferrer"{% endif %}
|
||||||
|
>
|
||||||
|
<span>{{ item.get('label', '') }}</span>
|
||||||
|
{% if item.get('badge') is not none %}
|
||||||
|
<span class="rounded-full bg-brand-50 px-2 py-0.5 text-[11px] font-bold text-brand-700">{{ item.get('badge') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{#
|
||||||
|
UI/UX V2 Phase 1A — Page header component (INERT UNTIL INCLUDED)
|
||||||
|
|
||||||
|
Expected optional context:
|
||||||
|
page_title_v2 (string)
|
||||||
|
page_description_v2 (string)
|
||||||
|
back_url_v2 (string)
|
||||||
|
back_label_v2 (default "Back")
|
||||||
|
page_actions_v2: list of mappings:
|
||||||
|
label, url, variant(primary|secondary|danger), visible, external
|
||||||
|
|
||||||
|
This component never discovers, moves, hides or rewrites existing page buttons.
|
||||||
|
#}
|
||||||
|
{% set _title = page_title_v2|default('', true) %}
|
||||||
|
{% set _description = page_description_v2|default('', true) %}
|
||||||
|
{% set _back_url = back_url_v2|default('', true) %}
|
||||||
|
{% set _back_label = back_label_v2|default('Back', true) %}
|
||||||
|
{% set _actions = page_actions_v2|default([], true) %}
|
||||||
|
|
||||||
|
{% if _title or _description or _back_url or _actions %}
|
||||||
|
<header class="space-y-3" data-uiux-v2="page-header">
|
||||||
|
{% if _back_url %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ _back_url }}" class="inline-flex items-center gap-1.5 rounded-lg px-1 py-1 text-sm font-semibold text-slate-600 transition hover:text-brand-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500">
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 1.06L5.56 9.25h10.69A.75.75 0 0 1 17 10Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ _back_label }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
{% if _title %}
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-950 sm:text-3xl">{{ _title }}</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% if _description %}
|
||||||
|
<p class="mt-1 max-w-3xl text-sm leading-6 text-slate-600">{{ _description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if _actions %}
|
||||||
|
<div class="flex shrink-0 flex-wrap items-center gap-2" aria-label="Page actions">
|
||||||
|
{% for action in _actions %}
|
||||||
|
{% if action.get('visible', true) %}
|
||||||
|
{% set _variant = action.get('variant', 'secondary') %}
|
||||||
|
{% set _classes = 'bg-brand-600 text-white hover:bg-brand-700 focus-visible:ring-brand-500' if _variant == 'primary' else ('bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500' if _variant == 'danger' else 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 hover:text-slate-950 focus-visible:ring-brand-500') %}
|
||||||
|
<a
|
||||||
|
href="{{ action.get('url', '#') }}"
|
||||||
|
class="inline-flex min-h-10 items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 {{ _classes }}"
|
||||||
|
{% if action.get('external', false) %}target="_blank" rel="noopener noreferrer"{% endif %}
|
||||||
|
>
|
||||||
|
{{ action.get('label', '') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user