Compare commits

...

37 Commits

Author SHA1 Message Date
jared 0af46954d7 feat: back-port design system utilities from tinker_tickets to web_template
Lint / JS (eslint) (push) Successful in 10s
Sticky footer layout:
- body: add display:flex + flex-direction:column
- .lt-main: add flex:1 + width:100% + min-width:0 so it fills remaining height
- .lt-main.lt-container combined-selector specificity fix (prevents lt-container
  padding from overriding lt-main padding-top in responsive breakpoints)
- Responsive breakpoints updated to use .lt-main.lt-container combined selector

Modal utilities:
- .lt-modal-xs (280px), .lt-modal-sm (360px) — size modifiers
- .lt-modal-header--danger — danger variant (red bg tint + accent border/title)

Badge:
- .lt-badge-sm — compact size (0.5rem font, 0.05/0.3rem padding)

KV rows:
- .lt-kv-row (display:contents), .lt-kv-label, .lt-kv-value — alternate KV
  row pattern where children become direct lt-kv-grid items

Avatar image overlay:
- .lt-avatar: add position:relative so img can overlay initials
- .lt-avatar img: use absolute inset:0 overlay (was flat width/height)
- .lt-avatar img.lt-avatar-img-err: display:none when JS marks broken image

Extended markdown styles:
- list-style: disc/decimal + colored ::marker (cyan/orange)
- mark, del, sub, sup, task-item, task-cb, task-done, task-todo
- Footnote helpers: fn-ref, fn-hr, fn-list, fn-item, fn-back
- .md-image alias on img rule

