Imported from janbkrejci/TSWebUI-shadcn (
LLM/skills/tswebui/SKILL.md). Install upstream withnpx skills add janbkrejci/TSWebUI-shadcn --skill tswebui. Copyright stays with the author.
TSWebUI-shadcn Component Library
A comprehensive UI component library built on Next.js 16 + React 19 + Shadcn/UI + Tailwind CSS v4. All components are client components ("use client").
Table of Contents
- Prerequisites & Installation
- Localization
- ThemeProvider
- ModeToggle
- TsLayout
- TopBar
- Sidebar
- TsWindow
- TsForm
- TsTable
- TsFormEditor
Prerequisites & Installation
Required Stack
The consuming project must have:
- Next.js 16+ (App Router with
"use client"support) - React 19+
- Tailwind CSS v4 (CSS-first config)
- Shadcn/UI initialized — if not yet set up, run:
This createsnpx shadcn@latest initcomponents.json,src/lib/utils.ts(with thecn()helper usingclsx+tailwind-merge), and the@/path alias intsconfig.json. All TSWebUI components depend on these.
How to Install a Component
All TSWebUI components are published in an online Shadcn registry. Install any component with a single command:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/<component>.json
This command:
- Downloads the component source files into
src/components/ts-web-ui/<component>/ - Installs all required npm dependencies (e.g.
react-hook-form,@tanstack/react-table) - Installs all required Shadcn UI primitives (e.g.
input,checkbox,select) - Installs all required other TSWebUI components (transitive dependencies)
No manual npm installs or file copying needed — one command does everything.
Available Components
| Component | Install Command |
|---|---|
| ThemeProvider | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/theme-provider.json |
| ModeToggle | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/mode-toggle.json |
| LocaleToggle | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/locale-toggle.json |
| Locale (system) | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/locale.json |
| TsLogo | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-logo.json |
| TsTopbar | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-topbar.json |
| TsSidebar | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-sidebar.json |
| TsLayout (integrated) | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/integrated-layout.json |
| TsWindow | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-window.json |
| TsTable | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-table.json |
| TsForm | npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-form.json |
Dependency Graph
You don't need to install transitive dependencies manually. For reference, here's what each component pulls in:
- LocaleToggle →
locale,button+ Shadcndropdown-menu+ npm:lucide-react - TsForm →
locale,ts-table,button,alert-dialog+ 15 Shadcn primitives (form,input,select,checkbox,radio-group,switch,slider,popover,calendar,command,dialog,separator,tabs,textarea,toggle-group) + npm:react-hook-form,lucide-react,date-fns,react-markdown,remark-gfm,react-syntax-highlighter - TsTable →
locale,button+ Shadcncheckbox,dropdown-menu,input,select,table,badge+ npm:@tanstack/react-table,lucide-react,xlsx,date-fns - TsLayout (integrated) →
ts-sidebar,ts-topbar,theme-provider(and their transitive deps) - TsSidebar →
locale,button,ts-logo+ Shadcntooltip - TsTopbar →
ts-logo - TsWindow →
locale,button+ npm:react-rnd,lucide-react - ModeToggle →
button+ Shadcndropdown-menu+ npm:next-themes,lucide-react - ThemeProvider → npm:
next-themes - TsLogo → npm:
lucide-react
Where Files Are Installed
After running the install command, files appear at:
src/
├── components/
│ ├── ui/ # Shadcn primitives (input, checkbox, etc.)
│ └── ts-web-ui/ # TSWebUI components
│ ├── ui/ # TSWebUI overrides (button, alert-dialog)
│ ├── ts-form/ # TsForm + all widgets
│ │ ├── index.tsx
│ │ ├── types.ts
│ │ ├── widget-types.ts
│ │ ├── utils.ts
│ │ ├── ts-form-field.tsx
│ │ ├── ts-form-layout.tsx
│ │ ├── ts-form-confirmation-dialog.tsx
│ │ └── widgets/ # 20+ field type widgets
│ ├── ts-table/ # TsTable + sub-components
│ ├── ts-window/ # TsWindow + WindowProvider
│ ├── locale/ # Localization (TsLocaleProvider, en, cs)
│ ├── locale-toggle/ # LocaleToggle (language switcher for TopBar)
│ ├── ts-layout/ # TsLayout (integrated shell)
│ ├── ts-sidebar/ # Sidebar system
│ ├── ts-topbar/ # TopBar
│ ├── ts-logo/ # Logo component
│ ├── theme-provider/ # ThemeProvider
│ └── mode-toggle/ # ModeToggle
└── lib/
└── utils.ts # cn() utility (created by shadcn init)
Import Convention
All imports use the @/ path alias (configured in tsconfig.json by shadcn init):
import { TsLocaleProvider, cs, en, useTsLocale, useTsLocaleSetter } from "@/components/ts-web-ui/locale"
import { LocaleToggle } from "@/components/ts-web-ui/locale-toggle"
import { ModeToggle } from "@/components/ts-web-ui/mode-toggle"
import { ThemeProvider } from "@/components/ts-web-ui/theme-provider"
import { TsForm } from "@/components/ts-web-ui/ts-form"
import { TsLayout } from "@/components/ts-web-ui/ts-layout"
import { Logo } from "@/components/ts-web-ui/ts-logo"
import { TsTable } from "@/components/ts-web-ui/ts-table"
import { TopBar, TopBarGroup } from "@/components/ts-web-ui/ts-topbar"
import {
TsWindow,
WindowOutlet,
WindowProvider,
useWindowManager,
} from "@/components/ts-web-ui/ts-window"
Quick Start Example
To add a complete application shell with forms and theme switching to a fresh Next.js project:
# 1. Initialize shadcn (if not done yet)
npx shadcn@latest init
# 2. Install the integrated layout (includes sidebar, topbar, theme provider)
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/integrated-layout.json
# 3. Install the form system (includes table and all field widgets)
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-form.json
# 4. Install the theme toggle button
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/mode-toggle.json
# 5. Install the window system (if needed)
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-window.json
Localization
Location: src/components/ts-web-ui/locale/
All static UI texts in every TSWebUI component are localizable. The library ships with English (default) and Czech presets and supports fully custom locale objects.
Architecture
| Export | Source | Description |
|---|---|---|
TsLocaleProvider |
locale/context.tsx |
React context provider — wrap app or subtree |
useTsLocale() |
locale/context.tsx |
Hook returning the current TsLocale (accepts optional override) |
useTsLocaleSetter() |
locale/context.tsx |
Hook returning { localeName, setLocaleName } — use in locale switcher components |
en |
locale/en.ts |
English locale preset (default) |
cs |
locale/cs.ts |
Czech locale preset |
TsLocale |
locale/types.ts |
Full locale type (strings + formatting) |
TsLocaleStrings |
locale/types.ts |
All translatable string keys (table, form, window, sidebar, formEditor) |
TsLocaleFormatting |
locale/types.ts |
locale (BCP 47 tag, e.g. "en-US") and optional timezone (IANA, e.g. "Europe/Prague") |
Import
import { TsLocaleProvider, cs, en, useTsLocale, useTsLocaleSetter } from "@/components/ts-web-ui/locale"
import type { TsLocale, TsLocaleFormatting, TsLocaleStrings } from "@/components/ts-web-ui/locale"
Usage — Context Provider
Wrap your app (or a subtree) with TsLocaleProvider. All TSWebUI components inside the provider automatically pick up the locale:
import { TsLocaleProvider } from "@/components/ts-web-ui/locale"
// Preset by name
<TsLocaleProvider locale="cs">
{children}
</TsLocaleProvider>
// Full custom object
<TsLocaleProvider locale={myCustomLocale}>
{children}
</TsLocaleProvider>
If no provider is present, English (en) is used.
TsLocaleProvider holds mutable state internally — the active locale can be changed at runtime by any child component using useTsLocaleSetter().
Usage — Locale Setter Hook
Use useTsLocaleSetter() in custom locale-switcher components. It returns { localeName, setLocaleName } where localeName is the active preset name string and setLocaleName changes the locale globally:
import { useTsLocaleSetter } from "@/components/ts-web-ui/locale"
function MyLocaleSwitcher() {
const { localeName, setLocaleName } = useTsLocaleSetter()
return (
<button onClick={() => setLocaleName(localeName === "en" ? "cs" : "en")}>
{localeName === "en" ? "Switch to Czech" : "Switch to English"}
</button>
)
}
LocaleToggle — Ready-Made Language Switcher
LocaleToggle is a pre-built dropdown for the TopBar. It uses useTsLocaleSetter() internally and shows SVG-flag buttons. Czech (🇨🇿 Česky) is listed first and is the default locale in the demo app. Install it with:
import { LocaleToggle } from "@/components/ts-web-ui/locale-toggle"
import { TopBar, TopBarGroup } from "@/components/ts-web-ui/ts-topbar"
import { ModeToggle } from "@/components/ts-web-ui/mode-toggle"
<TopBar
rightContent={
<TopBarGroup>
<LocaleToggle />
<ModeToggle />
</TopBarGroup>
}
/>
Usage — Component-Level Override
TsTable and TsForm accept a locale prop that overrides the context for that component and its children:
import { cs } from "@/components/ts-web-ui/locale"
<TsTable data={data} columnDefinitions={cols} locale={cs} />
<TsForm layout={layout} fields={fields} locale="cs" />
The locale prop accepts either a preset name string ("en", "cs") or a full TsLocale object.
Usage — Hook
useTsLocale(override?) returns the resolved TsLocale. It reads from context by default but accepts an optional override (string or object):
const locale = useTsLocale() // from context
const locale = useTsLocale("cs") // force Czech
const { strings, formatting } = locale
Creating a Custom Locale
Spread an existing preset and override only what you need:
import { en } from "@/components/ts-web-ui/locale"
import type { TsLocale } from "@/components/ts-web-ui/locale"
const myLocale: TsLocale = {
strings: {
...en.strings,
table: { ...en.strings.table, search: "Find...", noRecords: "Nothing here" },
form: { ...en.strings.form, required: "Mandatory" },
},
formatting: { locale: "en-GB", timezone: "Europe/London" },
}
String Categories
TsLocaleStrings is organized into five groups:
| Group | Keys (selected) | Used By |
|---|---|---|
table |
search, columns, viewColumns, searchColumns, clearAllFilters, export, exportAll(count), exportFiltered(count), exportSelected(count), import, newRecord, noRecords, rowsPerPage, pageOf, rowsSelected, selectAll, copyToClipboard, moveLeft, moveRight, first/previous/next/last, importResults, etc. | TsTable (toolbar, view, pagination, columns) |
form |
required, showPassword, hidePassword, selectPlaceholder, searchPlaceholder, notFound, customValueAdd, clear, today, addFile(s), selectEntity, chooseFromList, etc. | TsForm (all field widgets) |
window |
centerOnScreen, fitToContent | TsWindow (titlebar buttons) |
sidebar |
closeMenu, openMenu, expandMenu, collapseMenu | TsSidebar (aria-labels, tooltips) |
nav |
logoText, sectionOverview, sectionComponents, sectionUtilities, sectionFormWidgets, overview, window, table, form, topbar, sidebar, integratedLayout, formEditor, themeProvider, modeToggle | App layout (sidebar nav labels, logo text) |
formEditor |
~120 keys covering toolbar (undo/redo/reset/import/export/preview), canvas (addRow, dragFieldHere, field palette group labels, field palette type labels), drag overlay, properties panel (all field properties, button properties, every section heading, all variant options, all validation error messages). Also contains nested objects fieldTypeLabels (21 entries, one per field type) and fieldGroupLabels (6 entries for palette groups). |
TsFormEditor |
Some keys are functions for interpolation: pageOf(page, total), rowsSelected(selected, total), selected(count), fieldNotFound(field), customValueAdd(value), selectEntity(entity), etc.
ThemeProvider
Location: src/components/ts-web-ui/theme-provider/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/theme-provider.json
Auto-installed dependencies: next-themes
Wraps next-themes to eliminate hydration mismatch errors. Renders a placeholder <div> until the client is mounted.
Usage
Place in your root layout.tsx:
import { ThemeProvider } from "@/components/ts-web-ui/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
Behavior
attribute="class"— adds.dark/.lightclass to<html>defaultTheme="system"— respects OS preferenceenableSystem— allows system theme detectiondisableTransitionOnChange— prevents flash on theme switch- Until mounted, renders
<div className="min-h-screen bg-background" />to avoid hydration errors
Theme CSS Variables
Define in globals.css:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
/* ... more semantic colors ... */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* ... dark variants ... */
}
Use in components: bg-background, text-foreground, bg-primary, text-muted-foreground, etc.
ModeToggle
Location: src/components/ts-web-ui/mode-toggle/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/mode-toggle.json
Auto-installed dependencies: next-themes, lucide-react, TSWebUI button, Shadcn dropdown-menu
A dropdown button that switches between Light / Dark / System themes.
Usage
import { ModeToggle } from "@/components/ts-web-ui/mode-toggle"
;<ModeToggle />
Props
None. This component is self-contained. It uses useTheme() from next-themes internally.
Features
- Sun icon in light mode, Moon icon in dark mode (animated rotation/scale)
- Dropdown with three options: Light, Dark, System
- Checkmark indicator on the currently active theme
TsLayout
Location: src/components/ts-web-ui/ts-layout/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/integrated-layout.json
Auto-installed dependencies: lucide-react, ts-sidebar, ts-topbar, theme-provider (and their transitive dependencies: ts-logo, button, Shadcn tooltip)
An integrated application shell that combines TopBar + Sidebar + main content area into a single component.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
— | Main content area |
navigation |
NavSection[] | NavItem[] |
undefined |
Sidebar navigation data |
logo |
ReactNode |
undefined |
Logo element for sidebar and topbar |
topBarLeft |
ReactNode |
undefined |
Content for the left side of the topbar |
topBarCenter |
ReactNode |
undefined |
Content for the center of the topbar |
topBarRight |
ReactNode |
undefined |
Content for the right side of the topbar |
contained |
boolean |
false |
If true, uses absolute positioning to fit inside a bounded container instead of full viewport |
Navigation Data Types
interface NavItem {
name: string // Unique identifier
href: string // URL path
label: string // Display text
icon: LucideIcon | ReactNode // Icon component or element
exact?: boolean // If true, match href exactly (not prefix)
}
interface NavSection {
title: string // Section heading
items: NavItem[] // Items in this section
}
Full Application Layout Example
"use client"
import { Home, Settings, Users } from "lucide-react"
import { TsLocaleProvider } from "@/components/ts-web-ui/locale"
import { ModeToggle } from "@/components/ts-web-ui/mode-toggle"
import { ThemeProvider } from "@/components/ts-web-ui/theme-provider"
import { TsLayout } from "@/components/ts-web-ui/ts-layout"
import { Logo } from "@/components/ts-web-ui/ts-logo"
import { TopBarGroup } from "@/components/ts-web-ui/ts-topbar"
const NAVIGATION = [
{
title: "Application",
items: [
{ name: "dashboard", label: "Dashboard", href: "/", icon: Home, exact: true },
{ name: "users", label: "Users", href: "/users", icon: Users },
{ name: "settings", label: "Settings", href: "/settings", icon: Settings },
],
},
]
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>
<TsLocaleProvider locale="en">
<TsLayout
navigation={NAVIGATION}
logo={<Logo text="My App" href="/" />}
topBarRight={
<TopBarGroup>
<ModeToggle />
</TopBarGroup>
}
>
{children}
</TsLayout>
</TsLocaleProvider>
</ThemeProvider>
</body>
</html>
)
}
TopBar
Location: src/components/ts-web-ui/ts-topbar/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-topbar.json
Auto-installed dependencies: lucide-react, ts-logo
Tip: If you plan to use the full layout, install
integrated-layoutinstead — it includes TopBar, Sidebar, and ThemeProvider.
A sticky top bar with three content slots (left, center, right). Auto-detects SidebarProvider and shows a hamburger trigger.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
leftContent |
ReactNode |
undefined |
Content on the left side |
centerContent |
ReactNode |
undefined |
Content in the center (flex-1) |
rightContent |
ReactNode |
undefined |
Content on the right side |
height |
number |
56 |
Height in pixels |
bordered |
boolean |
true |
Show bottom border |
showTrigger |
boolean |
true |
Auto-show sidebar trigger if inside SidebarProvider |
className |
string |
undefined |
Additional CSS classes |
Exported Sub-components
| Component | Description |
|---|---|
TopBarGroup |
Flexbox wrapper (flex items-center gap-2) for grouping topbar items |
Logo |
Re-exported from ts-logo — renders text/icon with optional link |
TopBarProvider (Optional)
Provides topbar height to child components via context:
import { TopBarProvider, useTopBar } from "@/components/ts-web-ui/ts-topbar"
;<TopBarProvider height={56}>
{/* children can call useTopBar() to get { height } */}
</TopBarProvider>
Standalone Usage
import { ModeToggle } from "@/components/ts-web-ui/mode-toggle"
import { Logo } from "@/components/ts-web-ui/ts-logo"
import { TopBar, TopBarGroup } from "@/components/ts-web-ui/ts-topbar"
;<TopBar
leftContent={<Logo text="My App" href="/" />}
centerContent={<Input placeholder="Search..." />}
rightContent={
<TopBarGroup>
<ModeToggle />
</TopBarGroup>
}
height={56}
bordered
/>
Sidebar
Location: src/components/ts-web-ui/ts-sidebar/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-sidebar.json
Auto-installed dependencies: lucide-react, TSWebUI button, ts-logo, Shadcn tooltip
Tip: If you plan to use the full layout, install
integrated-layoutinstead — it includes TopBar, Sidebar, and ThemeProvider.
A fully-featured collapsible sidebar with data-driven navigation, mobile responsiveness, and localStorage persistence. All accessibility labels (close/open menu, expand/collapse) are automatically localized via useTsLocale() context.
System Components
| Component | Description |
|---|---|
SidebarProvider |
Context provider — wraps the entire layout |
Sidebar |
The sidebar element itself |
SidebarContent |
Scrollable content area |
SidebarHeader |
Header with logo and close button |
SidebarSection |
Grouped navigation section with title |
SidebarItem |
Single navigation item with icon |
SidebarFooter |
Bottom section with border |
SidebarInset |
Main content area that adjusts margins based on sidebar state |
SidebarTrigger |
Hamburger toggle button |
SidebarCollapseTrigger |
Circular collapse/expand button on sidebar edge |
SidebarProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen |
boolean |
true |
Initial open state |
mobileBreakpoint |
number |
768 |
Pixel width below which mobile behavior activates |
topBarHeight |
number |
56 |
Height of the topbar in pixels (for offset calculations) |
width |
string |
"16rem" |
Sidebar width when expanded |
collapsedWidth |
string |
"4rem" |
Sidebar width when collapsed (icons only) |
Sidebar Props
| Prop | Type | Default | Description |
|---|---|---|---|
navigation |
NavSection[] | NavItem[] |
undefined |
Data-driven navigation (auto-renders items) |
logo |
ReactNode |
undefined |
Logo element for the sidebar header |
className |
string |
undefined |
Additional CSS classes |
useSidebar() Hook
Returns the sidebar context:
const {
isOpen, // boolean — sidebar visibility
toggle, // () => void — toggle open/closed
open, // () => void — force open
close, // () => void — force close
isCollapsed, // boolean — collapsed to icon-only mode
toggleCollapsed, // () => void — toggle collapsed state
isMobile, // boolean — mobile viewport detected
topBarHeight, // number — topbar height in px
width, // string — expanded width
collapsedWidth, // string — collapsed width
} = useSidebar()
Features
- Responsive: Auto-closes on mobile, overlay mode with backdrop
- Collapsible: Icon-only mode with tooltips on desktop
- Persistent: Saves open/collapsed state to localStorage
- Active detection: Highlights current route using
usePathname() - Data-driven: Pass
navigationprop for auto-rendered nav items with Lucide icons
TsWindow
Location: src/components/ts-web-ui/ts-window/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-window.json
Auto-installed dependencies: react-rnd, lucide-react, TSWebUI button
A draggable, resizable window system with minimize/maximize/restore, Z-index management, and an imperative API. Titlebar button tooltips ("Center on Screen", "Fit to Content") are automatically localized via useTsLocale() context.
Architecture
Three components work together:
WindowProvider— React context providing the window managerWindowOutlet— Renders all open windows (place inside a relative-positioned container)useWindowManager()— Hook to interact with the window system
TsWindowProps
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | number |
— | Unique window identifier |
title |
string |
"Window" |
Title displayed in the header bar |
defaultWidth |
number |
400 |
Initial width in pixels |
defaultHeight |
number |
300 |
Initial height in pixels |
defaultTop |
number |
100 |
Initial Y position in pixels |
defaultLeft |
number |
100 |
Initial X position in pixels |
minWidth |
number |
200 |
Minimum allowed width |
minHeight |
number |
100 |
Minimum allowed height |
children |
ReactNode |
— | Window content |
useWindowManager() Hook
| Method/Property | Signature | Description |
|---|---|---|
openWindow |
(content: ReactNode, options?: Partial<TsWindowProps> & { id?: string }) => void |
Opens a new window. If id already exists, brings it to front. |
closeWindow |
(id: string) => void |
Closes the window with the given ID |
getWindow |
(id: string) => TsWindowRef | null |
Returns the imperative handle for a window |
windows |
WindowItem[] |
Array of currently open window objects |
isInteracting |
boolean |
Whether any window is being dragged/resized |
setInteracting |
(interacting: boolean) => void |
Set the interaction state |
TsWindowRef (Imperative API)
Retrieved via getWindow(id) or React ref:
| Method | Description |
|---|---|
minimize() |
Minimizes the window to a small title-only bar |
maximize() |
Maximizes the window to fill its parent container |
restore() |
Restores from minimized or maximized state |
close() |
Closes the window (removes from DOM) |
centerOnScreen() |
Centers the window within its parent container |
fitToContent() |
Adjusts height to match content scroll height |
bringToFront() |
Increments Z-index to place above other windows |
Window Features
- macOS-style traffic lights: Red (close), Yellow (minimize), Green (maximize/restore)
- Double-click titlebar: Toggles maximize/restore
- Center button: Target icon in titlebar centers window
- Fit-to-content button: Adjusts height to content
- Drag containment: Window header always stays within parent bounds
- Resize containment: Window cannot be resized outside parent
- Z-index management: Global counter ensures focused window is always on top
- Auto-fit on mount: Window adjusts height to content on initial render
- ResizeObserver: Adapts when parent container resizes
Complete Usage Example
"use client"
import { Button } from "@/components/ui/button"
import { WindowOutlet, WindowProvider, useWindowManager } from "@/components/ts-web-ui/ts-window"
function WindowContent({ id }: { id: string }) {
const { getWindow } = useWindowManager()
return (
<div className="space-y-4">
<p>
Content for window <strong>{id}</strong>
</p>
<div className="flex gap-2">
<Button size="sm" onClick={() => getWindow(id)?.centerOnScreen()}>
Center
</Button>
<Button size="sm" variant="outline" onClick={() => getWindow(id)?.minimize()}>
Minimize
</Button>
<Button size="sm" variant="destructive" onClick={() => getWindow(id)?.close()}>
Close
</Button>
</div>
</div>
)
}
function MyApp() {
const { openWindow } = useWindowManager()
const handleOpen = () => {
const id = `win-${Math.random().toString(36).substring(7)}`
openWindow(<WindowContent id={id} />, {
id,
title: "My Window",
defaultWidth: 400,
defaultHeight: 300,
defaultLeft: 150,
defaultTop: 100,
})
}
return (
<div className="h-screen flex flex-col">
<div className="p-4">
<Button onClick={handleOpen}>Open Window</Button>
</div>
<div className="flex-1 relative overflow-hidden">
<WindowOutlet />
</div>
</div>
)
}
export default function Page() {
return (
<WindowProvider>
<MyApp />
</WindowProvider>
)
}
WindowOutlet Props
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string |
undefined |
Additional CSS classes (default includes absolute inset-0 pointer-events-none z-50) |
TsForm
Location: src/components/ts-web-ui/ts-form/index.tsx
Install:
npx shadcn@latest add https://janbkrejci.github.io/TSWebUI-shadcn/registry/ts-form.json
Auto-installed dependencies:
- npm:
react-hook-form,lucide-react,date-fns,react-markdown,remark-gfm,react-syntax-highlighter - TSWebUI:
button,alert-dialog,ts-table - Shadcn:
form,alert,badge,calendar,checkbox,command,dialog,input,popover,radio-group,select,separator,slider,switch,tabs,textarea,toggle-group
A fully JSON-driven form engine that generates complete forms from data definitions — including layout, validation, field types, buttons, and confirmation dialogs.
TsFormProps
| Prop | Type | Default | Description |
|---|---|---|---|
layout |
TsLayout |
— | Required. Layout structure defining rows and/or tabs |
fields |
Record<string, TsFieldDef> |
— | Required. Dictionary of field definitions keyed by field name |
values |
Record<string, unknown> |
{} |
Initial/current form values |
buttons |
TsButton[] |
[] |
Action buttons rendered at the bottom of the form |
errors |
TsErrors |
undefined |
External validation errors (from server/parent) |
activeTab |
string | number |
undefined |
Controlled active tab (label string or 0-based index) |
onTabChange |
(tab: string | number) => void |
undefined |
Callback when user switches tabs |
onAction |
(action: string, data: Record<string, unknown>) => void |
undefined |
Primary callback. Fires for all button actions (submit, delete, custom, etc.) |
onFieldChange |
(name: string, value: unknown, data: Record<string, unknown>) => void |
undefined |
Fires when a field value changes. Emits on every change for all editable field types (text, number, textarea, password, date, select, checkbox, switch, etc.). Display-only types (markdown, infobox, empty, separator) never emit. De-duplication guard prevents redundant emissions when the value did not actually change. |
readOnly |
boolean |
false |
Sets all fields to read-only and hides the button bar |
className |
string |
undefined |
Additional CSS classes for the form element |
locale |
string | TsLocale |
undefined |
UI locale override — preset name ("en", "cs") or full TsLocale object for all static texts. Also used for date/number formatting when TsLocale.formatting.locale is set |
Layout System
TsLayout
interface TsLayout {
tabs?: TsTab[] // Multi-tab form (takes priority over rows)
rows?: TsRow[] // Single-page form
}
TsTab
interface TsTab {
label: string // Tab display name
rows: TsRow[] // Rows within this tab
}
TsRow
An array of TsRowItem:
type TsRow = TsRowItem[]
interface TsRowItem {
field: string // Key from the fields dictionary
width?: string // CSS grid width: "1fr", "200px", "50%", etc. (default: "1fr")
type?: "empty" | "separator" // Special layout types (overrides field lookup)
label?: string // Label for separator type
align?: "left" | "center" | "right" // Horizontal alignment within grid cell
}
Layout Example
const layout: TsLayout = {
tabs: [
{
label: "General",
rows: [
[
{ field: "firstName", width: "1fr" },
{ field: "lastName", width: "1fr" },
],
[
{ field: "email", width: "2fr" },
{ field: "age", width: "100px" },
],
[{ type: "separator", label: "Additional Info", field: "sep1" }],
[{ field: "bio" }],
],
},
{
label: "Settings",
rows: [[{ field: "role" }, { field: "active" }]],
},
],
}
Field Types Reference
All field types share these base properties:
| Property | Type | Default | Description |
|---|---|---|---|
type |
TsFieldType |
— | Required. Discriminator for the field type |
label |
string |
undefined |
Label text above the field |
required |
boolean |
false |
Visual indicator and validation check |
hidden |
boolean |
false |
Hidden from UI but present in data |
hideLabel |
boolean |
false |
Hides label but preserves layout slot |
disabled |
boolean |
false |
Disables user interaction |
readonly |
boolean |
false |
Read-only visual state |
hint |
string |
undefined |
Help text below the field |
error |
string |
undefined |
Static error message (prefer errors prop for dynamic validation) |
excludeFromSubmit |
boolean |
false |
Exclude value from submitted data |
autofocus |
boolean |
false |
Auto-focus on mount or tab change |
enterAction |
string |
undefined |
Action on Enter key: "submit", "focus:next", or custom action |
escapeAction |
string |
undefined |
Action on Escape key: "clear" clears value, other strings emit action |
Text / Password
{ type: "text", label: "Name", placeholder: "John", selectAllOnFocus: true }
{ type: "password", label: "Password", placeholder: "••••••" }
| Property | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder text |
selectAllOnFocus |
boolean |
Select all text on focus |
Textarea
{ type: "textarea", label: "Bio", rows: 4, placeholder: "Tell us about yourself" }
| Property | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
— | Placeholder text |
rows |
number |
3 |
Number of visible text lines |
selectAllOnFocus |
boolean |
— | Select all text on focus |
Number
{ type: "number", label: "Age", min: 0, max: 150, step: 1, roundTo: 0, locale: "en-US" }
| Property | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder text |
min |
number |
Minimum value |
max |
number |
Maximum value |
step |
number |
Step increment |
roundTo |
number |
Decimal places for rounding/display |
locale |
string |
Locale for number formatting (e.g. "cs-CZ") |
selectAllOnFocus |
boolean |
Select all text on focus |
Slider
{ type: "slider", label: "Volume", min: 0, max: 100, step: 1 }
| Property | Type | Description |
|---|---|---|
min |
number |
Minimum value |
max |
number |
Maximum value |
step |
number |
Step increment |
Select
{
type: "select",
label: "Role",
placeholder: "Choose a role",
options: [
{ value: "admin", label: "Administrator" },
{ value: "user", label: "User" },
]
}
| Property | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder when no option selected |
options |
TsFieldOptions[] | string[] |
Available options |
Multiselect
{
type: "multiselect",
label: "Skills",
placeholder: "Select skills...",
options: ["JavaScript", "TypeScript", "React"],
allowCustom: true,
notFoundMessage: "No skills match."
}
| Property | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder text |
options |
TsFieldOptions[] | string[] |
Available options |
allowCustom |
boolean |
Allow adding custom values directly from search |
notFoundMessage |
string |
Message when search has no results (or custom add prompt) |
Combobox
{
type: "combobox",
label: "Country",
options: [{ value: "us", label: "United States" }],
allowCustom: true,
clearable: true,
selectAllOnFocus: true
}
| Property | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder text |
options |
TsFieldOptions[] | string[] |
Available options |
allowCustom |
boolean |
Allow custom values not in options |
clearable |
boolean |
Show a clear button |
selectAllOnFocus |
boolean |
Select all text on focus |
notFoundMessage |
string |
Message when no options match |
Radio
{
type: "radio",
label: "Gender",
options: [
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
]
}
| Property | Type | Description |
|---|---|---|
options |
TsFieldOptions[] | string[] |
Available options |
Checkbox
{ type: "checkbox", label: "I agree to the terms" }
Boolean value. No additional properties beyond base.
Switch
{ type: "switch", label: "Active Account" }
Boolean toggle. No additional properties beyond base.
Button Group
{
type: "button-group",
label: "Status",
options: [
{ value: "draft", label: "Draft", variant: "outline" },
{ value: "published", label: "Published", variant: "default" },
],
variant: "process" // Optional: renders as chevron process stepper
}
| Property | Type | Description |
|---|---|---|
options |
TsFieldOptions[] | string[] |
Available options (each can have variant) |
variant |
"process" |
Optional: renders as a horizontal process stepper with chevrons |
Date
{
type: "date",
label: "Birth Date",
dateFormat: "d.M.yyyy",
locale: "cs-CZ",
showTodayButton: true,
showClearButton: true,
todayButtonText: "Today",
clearButtonText: "Clear"
}
| Property | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
— | Placeholder for text input |
dateFormat |
string |
"d.M.yyyy" |
date-fns format string for display |
locale |
string |
— | Locale for calendar (e.g. "cs-CZ") |
selectAllOnFocus |
boolean |
— | Select text on focus |
showTodayButton |
boolean |
— | Show "Today" button in popup |
showClearButton |
boolean |
— | Show "Clear" button in popup |
todayButtonText |
string |
— | Custom label for Today button |
clearButtonText |
string |
— | Custom label for Clear button |
disableFuture |
boolean |
— | Disallow selecting dates after today (today stays selectable) |
maxDate |
string |
— | Latest selectable date (ISO); later dates are disabled and rejected on manual entry |
minDate |
string |
— | Earliest selectable date (ISO); earlier dates are disabled and rejected on manual entry |
Value format: The stored value for
type: "date"is always aYYYY-MM-DDstring (e.g."2024-03-15"), not a JSDateobject. This is timezone-safe and maps directly to a SQLDATEcolumn. The widget accepts pre-existingDateobjects or ISO strings as input but always writes back aYYYY-MM-DDstring. For timestamps with time component, usetype: "datetime"(which works the same way but the display format includes time). For DB best practice: store date-only values asDATE/YYYY-MM-DD, store moments as UTC ISO datetime.
Limiting the range: Use
disableFuture: truefor fields that can never be in the future (e.g. exchange-rate dates), ormaxDate/minDatefor explicit bounds. Disabled days are not clickable in the calendar and out-of-range manual text entry is rejected (reverts to the last valid value).
DateTime
Same properties as Date (including disableFuture / maxDate / minDate), but with time component. Default format: "d.M.yyyy HH:mm".
{ type: "datetime", label: "Event Start", dateFormat: "d.M.yyyy HH:mm" }
File
{
type: "file",
label: "Attachments",
accept: ".pdf,.doc,image/*",
multiple: true,
innerLabel: "Drop files here",
showDropZone: true,
addFileLabel: "Add file"
}
| Property | Type | Default | Description |
|---|---|---|---|
accept |
string |
— | Accepted file types (MIME or extensions) |
multiple |
boolean |
— | Allow multiple file selection |
innerLabel |
string |
— | Label inside the drop zone |
showDropZone |
boolean |
true |
Show drag-and-drop area |
addFileLabel |
string |
— | Label for the "Add file" link |
File value format: Array<File | TsFileDescriptor> where:
interface TsFileDescriptor {
id?: string | number
name: string
size?: number
url?: string
type?: string
}
Button (In-Form)
{ type: "button", label: "Generate Report", action: "generate", variant: "outline" }
// Compact icon-only button (e.g. a swap control):
{ type: "button", action: "swap", icon: "ArrowLeftRight", iconOnly: true, variant: "outline" }
| Property | Type | Description |
|---|---|---|
action |
string |
Action name emitted on click |
variant |
TsButtonVariant |
Visual variant |
icon |
string |
Lucide icon name rendered inside the button (e.g. ArrowLeftRight) |
iconOnly |
boolean |
Render a compact, square icon-only button (no label, no full width) |
Separator
{ type: "separator", label: "Section Title" }
Used via layout type: "separator" in row items. Renders a horizontal rule with optional label.
Empty
{
type: "empty"
}
Used via layout type: "empty". Creates an invisible placeholder in the grid.
Table (Nested)
{
type: "table",
label: "Line Items",
columns: [
{ key: "product", title: "Product", type: "text" },
{ key: "qty", title: "Qty", type: "number", align: "right" },
{ key: "price", title: "Price", type: "number", align: "right" },
],
showCreateButton: true
}
| Property | Type | Description |
|---|---|---|
columns |
TsTableColumnDef[] |
Column definitions (same format as TsTable) |
showCreateButton |
boolean |
Show "Add row" button |
Value: Array<Record<string, unknown>> — array of row objects.
Relationship
{
type: "relationship",
label: "Assigned User",
targetEntity: "User",
mode: "single", // or "multiple"
variant: "dropdown", // or "dialog"
modalMaxWidth: "900px",
displayFields: ["name", "email"],
chipDisplayFields: ["name"],
columns: [
{ key: "name", title: "Name", sortable: true, filterable: true },
{ key: "email", title: "Email", visible: false },
],
showCreateButton: false,
showImportButton: false,
showExportButton: false,
showColumnSelector: true,
valueField: "id",
options: [
{ id: 1, name: "John", email: "john@example.com" },
{ id: 2, name: "Jane", email: "jane@example.com" },
]
}
| Property | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
— | Placeholder text |
targetEntity |
string |
— | Entity name for labels |
mode |
"single" | "multiple" |
"single" |
Selection mode |
variant |
"dropdown" | "dialog" |
"dropdown" |
UI variant (popover or modal) |
modalMaxWidth |
string |
— | CSS max-width for the dialog variant, e.g. "900px" |
displayFields |
string[] |
— | Fields shown in search results |
chipDisplayFields |
string[] |
— | Fields shown in selected chip |
columns |
TsTableColumnDef[] |
— | Full column definitions (overrides displayFields) |
showCreateButton |
boolean |
false |
Show the nested TsTable "New record" button |
showImportButton |
boolean |
false |
Show the nested TsTable "Import" button |
showExportButton |
boolean |
false |
Show the nested TsTable "Export" button |
showColumnSelector |
boolean |
true |
Show the nested TsTable column selector |
enableSorting |
boolean |
true |
Enable sorting in the nested TsTable |
enableFiltering |
boolean |
true |
Enable column filtering in the nested TsTable |
enablePagination |
boolean |
true |
Enable pagination in the nested TsTable |
enableRowMenu |
boolean |
true |
Enable row action menu in the nested TsTable |
enableColumnResizing |
boolean |
true |
Enable column resizing in the nested TsTable |
enableColumnReordering |
boolean |
true |
Enable column reordering in the nested TsTable |
pageSize |
number |
10 |
Initial nested TsTable page size |
pageSizeOptions |
number[] |
— | Nested TsTable page size options |
unhideableColumns |
string[] |
— | Column keys that cannot be hidden in selector |
valueField |
string |
— | Primary key field for stored value |
options |
Record<string, unknown>[] |
— | Available records to select from |
Infobox
{
type: "infobox",
label: "Notice",
content: "This form is read-only.",
variant: "warning", // "default" | "information" | "warning" | "success" | "destructive"
icon: "AlertTriangle",
closable: true
}
| Property | Type | Description |
|---|---|---|
content |
string |
Static text content |
value |
ReactNode |
Dynamic content |
variant |
TsInfoboxVariant |
Visual style |
icon |
string |
Lucide icon name (overrides variant default) |
closable |
boolean |
Allow user to dismiss |
Markdown
{
type: "markdown",
content: "### Title\n\n**Bold** text with [links](https://example.com)"
}
| Property | Type | Description |
|---|---|---|
content |
string |
Static markdown content |
value |
string |
Dynamic markdown content from form data |
The markdown renderer (
react-markdown+remark-gfm+react-syntax-highlighter) is lazy-loaded (code-split) and rendered inside aReact.Suspenseboundary. Importing<TsForm>therefore does not pull the ESM-only markdown toolchain into the module graph, so forms without markdown fields load and unit-test cleanly undernext/jest(nomoduleNameMapperstub required) and the heavy deps stay out of the main bundle. Tests that render a markdown field should await it (e.g. `await screen.f
*Truncated - read the full file at https://github.com/janbkrejci/TSWebUI-shadcn/blob/22c56cdb9afd650b01d22ee494eb55119f2de595/LLM/skills/tswebui/SKILL.md