chore: initialize @limitless/ui git repo + add AppShell

This brings the long-untracked @limitless/ui source tree under version
control. Until now /srv/k8s/templates/limitless-ui has been a plain
file: dependency consumed by gscChronos / gscCRM / gscAdmin, with
copies scattered across web/gsc{Portal,WWW,Aether,Register}/ and
apps/gsc{Meet,Share}/. None were git-tracked.

Treating /srv/k8s/templates/limitless-ui as the canonical going
forward; secondary copies should be replaced with this version
in their consumers' Dockerfiles when they next get touched.

Changes in this initial commit beyond the snapshot:
- Add src/layout/AppShell.tsx — runtime-loaded chrome (header,
  sidebar, footer) backed by gsc-shell-api. Public surface:
    AppShell, ShellProvider, useShell, ShellConfig types
  Framework-agnostic (no Next.js dep). Apps pass appKey + apiUrl +
  getToken; AppShell composes the existing PageShell / Navbar /
  Sidebar / Footer primitives with API data.
- Re-export AppShell from src/index.ts.
- Fix build script: `tsc -p tsconfig.json --noEmit false`. The bare
  `tsc` command was a no-op because tsconfig.json sets noEmit:true
  for typecheck speed. Existing dist/ only existed because of an
  earlier emit; clean rebuilds were silently broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-10 09:42:57 +02:00
commit cf068ce4ec
115 changed files with 36542 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
import React, { useEffect, useRef, useCallback } from 'react';
export interface OffcanvasProps {
/** Whether the offcanvas is open */
isOpen: boolean;
/** Callback when offcanvas should close */
onClose: () => void;
/** Placement of the offcanvas */
placement?: 'start' | 'end' | 'top' | 'bottom';
/** Title for the header */
title?: React.ReactNode;
/** Show backdrop */
backdrop?: boolean | 'static';
/** Allow scrolling body when open */
scroll?: boolean;
/** Enable keyboard (Escape) to close */
keyboard?: boolean;
/** Additional CSS classes */
className?: string;
/** Header CSS classes */
headerClassName?: string;
/** Body CSS classes */
bodyClassName?: string;
/** Children content */
children?: React.ReactNode;
}
export const Offcanvas: React.FC<OffcanvasProps> = ({
isOpen,
onClose,
placement = 'start',
title,
backdrop = true,
scroll = false,
keyboard = true,
className = '',
headerClassName = '',
bodyClassName = '',
children,
}) => {
const offcanvasRef = useRef<HTMLDivElement>(null);
// Handle escape key
useEffect(() => {
if (!keyboard || !isOpen) return;
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeydown);
return () => document.removeEventListener('keydown', handleKeydown);
}, [keyboard, isOpen, onClose]);
// Handle body scroll
useEffect(() => {
if (isOpen && !scroll) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen, scroll]);
// Handle backdrop click
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
if (backdrop === 'static') return;
if (e.target === e.currentTarget) {
onClose();
}
}, [backdrop, onClose]);
// Focus trap
useEffect(() => {
if (!isOpen) return;
const offcanvas = offcanvasRef.current;
if (!offcanvas) return;
const focusableElements = offcanvas.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement?.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement?.focus();
e.preventDefault();
}
}
};
firstElement?.focus();
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}, [isOpen]);
const classes = [
'll-offcanvas',
`ll-offcanvas-${placement}`,
isOpen && 'll-offcanvas-show',
className,
].filter(Boolean).join(' ');
if (!isOpen && !backdrop) return null;
return (
<>
{/* Backdrop */}
{backdrop && (
<div
className={`ll-offcanvas-backdrop ${isOpen ? 'll-offcanvas-backdrop-show' : ''}`}
onClick={handleBackdropClick}
/>
)}
{/* Offcanvas */}
<div
ref={offcanvasRef}
className={classes}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby={title ? 'll-offcanvas-title' : undefined}
>
{/* Header */}
{title && (
<div className={`ll-offcanvas-header ${headerClassName}`}>
<h5 className="ll-offcanvas-title" id="ll-offcanvas-title">
{title}
</h5>
<button
type="button"
className="ll-offcanvas-close"
onClick={onClose}
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
)}
{/* Body */}
<div className={`ll-offcanvas-body ${bodyClassName}`}>
{children}
</div>
</div>
</>
);
};
export interface OffcanvasHeaderProps {
className?: string;
children?: React.ReactNode;
}
export const OffcanvasHeader: React.FC<OffcanvasHeaderProps> = ({
className = '',
children,
}) => (
<div className={`ll-offcanvas-header ${className}`}>
{children}
</div>
);
export interface OffcanvasTitleProps {
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
className?: string;
children?: React.ReactNode;
}
export const OffcanvasTitle: React.FC<OffcanvasTitleProps> = ({
as: Component = 'h5',
className = '',
children,
}) => (
<Component className={`ll-offcanvas-title ${className}`}>
{children}
</Component>
);
export interface OffcanvasBodyProps {
className?: string;
children?: React.ReactNode;
}
export const OffcanvasBody: React.FC<OffcanvasBodyProps> = ({
className = '',
children,
}) => (
<div className={`ll-offcanvas-body ${className}`}>
{children}
</div>
);