Footer keyboard hints:
- .lt-footer-hints, .lt-footer-hint, .lt-footer-key, .lt-footer-sep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:37:41 -04:00
jared 8df14ebbe3 fix: polish SLA banner component — gradient fill, dismiss persistence, terminal icons
Lint / JS (eslint) (push) Successful in 6s
- lt-sla-p2 .lt-sla-fill: upgrade from flat amber to gradient (#FFB300 →
  #ffd740) for visual consistency with P1 red→orange fill
- lt-sla-dismiss: add transition (0.15s ease) and :focus-visible outline so
  keyboard users get a visible focus ring
- Demo dismiss: replace .remove() with hidden + sessionStorage so banners
  stay dismissed across page navigation (data-sla-id attribute wires the key)
- Demo icons: swap emoji (🔴 🟠) for terminal-style text [ ! ] / [ ~ ] —
  emoji rendering is platform-specific and breaks the monospace aesthetic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:27:10 -04:00
jared 39862fab3b feat: add SLA banner component and gradient progress bar fills from design test
Lint / JS (eslint) (push) Successful in 8s
SLA banners (.lt-sla-p1 / .lt-sla-p2):
- P1 pulsing red banner with lt-sla-pulse keyframe
- P2 static amber banner
- Subcomponents: lt-sla-icon, lt-sla-info, lt-sla-title, lt-sla-bar,
  lt-sla-fill, lt-sla-meta, lt-sla-dismiss
- Light theme overrides included
- Demo section added to base.html with dismiss wiring

Progress bar gradient fills:
- Default (orange), --cyan, --green, --red variants now use
  linear-gradient fills instead of flat accent colors for more
  dramatic terminal readout appearance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 17:18:33 -04:00
jared 8f2f310fe2 Add unprefixed class aliases for monitoring app compatibility
Lint / JS (eslint) (push) Successful in 7s
Add .dot-up/.dot-down/.dot-degraded/.dot-unknown as aliases for lt-dot-* variants; add .chip/.chip-ok/.chip-warning/.chip-critical as aliases for lt-chip-* variants; add .lt-table tr.row-* state definitions for table row colouring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:46:57 -04:00
jared 19d6a2883c docs: add new-app guide and EJS layout skeleton
Lint / JS (eslint) (push) Successful in 13s
README.md:
- Add 'Starting a New App' section with step-by-step instructions:
  nginx alias setup, which skeleton to copy, how to define nav for
  each framework, app.css pattern, lt.init() call
- Update File Structure section: remove app-specific labels from
  framework skeletons (was 'PHP / Tinker Tickets' etc.), add
  layout.ejs to the node/ listing

node/layout.ejs:
- New EJS base layout skeleton matching the PHP/Python equivalents:
  generic nav via navLinks locals, lt-* class names throughout,
  CSP nonce on all script tags, pageStyles/pageScripts arrays,
  CURRENT_USER + CSRF_TOKEN globals injected at runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:00:01 -04:00
jared 75c57092f8 fix: make layout templates generic — remove app-specific nav and config
Lint / JS (eslint) (push) Successful in 10s
php/layout.php: nav is now data-driven via $navLinks array (supports
  top-level links, dropdowns, adminOnly flag); removed tinker_tickets
  hardcoded nav items; moved APP_CONFIG note to comment
python/base.html: nav driven by nav_links list from context; removed
  gandalf-specific routes (links_page, inspector, suppressions_page);
  removed APP_CONFIG.ticketWebUrl from shared script block; added
  nav_links format documentation in header comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:53:29 -04:00
jared f61705afb8 docs: add Authelia portal integration guide
Lint / JS (eslint) (push) Successful in 12s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:30:39 -04:00
jared 140a57a029 docs: add CI lint badge to README
Lint / JS (eslint) (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:28:23 -04:00
jared f9445ed5ae ci: add ESLint workflow
Lint / JS (eslint) (push) Has been cancelled
- .gitea/workflows/lint.yml: lint base.js and node/ directory
- .eslintrc.json: browser + node environment, CommonJS
- .gitignore: ignore node_modules/ and .env
- package.json + package-lock.json: eslint@8 dev dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:25:53 -04:00
jared 044adb3a18 Fix nav dropdown dismissing when cursor moves into menu
top:calc(100%+4px) left a 4px dead zone between the trigger and
menu that broke :hover continuity. Changed to top:100% with
padding-top:6px + margin-top:-2px so the hoverable area is
contiguous. Updated ::before decorative line to top:6px to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 00:43:33 -04:00
jared 842190d225 fix: light theme override for lt-nav-dropdown-menu
Menu had hardcoded dark background (rgba(6,12,20,0.98)) with no light
theme rule, leaving it black regardless of theme toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:59:57 -04:00
jared a807944b50 feat: implement RECOMMENDED_ADDITIONS — cursor, scanlines, radar, display-field, VT323
- --font-crt: 'VT323' token added to :root; VT323 added to Google Fonts link
- .lt-cursor / .lt-cursor--cyan/orange/red — blinking block cursor via CSS ::after
- .lt-scanlines — opt-in CRT horizontal scanline overlay on body/container (light-mode suppressed)
- .lt-radar / --sm / --lg / --green — radar sweep loading indicator as lt-spinner alternative
- .lt-display-field — readable non-editable field variant (distinct from :disabled opacity:0.45)
- base.html demos: radar variants in loading row, display-field in forms, cursor+VT323 in tags section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:50:46 -04:00
jared 1b7e57d9f5 Add gotchas section: disabled/readonly display patterns
Documents the opacity:0.45 behavior on :disabled and [readonly] elements
and the correct workarounds for display-only contexts (edit-mode toggle
selects, copy inputs, pre-wrap description areas).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 19:43:49 -04:00
jared 083a918729 docs: add LDAP avatar integration guide to README
Documents the lldap service account setup, avatar endpoint pattern,
CSS photo-over-initials approach, and HTML template used in tinker_tickets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:04:57 -04:00
jared de30ff13e6 fix: add in-memory _bootFired flag to runBoot as primary dedup guard
sessionStorage alone could be bypassed in hash-URL edge cases. A module-scoped
_bootFired flag blocks any second runBoot call within the same JS context
regardless of sessionStorage state, then sessionStorage handles cross-reload
suppression. Also restores boot call in private init() to preserve original
dual-path structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:52:31 -04:00
jared 32f7b49f98 fix: remove boot call from private init() to prevent hash-URL double-boot
Private init() and lt.init() both called runBoot(), creating two code paths.
With base.html# URLs the sessionStorage guard could be bypassed causing every
boot line to appear twice. Boot is now exclusively triggered by lt.init() or
lt.boot.run(), which already has its own _ltInitialized guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:46:34 -04:00
jared 80011e6de5 docs: update README for LotusGuild Terminal Design System v1.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:32:58 -04:00
jared d651cfbe2c audit pass 15: type=button on JS createElement and innerHTML buttons
- Toast close button: set closeEl.type = 'button' on createElement
- Combobox tag remove button: add type="button" to innerHTML string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:24:10 -04:00
jared bdf3ad085f audit pass 14: type=button on JS-generated button HTML strings
Add type="button" to all buttons created via innerHTML in JS:
- Lightbox close/prev/next buttons (3 instances)
- Pagination prev/page/ellipsis/next buttons (7 instances)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:22:11 -04:00
jared 5caaf38e9a audit pass 13: context menu arrow key navigation
Add ArrowUp/ArrowDown/Home/End keyboard navigation between context
menu items per WAI-ARIA menu widget specification. Focus wraps at
top and bottom boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:18:17 -04:00
jared 7630da8abd fix: boot sequence lines appearing twice
Two init paths both called runBoot() before sessionStorage was set
(the key was only written on completion, ~1.1s later):
- Private init() at DOMContentLoaded → runBoot()
- lt.init() HTML call on DOMContentLoaded → ltInit() → runBoot()

Both found no session key and started simultaneous setInterval loops
on the same <pre>, each appending the same message → every line twice.

Fix: set the sessionStorage key immediately at the start of runBoot()
so any concurrent second call exits early.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:12:05 -04:00
jared 45b968b77d audit pass 12: type=button, focus restore, :focus-visible on links
HTML:
- Add type="button" to all remaining buttons (nav drawer close, menu
  btn, theme toggle, notif bell, right drawer close, tab buttons,
  sidebar toggle, alert close x4, code copy, tab bar buttons x4,
  detail panel open, modal close x2, keyboard shortcuts close)
- Add aria-label="Search commands" to command palette input
- Notification panel close(true): restore focus to bell on Escape
- Generic dropdowns: add Escape key handler with trigger focus restore

CSS:
- Add a:focus-visible global focus ring
- Add .lt-nav-dropdown-menu li a:focus-visible
- Add .lt-markdown a:focus-visible
- Fix dead .lt-typeahead-option selector → .lt-typeahead-item with
  :hover, .is-focused, :focus-visible for light theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 13:49:23 -04:00
jared ca2d6d225e audit pass 10-11: type=button, XSS escaping, focus/ARIA fixes
HTML:
- Add type="button" to all buttons outside forms (22 instances)
- Add aria-label="Add comment" to unlabelled textarea#td-comment

JS:
- Escape alt text and link text in markdown renderer with escHtml()
  to prevent XSS in image alt/link content
- Fix nested modal focus: only restore trigger focus when no other
  modal is still open; add document.contains guard

CSS:
- Add .lt-nav-link:focus-visible focus ring (was missing entirely)
- Fix .lt-typeahead-option (dead selector) → .lt-typeahead-item with
  :hover, .is-focused, and :focus-visible for light theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:46:31 -04:00
jared 8b54efef61 audit pass 9: XSS fix, focus management, ARIA labels, and :focus-visible gaps
JS:
- Lightbox: remove keydown listener on close (memory leak fix)
- Lightbox: restore focus to trigger on close
- Right drawer: fix aria-hidden="false" anti-pattern to removeAttribute
- Markdown renderer: block javascript:/data: protocol URIs in link and
  image replacements to prevent XSS
- Sidebar submenus: add aria-expanded tracking on toggle; hide decorative
  chevron from screen readers; initialize aria-expanded on mount

CSS:
- Add :focus-visible to .lt-sidebar-toggle (interactive button)
- Add :focus-visible to .lt-dropzone (focusable container)
- Fix .lt-stat-card:focus-visible outline-offset to -2px (clip-path clips it)
- Add light theme override for .lt-nav-drawer-link:focus-visible
- Adjust .lt-split-divider:focus-visible outline-offset to 3px

HTML:
- Range input: update aria-valuenow dynamically on input event
- Combobox label: add for="demo-combobox-input" association
- Typeahead label: add for="demo-typeahead-input" association
- Dropzone file input: add aria-label
- Notification items: add descriptive aria-label to all 4 items;
  add aria-hidden="true" to decorative dot spans
- Mark all read button: add type="button" to prevent accidental form submit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:16:12 -04:00
jared e8c1197613 audit pass 8: keyboard accessibility and table semantics
CSS:
- Fix light theme input/select/textarea :focus -> :focus-visible

HTML:
- Worker metrics table: convert label <td> to <th scope="row"> for screen readers
- Add aria-label to worker metrics table
- Sticky table: add scope="col" to all column headers
- Keyboard shortcuts modal table: add scope="col" to headers
- Kanban cards: remove tabindex="0" from role="article" (non-interactive)
- Advanced filter: ensure all 3 label/select pairs have for/id associations

JS:
- Lightbox: fix keydown listener leak by storing bound reference for removeEventListener
- Lightbox: save/restore trigger focus on open/close
- Sortable table: add tabindex="0" and Enter/Space keydown handler on sortable <th>
- Split pane: add tabindex="0", role="separator", aria-label, and arrow/Home/End
  keyboard resize support on divider (5% steps)
- Form validation: handle <select multiple> required check via selectedOptions.length

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:09:29 -04:00
jared b84d71dd7a audit pass 7: ARIA, focus management, and label fixes
CSS:
- Add :focus-visible to sortable th, breadcrumb links, list links, cmd-item,
  combobox-option, typeahead-item, sidebar-sub-link, split-divider, stat-card
- Fix .lt-skip-link:focus to also include :focus-visible for spec compliance

JS:
- Mobile nav: add focus trap (_trapFocus), save/restore trigger focus, fix
  aria-hidden="false" to removeAttribute pattern, add document.contains guard
- Combobox: add aria-activedescendant on _moveFocus; add unique IDs to options;
  clear aria-activedescendant on close; wrap querySelectorAll in Array.from
- Typeahead: add aria-activedescendant on _moveFocus; add unique IDs to items;
  add aria-busy during async search; clear aria-activedescendant on select;
  wrap querySelectorAll in Array.from
- Command palette: add unique IDs to items; set/clear aria-activedescendant
  on move and mouseenter; clear on close
- Lightbox: add document.contains guard on focus setTimeout
- Stats filter: add Enter/Space keyboard handler for role="button" cards

HTML:
- Stat cards: add role="button" tabindex="0" aria-label (interactive divs)
- Advanced filter selects: add id/for associations to all 3 label+select pairs
- Accordion SVG icons: add aria-hidden="true" (decorative)
- Range input: add aria-label, aria-valuemin/max/now
- Wizard form controls: add id/for to all 4 label+input/select pairs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 20:02:15 -04:00
jared d08007cdd7 audit pass 6: accessibility, ARIA, and keyboard fixes
- JS: fix checkbox/radio required validation using .checked not .value
- JS: guard _cpTrigger.focus() with document.contains() check
- JS: add arrow/Home/End key navigation to tab groups (WCAG 2.1)
- JS: clamp context menu left edge with Math.max(8, ...) to prevent off-screen
- JS: fix wizard _show() to removeAttribute aria-hidden on active step
- HTML: add role="region" + aria-label to notification panel
- HTML: convert Assigned To span+div to label+select with for/id association
- HTML: add role="article" tabindex="0" aria-label to all kanban cards
- HTML: remove aria-hidden="false" anti-pattern from wizard active step
- CSS/HTML/JS: replace aria-hidden="false" show-hook with :not([aria-hidden])
  so open state is represented by absent attribute rather than false value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 19:51:21 -04:00
jared fdcadad23b fix: accessibility & quality audit pass 4+5
CSS:
- Add :active/:focus-visible to .lt-modal-close, .lt-drawer-right-close,
  .lt-notif-panel-clear, .lt-file-item-remove
- Add :focus-visible to .lt-accordion-header, .lt-tag-remove,
  .lt-combobox-tag-remove
- Add .lt-cmd-input-wrap:focus-within focus indicator (outline:none compensation)
- Add will-change: stroke-dashoffset to .lt-gauge-fill
- Add range slider :focus-visible thumb ring
- Fix .tok-cmt hardcoded #5c8c6a → var(--color-tok-cmt) w/ light-mode override
- Add .lt-skip-link component (visible on focus)
- Fix .lt-filter-group fieldset UA border reset

JS:
- Fix infinite scroll: store throttled handler ref so removeEventListener works
- Fix right drawer: remove close-button listeners in _rdClose (were never removed)
- Fix right drawer: add Tab focus trap (matches modal behaviour)
- Fix _cmdPaletteClose: restore focus to element that opened the palette
- Fix initSortTable: set aria-sort="ascending"/"descending"/"none" on th elements
- Fix switchTab: set aria-selected="true"/"false" on .lt-tab[data-tab] buttons
- Fix copy button timeout: guard with document.contains() before DOM mutation
- Fix combobox: add role=combobox, aria-expanded, aria-controls, role=listbox;
  toggle aria-expanded on open/close

HTML:
- Add skip nav link + id="main-content" on <main>
- Primary tab nav: add role=tablist, role=tab, aria-selected, aria-controls,
  id attrs; tab panels get role=tabpanel + aria-labelledby
- Tab bar demo: same ARIA wiring + aria-controls + role/labelledby on panels
- Sidebar filters: convert div+span to fieldset+legend for proper grouping
- Table sort headers: add aria-sort="none" (JS updates on click)
- Accordion: add aria-controls on headers, IDs on bodies
- Wizard: add aria-current="step" on active step indicator
- Table th: scope="col" on all column headers
- Row checkboxes: aria-label per ticket ID
- Worker metrics table: add <tbody>
- Progress bars: role=progressbar + aria-valuenow/min/max + aria-label
- Export + keyboard shortcuts modals: role=dialog, aria-modal, aria-labelledby

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:22:53 -04:00
jared 8585993602 merge: v1.1 feature branch → main
Merges all v1.1 work:
- 26 new components (theme toggle, right drawer, context menu, combobox,
  typeahead, split pane, wizard, sortable, lightbox, infinite scroll,
  countdown/stopwatch, markdown, auth, WebSocket, offline banner,
  avatar, timeline, skeleton variants, empty state, notification badge,
  chart containers, sidebar submenus, cookie utils, toast progress)
- Full mobile/responsive audit (50+ fixes, 8-breakpoint system)
- Complete light mode rebuild (professional, all components covered)
- lt.init() master initializer
- Bug fixes: duplicate function decls, clipboard crash, toast infinite
  recursion, kanban drag-and-drop, pagination module, notification
  dropdown, bulk actions dropdown, ticket detail editable form
- Cross-tab theme sync, color-scheme meta, WCAG table caption

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 00:12:24 -04:00
jared d5dc96178f feat: final production polish — light mode rebuild, lt.init(), cross-tab theme
Light mode — complete rebuild (section 51):
- Full token override for every surface, component, and state
- Desaturated accents: orange #c44e00, cyan #0062b8, green #006d35 etc.
- Glows replaced with subtle drop-shadow rings (no neon on white)
- Covers: toasts, modals, dropdowns, notification panel, context menu,
  right drawer, combobox, typeahead, wizard, timeline, pagination, tabs,
  badges, tables, code blocks, skeleton, avatar, sortable, markdown,
  scrollbars, buttons, nav drawer, WS status — nothing missed
- Dot-grid: neutral gray (was incorrect blue tint)
- Toast background uses var(--bg-overlay) instead of hardcoded dark rgba

Theme system improvements:
- _applyTheme now sets document.documentElement.style.colorScheme
- theme-color meta tag syncs on toggle (browser chrome follows theme)
- Cross-tab sync via storage event (toggle in one tab updates others)

lt.init() master initializer:
- Single call wires accordion, tooltip, alerts, clipboard, sidebar,
  submenus, and boot sequence
- Replaces 6+ individual .init() calls in user code
- demo HTML updated to use lt.init({ bootName: 'MY APP' })

Accessibility:
- Ticket table: add <caption class="lt-sr-only"> for WCAG compliance
- Table gets aria-label attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 00:12:04 -04:00
jared 1d8d274fdd feat: comprehensive mobile/responsive audit fixes
Touch targets:
- All .lt-btn-sm, .lt-menu-btn, .lt-page-btn, .lt-dropdown-trigger,
  .lt-notif-bell-btn now 44px minimum on pointer:coarse devices

Font size floors:
- Nothing below 0.72rem on XS; nav drawer, cmd palette, stats, timeline,
  dropdown items, notification timestamps all bumped to readable sizes

Notification panel:
- width: min(300px,92vw) prevents overflow on 320px screens
- max-height: min(280px,calc(80vh-110px)) for landscape safety
- title: 2-line clamp instead of single-line truncate
- Header/time font sizes increased to 0.75rem/0.7rem

Dropdown panels:
- Advanced filter: clamp(200px,60vw,260px) instead of fixed 240px
- All dropdowns: max-width:90vw + proper alignment on SM/XS
- Dropdown items: 0.78-0.8rem, 36px min-height

Toolbar:
- toolbar-left/right now flex-wrap on SM+XS; search expands full-width
- Result count hidden on XS (.lt-hide-xs) to uncramp narrow toolbars

Drawer:
- Right drawer goes full-width (100vw) on SM with border-top
- Status/Priority 2-col form grid collapses to 1-col (.lt-drawer-form-grid)

Table card mode:
- Breakpoint corrected from rogue 640px → 767px (system breakpoint)
- Row min-height 36px, font bumped to 0.78rem

Kanban:
- Keeps 2-col at SM and XS (was collapsing to 1-col, confusing semantics)

Grids:
- Chart demo, sortable demo: auto-fit minmax so they reflow naturally
- Countdown stat grid: auto-fit minmax(160px,1fr) instead of fixed 3-col
- Grid gap reduced to var(--space-sm) at XS

Split pane:
- Demo height: clamp(160px,30vh,240px) instead of fixed 200px
- Stacks vertically on SM with horizontal divider

Modals:
- max-width: 640px cap prevents 4K-wide modals; overridden on SM/XS

Header:
- Brand title truncated at max-width:110px on XS
- WS status indicator hidden on XS to free header space
- Admin badge hidden on XS
- Safe-area insets applied to header-right and toast container

Landscape phone:
- Modal max-height respects viewport; notif list shrinks

Breakpoint consistency:
- Removed rogue 640px breakpoint; all queries use 479/767/1023/1279px system

Section 78 added with all targeted overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:51:21 -04:00
jared 6ee9760168 fix: toast crash, notification dropdown, ticket detail editable, toolbar dropdowns
- Fix duplicate function showToast declaration causing infinite recursion
  (showToast declared twice → function hoisting → _origShowToast === self)
  Progress bar now inlined directly into _displayToast; Module 47 removed
- Notification bell now opens a proper dropdown panel with unread items,
  per-item click-to-read, "Mark all read", close on outside click/Esc
- Ticket detail drawer now has real editable fields (title, status,
  priority, assignee, description textarea, comment box) instead of
  read-only KV pairs; Save Changes and Post Comment buttons functional
- Advanced ▾ filter dropdown: status/priority/assignee selects + Apply/Reset
- Bulk Actions dropdown: Close/Reassign/Export/Delete with toast feedback
- Generic .lt-dropdown-trigger toggle system (works for any future dropdown)
- Add CSS sections 76 (notification panel) and 77 (dropdown widget + .lt-textarea)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:36:29 -04:00
jared 3847513594 fix: resolve DOMContentLoaded crash + wire kanban/pagination
- Fix lt.clipboard.init() → initCopyButtons() (crashed init handler
  before context menu, split pane, lightbox, theme btn could register)
- Add lt.pagination module (Module 55) with ellipsis rendering,
  prev/next, onChange callback; wire demo-pagination nav
- Upgrade lt.sortable for cross-list group dragging (shared module-level
  drag state enables kanban card movement between columns)
- Wire kanban columns (open/pending/inprogress/closed) to lt.sortable
  with group:'kanban' for drag-and-drop between columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:19:49 -04:00
jared 0c2e136cae Fix: resolve JS SyntaxErrors that crashed entire window.lt namespace
Root cause: two SyntaxErrors prevented the IIFE from completing,
meaning window.lt was never created and all features were broken.

Fixes applied:
1. CRITICAL: Renamed auth's apiFetch redeclaration → _apiFetchAuth
   (duplicate function declaration in strict-mode IIFE = SyntaxError)
2. CRITICAL: Renamed toast queue vars (_toastQueue/_toastActive already
   declared by original showToast in outer scope = SyntaxError)
   Replaced heavy queue wrapper with lightweight progress-bar injector
3. timer.countdown: classList.add(urgentClass) throws on space-separated
   class string — split/filter/forEach now used
4. contextMenu: _ctxItems declared as [] but used as string-keyed object
   — changed to {}
5. Split pane divider: background was border-dim (7% opacity, invisible)
   — raised to border-color (16%), added ::before hit-area expansion,
   hover state uses accent-cyan for clear visual feedback
6. Light theme: html background-image was hardcoded cyan rgba, didn't
   update with data-theme — added explicit html[data-theme="light"] rule
7. Light theme: .lt-header background was hardcoded rgba(3,5,8,0.96)
   — added explicit light-mode override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 23:04:24 -04:00
jared 6ad4cb2354 v1.1: Complete remaining 8 feature modules + 7 CSS component sections
JS modules added:
- lt.sidebarSubmenus — nested nav groups with expand/collapse, auto-opens active
- lt.infiniteScroll  — IntersectionObserver-based (scroll fallback), loading indicator
- lt.wizard          — multi-step form with indicators, validation hook, getData()
- lt.sortable        — HTML5 drag-to-reorder lists with placeholder ghost + bus event
- lt.timer           — countdown (urgent threshold + onExpire) + stopwatch (pause/reset)
- lt.lightbox        — full-screen image viewer, prev/next, ESC, caption, loop
- lt.auth            — JWT token management: setToken, refresh (auto + manual),
                       401 retry, onExpire hook, patches lt.api with Bearer header
- lt.markdown        — micro-renderer (no deps); auto-delegates to window.marked /
                       markdownit if present; renders headings/bold/italic/code/
                       links/lists/blockquotes/tables/HR

CSS sections added (69–75):
- Infinite scroll sentinel + loading indicator
- Wizard step indicators (connectors, active/complete/error states, nav footer)
- Sortable item dragging + placeholder ghost
- Countdown/timer display + urgency blink animation
- Image lightbox overlay (close/prev/next controls, caption, counter)
- Sidebar submenu groups (chevron, expand/collapse, active sub-link)
- Markdown output styling (.lt-markdown — all block elements themed)

HTML demos for all 8 new components added and wired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 22:42:16 -04:00
jared 0eb91f1937 v1.1: Add 10 new feature modules + 18 CSS component sections
JS modules added:
- lt.theme    — dark/light toggle, OS preference sync, localStorage persist
- lt.notif    — notification badge (set/inc/clear) on any element
- lt.rightDrawer — right-side detail panel with focus trap + return focus
- lt.contextMenu — right-click custom menu, keyboard nav, danger variant
- lt.offline  — navigator.onLine banner + event hooks
- lt.ws       — WebSocket manager with exponential backoff reconnect
- lt.combobox — multi-select with search, tag chips, keyboard nav
- lt.typeahead — async/sync autocomplete with match highlighting
- lt.cookie   — get/set/del with SameSite/Secure helpers
- lt.splitPane — pointer-events resizable split pane (horizontal/vertical)
- Toast queue: max-stack, progress bar drain animation, auto-drain

CSS sections added (51–68):
- Light theme (html[data-theme="light"]) with full variable overrides
- Theme toggle button (.lt-theme-btn)
- Skeleton loader variants (card, row, text, title, avatar, btn, badge)
- Empty state component (.lt-empty-state, --sm variant)
- Nav notification badge (.lt-notif-wrap / .lt-notif-badge)
- Right-side drawer (.lt-drawer-right + overlay)
- Sticky table header (.lt-table-sticky-wrap)
- Multi-select combobox (.lt-combobox, tags, dropdown)
- Context menu (.lt-context-menu, divider, label, danger)
- Offline banner (.lt-offline-banner)
- Timeline / activity feed (.lt-timeline, color variants)
- Avatar + avatar group + status ring (.lt-avatar)
- Split pane (.lt-split, .lt-split-divider with pointer drag)
- Chart container (.lt-chart-wrap, legend, axis, loading state)
- Toast queue stack + progress drain bar
- Autocomplete / typeahead (.lt-typeahead-dropdown, match highlight)
- WebSocket status indicator (.lt-ws-status, data-state variants)
- Print enhancements (extended @media print rules)

HTML demo sections for all new components added to base.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 22:29:55 -04:00
jared db67f0c92b v1.2 → v1.3: responsive overhaul, mobile nav, accessibility & bug fixes
- 8-breakpoint responsive system (xs→4k) replacing 3-breakpoint stub
- Off-canvas mobile nav drawer with swipe gestures and focus trap
- iOS-safe scroll lock for modals (position:fixed pattern + ref count)
- Modal focus trap with Tab cycling and return-focus to trigger
- Z-index stack overhaul: modal > nav drawer, toast above scanlines
- Fixed --border-dim CSS variable undefined across all v1.2 components
- Fixed badge absolute positioning clipping in Component Reference
- Fixed accordion CSS class mismatch (open → is-open)
- Fixed command palette selector/class mismatches
- Select dark mode: color-scheme:dark + option background
- 4K scaling: rem-based overrides for all hardcoded px elements
- Safe area insets for iPhone notch/home bar (viewport-fit:cover)
- Touch targets: 44px min on all interactive elements (pointer:coarse)
- Disabled glitch/pulse animations on coarse/hover:none devices
- Table responsive card mode with data-label attributes
- viewport.is() validation, debounce caching, 350ms orientation debounce
- initMobileNav() guard prevents duplicate listener registration
- cmd palette: input.select() on open, consistent scroll lock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 18:47:26 -04:00
13 changed files with 12140 additions and 2135 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"env": { "browser": true, "node": true, "es2021": true },
"parserOptions": { "ecmaVersion": 2021, "sourceType": "script" },
"rules": {
"no-unused-vars": "warn",
"no-undef": "warn",
"no-empty": "warn",
"semi": ["error", "always"],
"eqeqeq": "warn"
}
}
+20
View File
@@ -0,0 +1,20 @@
name: Lint
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
js-lint:
name: JS (eslint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install ESLint
run: npm install --save-dev eslint@8
- name: Run ESLint
run: npx eslint --ext .js base.js node/
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.env
*.env
+495
View File
@@ -0,0 +1,495 @@
# Authelia Portal — LotusGuild Terminal Design System Integration
This document covers everything that had to be **written from scratch** to theme the
Authelia authentication portal using the LotusGuild Terminal Design System v1.2.
It is a companion to `base.css` / `base.js` for future integrations with third-party
applications that use their own component frameworks.
---
## Why a separate file at all?
`base.css` targets exclusively `.lt-*` class names — the design system's own prefix.
Authelia's frontend is a React single-page application built on
**Material UI (MUI) v5**, which generates its own stable class names like
`.MuiCard-root`, `.MuiButton-containedPrimary`, etc.
Bridging the two requires a translation layer: the same design tokens and visual
patterns from `base.css`, re-expressed against MUI's class structure.
Additionally, Authelia v4.x (latest: 4.39.x) does **not** support native CSS
injection — `server.asset_path` only handles `logo.png` and `favicon.ico`.
The CSS is injected via **nginx `sub_filter`** in Nginx Proxy Manager, which rewrites
Authelia's HTML on the fly to include a `<link>` tag before `</head>`.
---
## Deployment architecture
```
Browser
└─► https://auth.lotusguild.org (NPM — LXC 139)
├─ GET /custom.css ──────── served from /data/custom_assets/authelia-custom.css
│ (nginx alias, no upstream request)
└─ GET /* ──────────────── proxied to Authelia (LXC 167 :9091)
HTML response is rewritten by sub_filter:
</head> → <link rel="stylesheet" href="/custom.css"></head>
```
**NPM config changed:** `/data/nginx/proxy_host/29.conf` (auth.lotusguild.org proxy host)
```nginx
# Serve LotusGuild Terminal custom CSS
location = /custom.css {
alias /data/custom_assets/authelia-custom.css;
add_header Content-Type "text/css";
add_header Cache-Control "public, max-age=3600";
}
location / {
...existing headers...
# Prevent upstream gzip so sub_filter can inspect the body
proxy_set_header Accept-Encoding "";
# Inject CSS link before </head>
sub_filter '</head>' '<link rel="stylesheet" href="/custom.css"></head>';
sub_filter_once on;
include /etc/nginx/conf.d/include/proxy.conf;
}
```
> **Warning:** NPM regenerates proxy_host conf files when you edit a proxy host in
> the UI. If host 29 is ever modified through NPM, re-apply the `sub_filter` block
> and the `/custom.css` location manually, or set the config via NPM's
> "Advanced" tab before saving.
**Authelia config changed:** `/etc/authelia/configuration.yml` — added `asset_path`
under `server:` so Authelia serves the resized logo and favicon:
```yaml
server:
address: tcp://0.0.0.0:9091
asset_path: /etc/authelia/assets
```
Assets in `/etc/authelia/assets/`:
- `logo.png` — 256 × 256 PNG resized from Tinker Tickets `assets/images/favicon.png`
- `favicon.ico` — 32 × 32 ICO from the same source
---
## What is NOT in `base.css` and had to be written
The sections below are ordered as they appear in `authelia-custom.css`.
---
### 1. Re-declaring design tokens with `!important`
**In `base.css`:** `:root { ... }` is declared once, without `!important`.
**What had to be added:** The entire `:root` block is repeated in `custom.css` because
Authelia's HTML is a self-contained SPA — `base.css` is never loaded. More critically,
MUI injects its own inline styles and CSS-in-JS rules at very high specificity.
Every property that needs to override MUI requires `!important`. `base.css` never
uses `!important` because it owns its namespace; here we are guests in MUI's DOM.
```css
/* base.css does NOT use !important anywhere */
body { background-color: var(--bg-primary); }
/* custom.css must fight MUI's inline styles */
body { background-color: var(--bg-primary) !important; }
```
The token values themselves are identical to `base.css` — only the override
mechanism is new.
---
### 2. Universal box-sizing reset with `!important`
**In `base.css`:** `*, *::before, *::after { box-sizing: border-box; }` — no flag needed
because `base.css` loads first.
**What had to be added:**
```css
*, *::before, *::after {
box-sizing: border-box !important;
}
```
MUI components set `box-sizing` inline on some elements. Without the flag the
clip-path geometry breaks on inputs and buttons.
---
### 3. MUI typography selectors
**In `base.css`:** Typography rules target `h1h6`, `a`, `p`, `body` — standard HTML
elements. No MUI class names exist in the design system.
**What had to be added:** Authelia renders text almost exclusively through MUI's
`<Typography>` component, which produces elements with `.MuiTypography-root` and
variant classes. Without targeting these, all text inherits MUI's Roboto font and
light grey colour instead of JetBrains Mono and `--text-primary`.
```css
.MuiTypography-root,
.MuiInputBase-input,
.MuiFormLabel-root,
.MuiFormHelperText-root,
label, p, span, div {
font-family: var(--font-mono) !important;
color: var(--text-primary) !important;
}
h1, h2, h3, h4, h5, h6,
.MuiTypography-h5,
.MuiTypography-h6 {
color: var(--accent-orange) !important;
text-shadow: var(--glow-orange) !important;
...
}
```
---
### 4. `.MuiCard-root` / `.MuiPaper-root` — the login card
**In `base.css`:** `.lt-card` implements the terminal card with `clip-path`, border,
background, and the `::before` corner triangle accent.
**What had to be added:** Authelia wraps its login form in a MUI `<Card>` (which
extends `<Paper>`). Neither selector exists in `base.css`. The visual result is
identical to `.lt-card` — the clip-path polygon, cyan border, box-glow, and
corner triangle are all copied — but they target different class names and require
`!important` throughout to override MUI's elevation shadows and `border-radius: 4px`.
```css
.MuiCard-root,
.MuiPaper-root {
background: var(--bg-card) !important;
border: 1px solid var(--border-color) !important;
border-radius: 0 !important; /* ← overrides MUI default */
clip-path: polygon(...) !important; /* same geometry as .lt-card */
...
}
.MuiCard-root::before,
.MuiPaper-root::before {
/* same corner triangle as .lt-card::before */
}
```
---
### 5. Logo element — `img[alt="Authelia"]`
**In `base.css`:** No equivalent. The design system has no logo slot component.
**What had to be added:** Authelia renders the portal logo as
`<img alt="Authelia" src="./static/media/logo.png">`. This selector targets it
specifically to apply the cyan drop-shadow and orange hover glow. Without it the
logo renders without any terminal aesthetic treatment.
```css
img[alt="Authelia"] {
filter: drop-shadow(0 0 12px rgba(0,212,255,0.4)) !important;
transition: filter 0.25s ease !important;
}
img[alt="Authelia"]:hover {
filter: drop-shadow(0 0 18px rgba(255,107,0,0.5)) !important;
}
```
---
### 6. MUI outlined input family
**In `base.css`:** `.lt-input`, `.lt-select`, `.lt-textarea` — custom elements with
`clip-path`, terminal background, and cyan focus ring.
**What had to be added:** MUI's outlined text field is a composition of three
separate elements that each need individual targeting:
| MUI class | What it controls | Equivalent in `base.css` |
|-----------|-----------------|--------------------------|
| `.MuiOutlinedInput-root` | Outer wrapper — background, clip-path | `.lt-input` outer |
| `.MuiOutlinedInput-notchedOutline` | The SVG border element | `.lt-input` border |
| `.MuiInputBase-input` | The actual `<input>` inside | `.lt-input` inner text |
| `.MuiInputLabel-root` | Floating label | `.lt-label` |
| `.MuiFormHelperText-root` | Error / hint text below field | `.lt-form-hint` |
MUI splits the border rendering into a separate SVG `<fieldset>` element
(`.MuiOutlinedInput-notchedOutline`), which requires its own border-color override.
`base.css` has no equivalent splitting since `.lt-input` is a single element with a
CSS border.
The `.Mui-focused` and `.Mui-error` state classes also need explicit overrides because
MUI applies its own colour through CSS-in-JS with high specificity.
```css
.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
border-color: var(--accent-cyan) !important;
box-shadow: var(--box-glow-cyan) !important;
}
.MuiFormHelperText-root.Mui-error {
color: var(--accent-red) !important;
text-shadow: var(--glow-red) !important;
}
```
Additionally, `caret-color` is set explicitly on the input because MUI does not
expose this through its theme — the blinking text cursor would otherwise be white.
---
### 7. MUI button family
**In `base.css`:** `.lt-btn`, `.lt-btn-primary`, `.lt-btn-danger`, `.lt-btn-ghost`
all using `clip-path` hexagon cuts, transparent backgrounds, and border+glow styling.
**What had to be added:** MUI uses four separate button variant classes. The primary
(sign-in) button is `.MuiButton-containedPrimary` which by default renders as a
solid filled rectangle. The entire button appearance — transparent background,
border, clip-path, text-transform, letter-spacing — has to be explicitly overridden
because MUI's `contained` variant applies an opaque `background-color` inline.
```css
/* MUI default: background-color: #1976d2; border-radius: 4px; box-shadow: ... */
.MuiButton-containedPrimary {
background: transparent !important; /* fight the inline fill */
border: 1px solid var(--accent-orange-border) !important;
border-radius: 0 !important;
clip-path: polygon(...) !important;
...
}
```
Text/link buttons (`.MuiButton-text`) are separate from contained buttons in MUI but
map to the same `.lt-btn-ghost` pattern in `base.css`.
---
### 8. MUI checkbox and switch
**In `base.css`:** `.lt-checkbox`, `.lt-switch` — fully custom-drawn using CSS only.
**What had to be added:** Authelia uses MUI's `<Checkbox>` (remember-me) and
`<Switch>` (settings toggles). These are SVG-based components. Because MUI controls
their colour through injected CSS variables, the only reliable override is to target
`.Mui-checked` state classes and use `!important`. There is no equivalent split
between `track` and `thumb` in `base.css`.
```css
.MuiSwitch-switchBase.Mui-checked { color: var(--accent-cyan) !important; }
.MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track {
background-color: var(--accent-cyan) !important;
opacity: 0.5 !important;
}
```
---
### 9. MUI alert / notification banners
**In `base.css`:** `.lt-inline-*` for inline messages (`.lt-inline-success`,
`.lt-inline-error`, etc.) and `.lt-toast-*` for toasts. These use a left border
accent pattern.
**What had to be added:** Authelia uses MUI `<Alert>` for login errors, success
messages, and 2FA prompts. MUI Alert has its own severity system with four variant
classes (`.MuiAlert-standardError`, etc.). Each severity needs explicit colour,
background, and border-color overrides. The `border-left: 3px solid currentColor`
pattern is adapted from `base.css`'s inline message style but applied to MUI's
class names with `clip-path: none !important` to cancel MUI's rounded corners.
---
### 10. MUI select, menu, and popover
**In `base.css`:** `.lt-select` for the select element itself; no dropdown overlay
component exists since the design system uses native `<select>`.
**What had to be added:** MUI's `<Select>` renders its dropdown as a `<Paper>`
element portalled to `<body>` with class `.MuiMenu-paper`. Without targeting this
separately, the dropdown popup inherits the browser default white background and
Roboto font, appearing completely unstyled. `.MuiMenuItem-root` hover and selected
states also need individual rules.
---
### 11. OTP / 2FA digit inputs — `input[type="tel"]`, `input[type="number"]`
**In `base.css`:** No equivalent. The design system has no numeric code input.
**What had to be added:** Authelia's one-time-password entry uses `<input type="tel">`
elements arranged in a row. These need oversized, centered, cyan-glowing characters
with wide letter-spacing to render like a retro code display.
```css
input[type="tel"],
input[type="number"] {
font-size: 1.2rem !important;
font-weight: 700 !important;
color: var(--accent-cyan) !important;
text-shadow: var(--glow-cyan) !important;
letter-spacing: 0.15em !important;
text-align: center !important;
caret-color: var(--accent-orange) !important;
}
```
---
### 12. MUI stepper — 2FA flow breadcrumb
**In `base.css`:** `.lt-stepper`, `.lt-step`, `.lt-step-num` — a custom CSS stepper
for multi-step wizard flows.
**What had to be added:** Authelia renders a MUI `<Stepper>` at the top of the 2FA
flow showing steps like "Username → Password → TOTP". MUI Stepper uses distinct
classes for label, icon, and connector that don't map 1-to-1 to `.lt-step-*`.
The step icon is an SVG circle rendered by React so it can only be tinted via
`color` and `filter: drop-shadow(...)`. The connector line between steps is a
separate `<hr>` with class `.MuiStepConnector-line`.
```css
.MuiStepIcon-root.Mui-active {
color: var(--accent-orange) !important;
filter: drop-shadow(0 0 6px rgba(255,107,0,0.6)) !important;
}
.MuiStepIcon-root.Mui-completed {
color: var(--accent-green) !important;
}
```
---
### 13. MUI linear and circular progress
**In `base.css`:** `.lt-progress` / `.lt-progress-bar` — a standard CSS progress bar
using `width` transitions. `.lt-spinner` — a rotating pseudo-element spinner.
**What had to be added:** Authelia uses MUI `<LinearProgress>` during API calls and
`<CircularProgress>` as the main loading spinner. MUI Linear Progress animates its
own internal elements (`.MuiLinearProgress-bar`) with keyframe animations; it cannot
be replaced with the `.lt-progress` pattern. The cyan-to-orange gradient and
`box-shadow` glow are new here — `base.css`'s progress bar uses a solid
`--accent-orange` fill.
```css
.MuiLinearProgress-bar {
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-orange)) !important;
box-shadow: 0 0 8px rgba(0,212,255,0.5) !important;
}
```
---
### 14. MUI tooltip
**In `base.css`:** `.lt-tooltip` — a CSS-only tooltip shown via `:hover` on a
`[data-tooltip]` attribute.
**What had to be added:** MUI `<Tooltip>` portals its bubble to `<body>` with class
`.MuiTooltip-tooltip`. It is a separate DOM node, not a pseudo-element, so
`.lt-tooltip` styles cannot reach it. A standalone rule sets the terminal background,
monospace font, and squared border.
---
### 15. `*:focus-visible` global ring
**In `base.css`:** Focus rings are defined individually per component
(`.lt-btn:focus-visible`, `.lt-input:focus-visible`, etc.).
**What had to be added:** A single global rule is more practical here because
Authelia contains many interactive MUI elements not known at design time, and
adding per-component rules for every possible focusable element would be brittle.
```css
*:focus-visible {
outline: 2px solid var(--accent-cyan) !important;
outline-offset: 2px !important;
box-shadow: var(--box-glow-cyan) !important;
}
```
---
### 16. Branding footer watermark — `#root::after`
**In `base.css`:** No equivalent. The design system does not add page-level
watermarks.
**What had to be added:** Authelia has no footer. The `#root::after` pseudo-element
attaches a fixed-position text watermark reading
`LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG` at the bottom of the viewport.
This uses `--text-muted`, `--font-mono`, uppercase, and wide letter-spacing to match
the terminal aesthetic without adding any HTML.
```css
#root::after {
content: 'LOTUSGUILD SECURE PORTAL // AUTH.LOTUSGUILD.ORG';
position: fixed;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 0.55rem;
letter-spacing: 0.18em;
color: var(--text-muted);
opacity: 0.6;
pointer-events: none;
}
```
---
## Patterns shared with `base.css` (not new)
The following are **not** documented above because they are direct re-uses of
existing `base.css` patterns, only re-targeted to new selectors:
| Pattern | `base.css` source | Re-used in `custom.css` for |
|---------|------------------|-----------------------------|
| Dot-grid background | `html { background-image: radial-gradient(...) }` | `body` |
| Scanlines overlay | `body::before` | `body::before` (same rule, `!important` added) |
| Corner vignette | `body::after` | `body::after` (same rule, `!important` added) |
| Card clip-path polygon | `.lt-card { clip-path: polygon(...) }` | `.MuiCard-root` |
| Card corner triangle | `.lt-card::before` | `.MuiCard-root::before` |
| Button clip-path hexagon | `.lt-btn { clip-path: polygon(...) }` | `.MuiButton-containedPrimary` |
| Input clip-path cut | `.lt-input { clip-path: polygon(...) }` | `.MuiOutlinedInput-root` |
| Scrollbar | `::-webkit-scrollbar-*` section 45 | identical, `border-radius: 0` added |
| Link colours | `a { color: var(--accent-cyan) }` | `a` (same, `!important` added) |
| Divider | `.lt-divider` border-color | `.MuiDivider-root` |
---
## Notes for future third-party integrations
1. **Always check for `!important` requirements.** CSS-in-JS frameworks (MUI, Emotion,
styled-components) inject styles at runtime with high specificity. Without
`!important`, most design token overrides will lose to injected inline styles.
2. **The split border problem.** MUI (and many component libraries) separate the
border from the element it visually belongs to. Always inspect the actual DOM
before writing selectors — what looks like one element is often three.
3. **Portalled overlays.** Dropdowns, tooltips, and modals in MUI are rendered into
a separate DOM tree (`<body>` portal). Scoped selectors inside a card or form
won't reach them. Target their root classes (`.MuiMenu-paper`, `.MuiTooltip-tooltip`)
directly.
4. **State classes.** MUI uses `.Mui-focused`, `.Mui-checked`, `.Mui-error`,
`.Mui-active`, `.Mui-completed`, `.Mui-disabled` as state modifiers. These are the
equivalent of `:focus`, `:checked`, etc. but applied by JavaScript — pseudo-class
selectors alone will sometimes not fire.
5. **SVG-based components.** Checkboxes, radio buttons, step icons, and spinners are
SVGs injected by React. They cannot be styled with `background`, `border`, or
`clip-path`. Use `color` (SVG `currentColor` inheritance) and `filter:
drop-shadow()` instead.
+1067 -454
View File
File diff suppressed because it is too large Load Diff
+5148 -1082
View File
File diff suppressed because it is too large Load Diff
+1464 -138
View File
File diff suppressed because it is too large Load Diff
+2532 -381
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
<%
LOTUSGUILD TERMINAL DESIGN SYSTEM — Node.js / Express EJS Base Layout
Extend this in every page template via res.render('page', { ... }).
Required Express setup (server.js / app.js):
const { requireAuth, cspNonce, injectLocals } = require('./middleware');
app.use(cspNonce);
app.use(requireAuth);
app.use(injectLocals);
app.set('view engine', 'ejs');
Locals injected automatically by middleware.js:
user { username, name, email, groups, isAdmin }
nonce CSP nonce string
appName process.env.APP_NAME
appSubtitle process.env.APP_SUBTITLE
Locals to set per-route (or via a second res.locals middleware):
pageTitle string — page <title> suffix
activeNav string — must match a navLinks[].key
navLinks array — navigation items:
[{ href: '/path', key: 'mykey', label: 'My Page' }, ...]
Dropdown:
{ label: 'Admin', key: 'admin', adminOnly: true, children: [
{ href: '/admin/users', label: 'Users' }
]}
pageStyles array — optional extra CSS hrefs
pageScripts array — optional extra <script src> paths
%>
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#030508">
<meta name="robots" content="noindex, nofollow">
<title><%= pageTitle ? pageTitle + ' — ' : '' %><%= appName || 'LotusGuild' %></title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
<!-- Design system -->
<link rel="stylesheet" href="/web_template/base.css">
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
<link rel="stylesheet" href="/assets/app.css">
<% if (typeof pageStyles !== 'undefined') { pageStyles.forEach(href => { %>
<link rel="stylesheet" href="<%- href %>">
<% }); } %>
<link rel="icon" href="/assets/favicon.png" type="image/png">
</head>
<body>
<!-- Boot overlay -->
<div id="lt-boot" class="lt-boot-overlay"
data-app-name="<%= (appName || 'APP').toUpperCase() %>"
style="display:none">
<pre id="lt-boot-text" class="lt-boot-text"></pre>
</div>
<!-- =========================================================
HEADER
========================================================= -->
<header class="lt-header">
<div class="lt-header-left">
<div class="lt-brand">
<a href="/" class="lt-brand-title" style="text-decoration:none">
<%= (appName || 'APP').toUpperCase() %>
</a>
<span class="lt-brand-subtitle"><%= appSubtitle || 'LotusGuild Infrastructure' %></span>
</div>
<nav class="lt-nav" aria-label="Main navigation">
<% (navLinks || []).forEach(link => {
if (link.adminOnly && !user.isAdmin) return;
if (link.children) { %>
<div class="lt-nav-dropdown">
<a href="#" class="lt-nav-link <%= (activeNav || '').startsWith(link.key) ? 'active' : '' %>">
<%= link.label %> ▾
</a>
<ul class="lt-nav-dropdown-menu">
<% link.children.forEach(child => { %>
<li><a href="<%= child.href %>"><%= child.label %></a></li>
<% }); %>
</ul>
</div>
<% } else { %>
<a href="<%= link.href %>"
class="lt-nav-link <%= activeNav === link.key ? 'active' : '' %>">
<%= link.label %>
</a>
<% } }); %>
</nav>
</div>
<div class="lt-header-right">
<% if (user && (user.name || user.username)) { %>
<span class="lt-header-user"><%= user.name || user.username %></span>
<% } %>
<% if (user && user.isAdmin) { %>
<span class="lt-badge-admin">admin</span>
<% } %>
</div>
</header>
<!-- =========================================================
MAIN CONTENT — provided by the including view via <%- body %>
or by structuring routes to render with this as a wrapper.
========================================================= -->
<main class="lt-main lt-container">
<%- body %>
</main>
<!-- =========================================================
SCRIPTS — all tags carry the CSP nonce
========================================================= -->
<!-- Runtime globals -->
<script nonce="<%= nonce %>">
window.CSRF_TOKEN = <%= JSON.stringify(csrfToken || '') %>;
window.CURRENT_USER = {
username: <%= JSON.stringify(user.username || '') %>,
name: <%= JSON.stringify(user.name || '') %>,
groups: <%= JSON.stringify(user.groups || []) %>,
isAdmin: <%= user.isAdmin ? 'true' : 'false' %>,
};
// App-specific config: set window.APP_CONFIG in your route's inline script,
// not here. This file is shared across all apps.
</script>
<!-- Design system -->
<script nonce="<%= nonce %>" src="/web_template/base.js"></script>
<!-- App JS -->
<script nonce="<%= nonce %>" src="/assets/app.js"></script>
<% if (typeof pageScripts !== 'undefined') { pageScripts.forEach(src => { %>
<script nonce="<%= nonce %>" src="<%- src %>"></script>
<% }); } %>
</body>
</html>
+1140
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"devDependencies": {
"eslint": "^8.57.1"
}
}
+30 -15
View File
@@ -17,8 +17,14 @@
* $nonce string CSP nonce from SecurityHeadersMiddleware::getNonce()
* $currentUser array ['username', 'name', 'is_admin', 'groups']
* $pageTitle string Page <title> suffix
* $activeNav string Which nav link is active ('dashboard','tickets',etc.)
* $activeNav string Which nav key is active — must match a $navLinks entry
* $config array From config/config.php
* $navLinks array Navigation items:
* [['href' => '/path', 'key' => 'mykey', 'label' => 'My Page'], ...]
* Nested (dropdown):
* ['label' => 'Admin', 'key' => 'admin', 'adminOnly' => true, 'children' => [
* ['href' => '/admin/users', 'label' => 'Users'],
* ]]
*/
// Defensive defaults
@@ -27,6 +33,7 @@ $currentUser = $currentUser ?? [];
$pageTitle = $pageTitle ?? 'Dashboard';
$activeNav = $activeNav ?? '';
$config = $config ?? [];
$navLinks = $navLinks ?? [];
$isAdmin = $currentUser['is_admin'] ?? false;
?>
<!DOCTYPE html>
@@ -40,7 +47,7 @@ $isAdmin = $currentUser['is_admin'] ?? false;
<!-- Unified design system CSS -->
<link rel="stylesheet" href="/web_template/base.css">
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
<link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '20260314'; ?>">
<link rel="stylesheet" href="/assets/css/app.css?v=<?php echo $config['CSS_VERSION'] ?? '1'; ?>">
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
</head>
@@ -67,25 +74,31 @@ $isAdmin = $currentUser['is_admin'] ?? false;
</div>
<nav class="lt-nav" aria-label="Main navigation">
<a href="/" class="lt-nav-link <?php echo $activeNav === 'dashboard' ? 'active' : ''; ?>">Dashboard</a>
<a href="/tickets" class="lt-nav-link <?php echo $activeNav === 'tickets' ? 'active' : ''; ?>">Tickets</a>
<?php foreach ($navLinks as $link): ?>
<?php
$skipAdminOnly = !empty($link['adminOnly']) && !$isAdmin;
if ($skipAdminOnly) continue;
?>
<?php if ($isAdmin): ?>
<?php if (!empty($link['children'])): ?>
<?php $parentActive = str_starts_with($activeNav, $link['key']); ?>
<div class="lt-nav-dropdown">
<a href="#" class="lt-nav-link <?php echo str_starts_with($activeNav, 'admin') ? 'active' : ''; ?>">
Admin
<a href="#" class="lt-nav-link <?php echo $parentActive ? 'active' : ''; ?>">
<?php echo htmlspecialchars($link['label']); ?>
</a>
<ul class="lt-nav-dropdown-menu">
<li><a href="/admin/templates">Templates</a></li>
<li><a href="/admin/workflow">Workflow</a></li>
<li><a href="/admin/recurring-tickets">Recurring</a></li>
<li><a href="/admin/custom-fields">Custom Fields</a></li>
<li><a href="/admin/user-activity">User Activity</a></li>
<li><a href="/admin/audit-log">Audit Log</a></li>
<li><a href="/admin/api-keys">API Keys</a></li>
<?php foreach ($link['children'] as $child): ?>
<li><a href="<?php echo htmlspecialchars($child['href']); ?>"><?php echo htmlspecialchars($child['label']); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<a href="<?php echo htmlspecialchars($link['href']); ?>"
class="lt-nav-link <?php echo $activeNav === $link['key'] ? 'active' : ''; ?>">
<?php echo htmlspecialchars($link['label']); ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</nav>
</div>
@@ -130,6 +143,8 @@ $isAdmin = $currentUser['is_admin'] ?? false;
username: <?php echo json_encode($currentUser['username'] ?? ''); ?>,
isAdmin: <?php echo $isAdmin ? 'true' : 'false'; ?>,
};
// App-specific config: set window.APP_CONFIG in your app's own <script> block,
// not here. This file is shared across all apps.
</script>
<!-- Unified design system JS -->
@@ -137,7 +152,7 @@ $isAdmin = $currentUser['is_admin'] ?? false;
<!-- App-specific JS (cache-busted) -->
<script nonce="<?php echo $nonce; ?>"
src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '20260314'; ?>">
src="/assets/js/app.js?v=<?php echo $config['JS_VERSION'] ?? '1'; ?>">
</script>
<!-- Per-page inline JS goes here in the including view, e.g.: -->
+35 -21
View File
@@ -17,7 +17,8 @@
Required Flask setup (app.py):
- Pass `nonce` into every render_template() call via a context processor
- Pass `user` dict from _get_user() helper
- Pass `config` dict with APP_NAME, etc.
- Pass `config` dict with APP_NAME, APP_SUBTITLE, etc.
- Pass `nav_links` list of dicts defining navigation
Context processor example:
@app.context_processor
@@ -25,6 +26,16 @@
import base64, os
nonce = base64.b64encode(os.urandom(16)).decode()
return dict(nonce=nonce, user=_get_user(), config=_config())
nav_links format (pass from route or context processor):
nav_links = [
{'href': url_for('index'), 'key': 'dashboard', 'label': 'Dashboard'},
{'href': url_for('settings'), 'key': 'settings', 'label': 'Settings'},
# Admin-only dropdown:
{'label': 'Admin', 'key': 'admin', 'admin_only': True, 'children': [
{'href': url_for('admin_users'), 'label': 'Users'},
]},
]
#}
<!DOCTYPE html>
<html lang="en">
@@ -64,24 +75,28 @@
</div>
<nav class="lt-nav" aria-label="Main navigation">
{# Each page sets {% block active_nav %}pagename{% endblock %} #}
{% set active = self.active_nav() | default('') %}
<a href="{{ url_for('index') }}"
class="lt-nav-link {% if active == 'dashboard' %}active{% endif %}">
Dashboard
{% for link in nav_links | default([]) %}
{% if not link.get('admin_only') or 'admin' in user.groups %}
{% if link.get('children') %}
<div class="lt-nav-dropdown">
<a href="#" class="lt-nav-link {% if active.startswith(link.key) %}active{% endif %}">
{{ link.label }} ▾
</a>
<a href="{{ url_for('links_page') }}"
class="lt-nav-link {% if active == 'links' %}active{% endif %}">
Link Debug
</a>
<a href="{{ url_for('inspector') }}"
class="lt-nav-link {% if active == 'inspector' %}active{% endif %}">
Inspector
</a>
<a href="{{ url_for('suppressions_page') }}"
class="lt-nav-link {% if active == 'suppressions' %}active{% endif %}">
Suppressions
<ul class="lt-nav-dropdown-menu">
{% for child in link.children %}
<li><a href="{{ child.href }}">{{ child.label }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<a href="{{ link.href }}"
class="lt-nav-link {% if active == link.key %}active{% endif %}">
{{ link.label }}
</a>
{% endif %}
{% endif %}
{% endfor %}
</nav>
</div>
@@ -107,17 +122,16 @@
All <script> tags MUST carry the nonce attribute for CSP.
========================================================= -->
<!-- Runtime config (no CSRF needed for Gandalf — SameSite=Strict) -->
<!-- Runtime config injected by the server -->
<script nonce="{{ nonce }}">
window.APP_CONFIG = {
ticketWebUrl: {{ config.get('ticket_api', {}).get('web_url', 'https://t.lotusguild.org/ticket/') | tojson }},
};
window.CURRENT_USER = {
username: {{ user.username | tojson }},
name: {{ (user.name or user.username) | tojson }},
groups: {{ user.groups | tojson }},
isAdmin: {{ ('admin' in user.groups) | lower }},
};
// App-specific config: set window.APP_CONFIG in your app's own template block,
// not here. This file is shared across all apps.
</script>
<!-- Unified design system JS -->
@@ -136,6 +150,6 @@
{% block active_nav %}dashboard{% endblock %}
Values: dashboard | links | inspector | suppressions
Value must match a 'key' in your nav_links list.
--------------------------------------------------------------- #}
{% block active_nav %}{% endblock %}