OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
146
app/Views/Templates/components/navigation/tabs.blade.php
Normal file
146
app/Views/Templates/components/navigation/tabs.blade.php
Normal file
@@ -0,0 +1,146 @@
|
||||
{{--
|
||||
Shared tab-group — the ONE tablist implementation (navigation.tabs).
|
||||
|
||||
ARIA button-tablist with roving tabindex, Arrow/Home/End keyboard support,
|
||||
optional persistence, and HTMX-safe vanilla-JS init (no jQuery). Replaces
|
||||
the legacy jQuery-UI tabs wrapper and the four hand-rolled copies of the
|
||||
same pattern (board tabs, report deck, goal dialog, Resource Allocation).
|
||||
|
||||
Props:
|
||||
group string REQUIRED. Unique id prefix for this group on the page;
|
||||
tab/panel ids derive from it ({group}-tab-{name} /
|
||||
{group}-panel-{name}).
|
||||
label string REQUIRED. aria-label for the tablist.
|
||||
variant string 'attached' (default) — gradient accent band with a
|
||||
translucent framed segment group (board/report look), or
|
||||
'floating' — free-floating fully-rounded pills (RA look).
|
||||
dark bool Floating variant only: dark-header color scheme
|
||||
(server-stamped, mirrors the theme's color mode).
|
||||
panels string 'toggle' (default) — the JS shows/hides elements marked
|
||||
data-tabs-panel for this group; 'manual' — the JS only
|
||||
manages tab state and dispatches the event below, and
|
||||
the page's own JS moves content (e.g. a deck track).
|
||||
storage string 'none' (default) | 'session' | 'local' — where the
|
||||
active tab persists across reloads.
|
||||
storageKey string Storage key; REQUIRED when storage != none. Scope it
|
||||
per entity (e.g. "ra-tab-{programId}").
|
||||
|
||||
Slots:
|
||||
default The x-global::navigation.tabs.tab children.
|
||||
actions Optional right-side cluster (filters, period pickers, pager
|
||||
arrows). Attached variant renders it inside the band.
|
||||
|
||||
Panel contract (caller-rendered):
|
||||
<div id="{group}-panel-{name}" role="tabpanel"
|
||||
aria-labelledby="{group}-tab-{name}" tabindex="0"
|
||||
data-tabs-panel="{name}" data-tabs-group="{group}">…</div>
|
||||
|
||||
Event contract: on every activation the root dispatches a bubbling
|
||||
CustomEvent "lt:tabs:changed" with detail {group, name, index} — pages with
|
||||
special behavior (deck translation, lazy loads) listen for it.
|
||||
--}}
|
||||
@props([
|
||||
'group',
|
||||
'label',
|
||||
'variant' => 'attached',
|
||||
'dark' => false,
|
||||
'panels' => 'toggle',
|
||||
'storage' => 'none',
|
||||
'storageKey' => null,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'lt-tabs lt-tabs--'.$variant.($dark ? ' lt-tabs--dark' : '')]) }}
|
||||
data-lt-tabs="{{ $group }}"
|
||||
data-tabs-panels="{{ $panels }}"
|
||||
data-tabs-storage="{{ $storage }}"
|
||||
@if ($storageKey) data-tabs-storage-key="{{ $storageKey }}" @endif>
|
||||
<div class="lt-tabs-group" role="tablist" aria-label="{{ $label }}">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@isset ($actions)
|
||||
<div {{ $actions->attributes->merge(['class' => 'lt-tabs-actions']) }}>{{ $actions }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
@once('lt-tabs-script')
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function activate(root, tabs, idx, focus) {
|
||||
tabs.forEach(function (t, i) {
|
||||
var on = i === idx;
|
||||
t.classList.toggle('on', on);
|
||||
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
t.setAttribute('tabindex', on ? '0' : '-1');
|
||||
});
|
||||
var tab = tabs[idx];
|
||||
if (focus) { tab.focus(); }
|
||||
|
||||
var group = root.getAttribute('data-lt-tabs');
|
||||
var name = tab.getAttribute('data-tab-name');
|
||||
|
||||
if (root.getAttribute('data-tabs-panels') === 'toggle') {
|
||||
document.querySelectorAll('[data-tabs-group="' + group + '"][data-tabs-panel]').forEach(function (p) {
|
||||
p.hidden = p.getAttribute('data-tabs-panel') !== name;
|
||||
});
|
||||
}
|
||||
|
||||
var storage = root.getAttribute('data-tabs-storage');
|
||||
var key = root.getAttribute('data-tabs-storage-key');
|
||||
if (key && (storage === 'session' || storage === 'local')) {
|
||||
try { (storage === 'session' ? sessionStorage : localStorage).setItem(key, name); } catch (e) { /* private mode */ }
|
||||
}
|
||||
|
||||
root.dispatchEvent(new CustomEvent('lt:tabs:changed', {
|
||||
bubbles: true,
|
||||
detail: { group: group, name: name, index: idx },
|
||||
}));
|
||||
}
|
||||
|
||||
function init(scope) {
|
||||
(scope.querySelectorAll ? scope : document).querySelectorAll('[data-lt-tabs]:not([data-tabs-init])').forEach(function (root) {
|
||||
root.setAttribute('data-tabs-init', '1');
|
||||
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
|
||||
if (!tabs.length) { return; }
|
||||
|
||||
tabs.forEach(function (t, i) {
|
||||
t.addEventListener('click', function () { activate(root, tabs, i, false); });
|
||||
});
|
||||
|
||||
// Roving focus on the tablist: Left/Right wrap, Home/End jump.
|
||||
root.querySelector('[role="tablist"]').addEventListener('keydown', function (e) {
|
||||
var current = tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
|
||||
var next = null;
|
||||
if (e.key === 'ArrowRight') { next = (current + 1) % tabs.length; }
|
||||
else if (e.key === 'ArrowLeft') { next = (current - 1 + tabs.length) % tabs.length; }
|
||||
else if (e.key === 'Home') { next = 0; }
|
||||
else if (e.key === 'End') { next = tabs.length - 1; }
|
||||
if (next !== null) { e.preventDefault(); activate(root, tabs, next, true); }
|
||||
});
|
||||
|
||||
// Restore persisted selection (falls back to the server-rendered state).
|
||||
var storage = root.getAttribute('data-tabs-storage');
|
||||
var key = root.getAttribute('data-tabs-storage-key');
|
||||
if (key && (storage === 'session' || storage === 'local')) {
|
||||
try {
|
||||
var saved = (storage === 'session' ? sessionStorage : localStorage).getItem(key);
|
||||
var idx = tabs.findIndex(function (t) { return t.getAttribute('data-tab-name') === saved; });
|
||||
if (idx >= 0) { activate(root, tabs, idx, false); return; }
|
||||
} catch (e) { /* private mode */ }
|
||||
}
|
||||
|
||||
// Sync panels to the server-rendered selection in toggle mode.
|
||||
if (root.getAttribute('data-tabs-panels') === 'toggle') {
|
||||
var current = tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
|
||||
activate(root, tabs, current >= 0 ? current : 0, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') { init(document); }
|
||||
else { document.addEventListener('DOMContentLoaded', function () { init(document); }); }
|
||||
if (window.htmx) { window.htmx.onLoad(init); }
|
||||
})();
|
||||
</script>
|
||||
@endonce
|
||||
@@ -0,0 +1,31 @@
|
||||
{{--
|
||||
One tab panel paired with x-global::navigation.tabs.tab by `name`.
|
||||
|
||||
Renders the full panel contract (id/role/aria-labelledby/tabindex +
|
||||
data attributes) so consumers never hand-write it. Works in both
|
||||
component modes: `toggle` (the tabs script shows/hides these) and
|
||||
`manual` (the page's own JS moves them — pass extra classes/attrs
|
||||
through, e.g. class="ra-page" inside a deck track).
|
||||
|
||||
Props:
|
||||
name string REQUIRED. Must match the paired tab's name.
|
||||
group string Usually inherited via @aware from the parent tabs
|
||||
component; pass explicitly when the panel renders
|
||||
outside the component tree (deck tracks, separate
|
||||
partials).
|
||||
--}}
|
||||
@props([
|
||||
'name',
|
||||
'group' => null,
|
||||
])
|
||||
@aware(['group' => null])
|
||||
|
||||
<div id="{{ $group }}-panel-{{ $name }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="{{ $group }}-tab-{{ $name }}"
|
||||
tabindex="0"
|
||||
data-tabs-panel="{{ $name }}"
|
||||
data-tabs-group="{{ $group }}"
|
||||
{{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
31
app/Views/Templates/components/navigation/tabs/tab.blade.php
Normal file
31
app/Views/Templates/components/navigation/tabs/tab.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
{{--
|
||||
One tab button inside x-global::navigation.tabs.
|
||||
|
||||
Props:
|
||||
name string REQUIRED. Pairs the tab with its panel
|
||||
({group}-tab-{name} controls {group}-panel-{name}).
|
||||
icon string Optional Font Awesome classes (e.g. "fa-users").
|
||||
count mixed Optional count badge; countLabel gives it an
|
||||
accessible name ("7 people" instead of bare "7").
|
||||
selected bool Server-rendered initial selection (exactly one tab
|
||||
per group should pass true).
|
||||
--}}
|
||||
@props([
|
||||
'name',
|
||||
'icon' => null,
|
||||
'count' => null,
|
||||
'countLabel' => null,
|
||||
'selected' => false,
|
||||
])
|
||||
@aware(['group'])
|
||||
|
||||
<button type="button"
|
||||
role="tab"
|
||||
id="{{ $group }}-tab-{{ $name }}"
|
||||
aria-controls="{{ $group }}-panel-{{ $name }}"
|
||||
aria-selected="{{ $selected ? 'true' : 'false' }}"
|
||||
tabindex="{{ $selected ? '0' : '-1' }}"
|
||||
data-tab-name="{{ $name }}"
|
||||
{{ $attributes->merge(['class' => 'lt-tab'.($selected ? ' on' : '')]) }}>
|
||||
@if ($icon)<i class="fa {{ $icon }}" aria-hidden="true"></i> @endif{{ $slot }}@if ($count !== null) <span class="ct" @if ($countLabel) aria-label="{{ $countLabel }}" @endif>{{ $count }}</span>@endif
|
||||
</button>
|
||||
Reference in New Issue
Block a user