@softium/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+
4
+ type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
5
+ type ButtonSize = 'sm' | 'md';
6
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: ButtonVariant;
8
+ size?: ButtonSize;
9
+ iconLeft?: ReactNode;
10
+ iconRight?: ReactNode;
11
+ /** stretch to the container width */
12
+ block?: boolean;
13
+ }
14
+ /**
15
+ * Button — the one primitive seeded deliberately (used everywhere). Everything else
16
+ * is extracted from real usage as it appears.
17
+ */
18
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
19
+
20
+ interface AppShellProps {
21
+ /** left sidebar content (usually <Sidebar>) */
22
+ sidebar: ReactNode;
23
+ /** top bar content (usually <Header>) */
24
+ header?: ReactNode;
25
+ /** main content area */
26
+ children: ReactNode;
27
+ /** collapse the sidebar to an icon-only rail (controlled) */
28
+ collapsed?: boolean;
29
+ /** expanded sidebar width in px. Default 224. */
30
+ sidebarWidth?: number;
31
+ /** on mobile: whether the sidebar drawer is open (controlled) */
32
+ sidebarOpen?: boolean;
33
+ /** on mobile: called when the backdrop is tapped to close the drawer */
34
+ onSidebarClose?: () => void;
35
+ className?: string;
36
+ }
37
+ /**
38
+ * AppShell — the docs/admin layout: a fixed left sidebar + a scrolling body with a
39
+ * sticky top header. Ported from the akron-ui shell:
40
+ * - desktop: sidebar can collapse to an icon-only rail (`collapsed`)
41
+ * - tablet: sidebar auto-collapses to the rail
42
+ * - mobile: sidebar becomes an off-canvas drawer (`sidebarOpen` + backdrop)
43
+ */
44
+ declare function AppShell({ sidebar, header, children, collapsed, sidebarWidth, sidebarOpen, onSidebarClose, className, }: AppShellProps): react.JSX.Element;
45
+
46
+ interface SidebarProps {
47
+ /** brand / logo shown in the sidebar's own header row */
48
+ header?: ReactNode;
49
+ /** pinned content at the very bottom (hidden while collapsed) */
50
+ footer?: ReactNode;
51
+ /** icon-only rail (controlled) */
52
+ collapsed?: boolean;
53
+ /** called by the built-in toggle to collapse (shown when expanded) */
54
+ onCollapse?: () => void;
55
+ /** called by the built-in toggle to expand (shown when collapsed) */
56
+ onExpand?: () => void;
57
+ /** grouped nav (usually <SidebarSection>) */
58
+ children: ReactNode;
59
+ className?: string;
60
+ }
61
+ /**
62
+ * Sidebar — vertical nav container with an optional brand header, a built-in
63
+ * collapse toggle (→ icon-only rail), scrollable grouped nav, and an optional
64
+ * pinned footer. Ported from akron-ui's LayoutSidebar.
65
+ */
66
+ declare function Sidebar({ header, footer, collapsed, onCollapse, onExpand, children, className, }: SidebarProps): react.JSX.Element;
67
+ interface SidebarBrandProps {
68
+ /** logo / mark slot */
69
+ logo?: ReactNode;
70
+ children: ReactNode;
71
+ /** small text under the title (e.g. version) */
72
+ subtitle?: ReactNode;
73
+ }
74
+ declare function SidebarBrand({ logo, children, subtitle }: SidebarBrandProps): react.JSX.Element;
75
+ interface SidebarSectionProps {
76
+ label?: ReactNode;
77
+ /** make the group header a toggle that shows/hides its items */
78
+ collapsible?: boolean;
79
+ /** initial open state when collapsible. Default true. */
80
+ defaultOpen?: boolean;
81
+ children: ReactNode;
82
+ }
83
+ declare function SidebarSection({ label, collapsible, defaultOpen, children, }: SidebarSectionProps): react.JSX.Element;
84
+ interface SidebarItemProps {
85
+ active?: boolean;
86
+ icon?: ReactNode;
87
+ /** label shown as a floating tooltip while the sidebar is collapsed */
88
+ tooltip?: string;
89
+ onClick?: () => void;
90
+ children: ReactNode;
91
+ }
92
+ declare function SidebarItem({ active, icon, tooltip, onClick, children }: SidebarItemProps): react.JSX.Element;
93
+
94
+ interface HeaderProps {
95
+ /** far-left slot shown only on mobile (a hamburger that opens the drawer) */
96
+ menuButton?: ReactNode;
97
+ /** left side — brand / breadcrumb. Ignored if `children` is provided. */
98
+ logo?: ReactNode;
99
+ /** left side — plain title. Ignored if `logo` or `children` is provided. */
100
+ title?: ReactNode;
101
+ /** left side override (takes precedence over `logo` / `title`) */
102
+ children?: ReactNode;
103
+ /** center nav slot */
104
+ nav?: ReactNode;
105
+ /** right side — actions (search, theme toggle, links, …) */
106
+ actions?: ReactNode;
107
+ className?: string;
108
+ }
109
+ /**
110
+ * Header — the top bar. A mobile menu button, then a brand/breadcrumb on the left,
111
+ * an optional center nav, and an actions cluster pushed to the right. Ported from
112
+ * akron-ui's Header.
113
+ */
114
+ declare function Header({ menuButton, logo, title, children, nav, actions, className, }: HeaderProps): react.JSX.Element;
115
+
116
+ type Theme = 'light' | 'dark';
117
+ interface UseThemeOptions {
118
+ /** theme to use when nothing is stored. Default 'light'. */
119
+ defaultTheme?: Theme;
120
+ /** persist the choice to localStorage. Default true. */
121
+ persist?: boolean;
122
+ }
123
+ /**
124
+ * useTheme — applies `data-theme` to <html> and (optionally) persists the choice.
125
+ * The actual re-skin is pure CSS (token swap), so this hook only flips the attribute.
126
+ */
127
+ declare function useTheme({ defaultTheme, persist }?: UseThemeOptions): {
128
+ theme: Theme;
129
+ setTheme: (next: Theme) => void;
130
+ toggleTheme: () => void;
131
+ };
132
+
133
+ interface ThemeToggleProps {
134
+ /** optional controlled value; when omitted the component manages its own theme */
135
+ theme?: Theme;
136
+ onToggle?: () => void;
137
+ /** shown in light mode (click → dark). Defaults to a moon icon. */
138
+ labelLight?: ReactNode;
139
+ /** shown in dark mode (click → light). Defaults to a sun icon. */
140
+ labelDark?: ReactNode;
141
+ }
142
+ /**
143
+ * ThemeToggle — flips light/dark. Uncontrolled by default (manages + persists its
144
+ * own theme); pass `theme` + `onToggle` to control it from outside.
145
+ */
146
+ declare function ThemeToggle({ theme: controlled, onToggle, labelLight, labelDark, }: ThemeToggleProps): react.JSX.Element;
147
+
148
+ interface SwitchProps {
149
+ checked: boolean;
150
+ onChange: (checked: boolean) => void;
151
+ /** text shown next to the switch */
152
+ label?: ReactNode;
153
+ disabled?: boolean;
154
+ className?: string;
155
+ }
156
+ /**
157
+ * Switch — an independent on/off toggle. Built on a real checkbox (role="switch")
158
+ * so it's keyboard- and screen-reader-accessible.
159
+ */
160
+ declare function Switch({ checked, onChange, label, disabled, className }: SwitchProps): react.JSX.Element;
161
+
162
+ /**
163
+ * @softium/ui
164
+ *
165
+ * General React primitives + app-shell layout for softium-ui.
166
+ * Remember to import the stylesheet once:
167
+ * import '@softium/ui/styles.css';
168
+ */
169
+ declare const VERSION = "0.0.0";
170
+
171
+ export { AppShell, type AppShellProps, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Header, type HeaderProps, Sidebar, SidebarBrand, type SidebarBrandProps, SidebarItem, type SidebarItemProps, type SidebarProps, SidebarSection, type SidebarSectionProps, Switch, type SwitchProps, type Theme, ThemeToggle, type ThemeToggleProps, type UseThemeOptions, VERSION, useTheme };
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ "use client";
2
+ import { forwardRef, useState, useCallback, useEffect, useId } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { ChevronLeft, ChevronDown, Moon, Sun } from 'lucide-react';
5
+ import { createPortal } from 'react-dom';
6
+
7
+ // src/components/Button.tsx
8
+
9
+ // src/util/cx.ts
10
+ function cx(...parts) {
11
+ return parts.filter(Boolean).join(" ");
12
+ }
13
+ var Button = forwardRef(function Button2({
14
+ variant = "secondary",
15
+ size = "md",
16
+ iconLeft,
17
+ iconRight,
18
+ block,
19
+ className,
20
+ children,
21
+ type,
22
+ ...rest
23
+ }, ref) {
24
+ return /* @__PURE__ */ jsxs(
25
+ "button",
26
+ {
27
+ ref,
28
+ type: type ?? "button",
29
+ className: cx(
30
+ "sft-button",
31
+ `sft-button--${variant}`,
32
+ `sft-button--${size}`,
33
+ block && "sft-button--block",
34
+ className
35
+ ),
36
+ ...rest,
37
+ children: [
38
+ iconLeft && /* @__PURE__ */ jsx("span", { className: "sft-button__icon", children: iconLeft }),
39
+ children != null && /* @__PURE__ */ jsx("span", { className: "sft-button__label", children }),
40
+ iconRight && /* @__PURE__ */ jsx("span", { className: "sft-button__icon", children: iconRight })
41
+ ]
42
+ }
43
+ );
44
+ });
45
+ function AppShell({
46
+ sidebar,
47
+ header,
48
+ children,
49
+ collapsed,
50
+ sidebarWidth = 224,
51
+ sidebarOpen = false,
52
+ onSidebarClose,
53
+ className
54
+ }) {
55
+ const vars = { "--sft-sidebar-width": `${sidebarWidth}px` };
56
+ return /* @__PURE__ */ jsxs(
57
+ "div",
58
+ {
59
+ className: cx("sft-app sft-shell", className),
60
+ style: vars,
61
+ "data-collapsed": collapsed || void 0,
62
+ "data-drawer-open": sidebarOpen || void 0,
63
+ children: [
64
+ /* @__PURE__ */ jsx("aside", { className: "sft-shell__sidebar", children: sidebar }),
65
+ sidebarOpen && onSidebarClose && /* @__PURE__ */ jsx("div", { className: "sft-shell__overlay", onClick: onSidebarClose, "aria-hidden": "true" }),
66
+ /* @__PURE__ */ jsxs("div", { className: "sft-shell__main", children: [
67
+ header && /* @__PURE__ */ jsx("div", { className: "sft-shell__header", children: header }),
68
+ /* @__PURE__ */ jsx("main", { className: "sft-shell__content", children })
69
+ ] })
70
+ ]
71
+ }
72
+ );
73
+ }
74
+ function Sidebar({
75
+ header,
76
+ footer,
77
+ collapsed = false,
78
+ onCollapse,
79
+ onExpand,
80
+ children,
81
+ className
82
+ }) {
83
+ const toggle = collapsed ? onExpand : onCollapse;
84
+ return /* @__PURE__ */ jsxs("div", { className: cx("sft-sidebar", collapsed && "sft-sidebar--collapsed", className), children: [
85
+ (header || toggle) && /* @__PURE__ */ jsxs("div", { className: "sft-sidebar__header", children: [
86
+ header && /* @__PURE__ */ jsx("div", { className: "sft-sidebar__header-content", children: header }),
87
+ toggle && /* @__PURE__ */ jsx(
88
+ "button",
89
+ {
90
+ type: "button",
91
+ className: "sft-sidebar__collapse",
92
+ onClick: toggle,
93
+ "aria-label": collapsed ? "expand sidebar" : "collapse sidebar",
94
+ children: /* @__PURE__ */ jsx(
95
+ ChevronLeft,
96
+ {
97
+ size: 16,
98
+ className: cx(
99
+ "sft-sidebar__collapse-icon",
100
+ collapsed && "sft-sidebar__collapse-icon--flipped"
101
+ )
102
+ }
103
+ )
104
+ }
105
+ )
106
+ ] }),
107
+ /* @__PURE__ */ jsx("nav", { className: "sft-sidebar__content", children }),
108
+ footer && /* @__PURE__ */ jsx("div", { className: "sft-sidebar__footer", children: footer })
109
+ ] });
110
+ }
111
+ function SidebarBrand({ logo, children, subtitle }) {
112
+ return /* @__PURE__ */ jsxs("div", { className: "sft-sidebar__brand", children: [
113
+ logo && /* @__PURE__ */ jsx("span", { className: "sft-sidebar__logo", children: logo }),
114
+ /* @__PURE__ */ jsxs("span", { className: "sft-sidebar__brand-text", children: [
115
+ /* @__PURE__ */ jsx("span", { className: "sft-sidebar__title", children }),
116
+ subtitle && /* @__PURE__ */ jsx("span", { className: "sft-sidebar__subtitle", children: subtitle })
117
+ ] })
118
+ ] });
119
+ }
120
+ function SidebarSection({
121
+ label,
122
+ collapsible = false,
123
+ defaultOpen = true,
124
+ children
125
+ }) {
126
+ const [open, setOpen] = useState(defaultOpen);
127
+ const hidden = collapsible && !open;
128
+ return /* @__PURE__ */ jsxs("div", { className: "sft-sidebar__section", children: [
129
+ label && (collapsible ? /* @__PURE__ */ jsxs(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ className: "sft-sidebar__section-toggle",
134
+ onClick: () => setOpen((v) => !v),
135
+ "aria-expanded": open,
136
+ children: [
137
+ /* @__PURE__ */ jsx("span", { children: label }),
138
+ /* @__PURE__ */ jsx(
139
+ ChevronDown,
140
+ {
141
+ size: 12,
142
+ className: cx(
143
+ "sft-sidebar__section-chevron",
144
+ open && "sft-sidebar__section-chevron--open"
145
+ )
146
+ }
147
+ )
148
+ ]
149
+ }
150
+ ) : /* @__PURE__ */ jsx("div", { className: "sft-sidebar__section-label", children: label })),
151
+ !hidden && /* @__PURE__ */ jsx("div", { className: "sft-sidebar__items", children })
152
+ ] });
153
+ }
154
+ function SidebarItem({ active, icon, tooltip, onClick, children }) {
155
+ const [tip, setTip] = useState(null);
156
+ const onEnter = useCallback((e) => {
157
+ const r = e.currentTarget.getBoundingClientRect();
158
+ setTip({ top: r.top + r.height / 2, left: r.right + 8 });
159
+ }, []);
160
+ const onLeave = useCallback(() => setTip(null), []);
161
+ return /* @__PURE__ */ jsxs(
162
+ "button",
163
+ {
164
+ type: "button",
165
+ className: cx("sft-sidebar__item", active && "sft-sidebar__item--active"),
166
+ "aria-current": active ? "page" : void 0,
167
+ onClick,
168
+ onMouseEnter: tooltip ? onEnter : void 0,
169
+ onMouseLeave: tooltip ? onLeave : void 0,
170
+ children: [
171
+ icon && /* @__PURE__ */ jsx("span", { className: "sft-sidebar__item-icon", children: icon }),
172
+ /* @__PURE__ */ jsx("span", { className: "sft-sidebar__item-label", children }),
173
+ tooltip && tip && createPortal(
174
+ /* @__PURE__ */ jsx("div", { className: "sft-sidebar__tooltip", style: { top: tip.top, left: tip.left }, children: tooltip }),
175
+ document.body
176
+ )
177
+ ]
178
+ }
179
+ );
180
+ }
181
+ function Header({
182
+ menuButton,
183
+ logo,
184
+ title,
185
+ children,
186
+ nav,
187
+ actions,
188
+ className
189
+ }) {
190
+ const left = children ?? logo ?? (title != null ? /* @__PURE__ */ jsx("h1", { className: "sft-header__title", children: title }) : null);
191
+ return /* @__PURE__ */ jsxs("header", { className: cx("sft-header", className), children: [
192
+ menuButton && /* @__PURE__ */ jsx("div", { className: "sft-header__menu", children: menuButton }),
193
+ left && /* @__PURE__ */ jsx("div", { className: "sft-header__left", children: left }),
194
+ nav && /* @__PURE__ */ jsx("nav", { className: "sft-header__nav", children: nav }),
195
+ actions && /* @__PURE__ */ jsx("div", { className: "sft-header__actions", children: actions })
196
+ ] });
197
+ }
198
+ var STORAGE_KEY = "softium.theme";
199
+ function readStored() {
200
+ if (typeof window === "undefined") return null;
201
+ try {
202
+ const v = window.localStorage.getItem(STORAGE_KEY);
203
+ return v === "light" || v === "dark" ? v : null;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ function useTheme({ defaultTheme = "light", persist = true } = {}) {
209
+ const [theme, setThemeState] = useState(() => readStored() ?? defaultTheme);
210
+ useEffect(() => {
211
+ if (typeof document !== "undefined") {
212
+ document.documentElement.setAttribute("data-theme", theme);
213
+ }
214
+ if (persist && typeof window !== "undefined") {
215
+ try {
216
+ window.localStorage.setItem(STORAGE_KEY, theme);
217
+ } catch {
218
+ }
219
+ }
220
+ }, [theme, persist]);
221
+ const setTheme = useCallback((next) => setThemeState(next), []);
222
+ const toggleTheme = useCallback(
223
+ () => setThemeState((t) => t === "light" ? "dark" : "light"),
224
+ []
225
+ );
226
+ return { theme, setTheme, toggleTheme };
227
+ }
228
+ function ThemeToggle({
229
+ theme: controlled,
230
+ onToggle,
231
+ labelLight = /* @__PURE__ */ jsx(Moon, { size: 16 }),
232
+ labelDark = /* @__PURE__ */ jsx(Sun, { size: 16 })
233
+ }) {
234
+ const internal = useTheme();
235
+ const theme = controlled ?? internal.theme;
236
+ const toggle = onToggle ?? internal.toggleTheme;
237
+ return /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: toggle, "aria-label": "Toggle theme", children: theme === "light" ? labelLight : labelDark });
238
+ }
239
+ function Switch({ checked, onChange, label, disabled, className }) {
240
+ const id = useId();
241
+ return /* @__PURE__ */ jsxs("label", { htmlFor: id, className: cx("sft-switch", disabled && "sft-switch--disabled", className), children: [
242
+ /* @__PURE__ */ jsx(
243
+ "input",
244
+ {
245
+ id,
246
+ type: "checkbox",
247
+ role: "switch",
248
+ "aria-checked": checked,
249
+ className: "sft-switch__input",
250
+ checked,
251
+ disabled,
252
+ onChange: (e) => onChange(e.target.checked)
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsx("span", { className: "sft-switch__track", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "sft-switch__thumb" }) }),
256
+ label != null && /* @__PURE__ */ jsx("span", { className: "sft-switch__label", children: label })
257
+ ] });
258
+ }
259
+
260
+ // src/index.ts
261
+ var VERSION = "0.0.0";
262
+
263
+ export { AppShell, Button, Header, Sidebar, SidebarBrand, SidebarItem, SidebarSection, Switch, ThemeToggle, VERSION, useTheme };
264
+ //# sourceMappingURL=index.js.map
265
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/cx.ts","../src/components/Button.tsx","../src/components/AppShell.tsx","../src/components/Sidebar.tsx","../src/components/Header.tsx","../src/hooks/useTheme.ts","../src/components/ThemeToggle.tsx","../src/components/Switch.tsx","../src/index.ts"],"names":["Button","jsxs","jsx","useState","useCallback"],"mappings":";;;;;;;;AACO,SAAS,MAAM,KAAA,EAAyD;AAC7E,EAAA,OAAO,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AACvC;ACgBO,IAAM,MAAA,GAAS,UAAA,CAA2C,SAASA,OAAAA,CACxE;AAAA,EACE,OAAA,GAAU,WAAA;AAAA,EACV,IAAA,GAAO,IAAA;AAAA,EACP,QAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,uBACE,IAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,MAAM,IAAA,IAAQ,QAAA;AAAA,MACd,SAAA,EAAW,EAAA;AAAA,QACT,YAAA;AAAA,QACA,eAAe,OAAO,CAAA,CAAA;AAAA,QACtB,eAAe,IAAI,CAAA,CAAA;AAAA,QACnB,KAAA,IAAS,mBAAA;AAAA,QACT;AAAA,OACF;AAAA,MACC,GAAG,IAAA;AAAA,MAEH,QAAA,EAAA;AAAA,QAAA,QAAA,oBAAY,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,kBAAA,EAAoB,QAAA,EAAA,QAAA,EAAS,CAAA;AAAA,QACzD,YAAY,IAAA,oBAAQ,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,qBAAqB,QAAA,EAAS,CAAA;AAAA,QAClE,SAAA,oBAAa,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,oBAAoB,QAAA,EAAA,SAAA,EAAU;AAAA;AAAA;AAAA,GAC9D;AAEJ,CAAC;ACvBM,SAAS,QAAA,CAAS;AAAA,EACvB,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,YAAA,GAAe,GAAA;AAAA,EACf,WAAA,GAAc,KAAA;AAAA,EACd,cAAA;AAAA,EACA;AACF,CAAA,EAAkB;AAChB,EAAA,MAAM,IAAA,GAAO,EAAE,qBAAA,EAAuB,CAAA,EAAG,YAAY,CAAA,EAAA,CAAA,EAAK;AAE1D,EAAA,uBACEC,IAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,EAAA,CAAG,mBAAA,EAAqB,SAAS,CAAA;AAAA,MAC5C,KAAA,EAAO,IAAA;AAAA,MACP,kBAAgB,SAAA,IAAa,MAAA;AAAA,MAC7B,oBAAkB,WAAA,IAAe,MAAA;AAAA,MAEjC,QAAA,EAAA;AAAA,wBAAAC,GAAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,oBAAA,EAAsB,QAAA,EAAA,OAAA,EAAQ,CAAA;AAAA,QAC9C,WAAA,IAAe,cAAA,oBACdA,GAAAA,CAAC,KAAA,EAAA,EAAI,WAAU,oBAAA,EAAqB,OAAA,EAAS,cAAA,EAAgB,aAAA,EAAY,MAAA,EAAO,CAAA;AAAA,wBAElFD,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,iBAAA,EACZ,QAAA,EAAA;AAAA,UAAA,MAAA,oBAAUC,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBAAqB,QAAA,EAAA,MAAA,EAAO,CAAA;AAAA,0BACtDA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,sBAAsB,QAAA,EAAS;AAAA,SAAA,EACjD;AAAA;AAAA;AAAA,GACF;AAEJ;AC/BO,SAAS,OAAA,CAAQ;AAAA,EACtB,MAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA,GAAY,KAAA;AAAA,EACZ,UAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAAiB;AACf,EAAA,MAAM,MAAA,GAAS,YAAY,QAAA,GAAW,UAAA;AAEtC,EAAA,uBACED,KAAC,KAAA,EAAA,EAAI,SAAA,EAAW,GAAG,aAAA,EAAe,SAAA,IAAa,wBAAA,EAA0B,SAAS,CAAA,EAC9E,QAAA,EAAA;AAAA,IAAA,CAAA,MAAA,IAAU,MAAA,qBACVA,IAAAA,CAAC,KAAA,EAAA,EAAI,WAAU,qBAAA,EACZ,QAAA,EAAA;AAAA,MAAA,MAAA,oBAAUC,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,+BAA+B,QAAA,EAAA,MAAA,EAAO,CAAA;AAAA,MAC/D,0BACCA,GAAAA;AAAA,QAAC,QAAA;AAAA,QAAA;AAAA,UACC,IAAA,EAAK,QAAA;AAAA,UACL,SAAA,EAAU,uBAAA;AAAA,UACV,OAAA,EAAS,MAAA;AAAA,UACT,YAAA,EAAY,YAAY,gBAAA,GAAmB,kBAAA;AAAA,UAE3C,QAAA,kBAAAA,GAAAA;AAAA,YAAC,WAAA;AAAA,YAAA;AAAA,cACC,IAAA,EAAM,EAAA;AAAA,cACN,SAAA,EAAW,EAAA;AAAA,gBACT,4BAAA;AAAA,gBACA,SAAA,IAAa;AAAA;AACf;AAAA;AACF;AAAA;AACF,KAAA,EAEJ,CAAA;AAAA,oBAEFA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAwB,QAAA,EAAS,CAAA;AAAA,IAC/C,0BAAUA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uBAAuB,QAAA,EAAA,MAAA,EAAO;AAAA,GAAA,EAC1D,CAAA;AAEJ;AAUO,SAAS,YAAA,CAAa,EAAE,IAAA,EAAM,QAAA,EAAU,UAAS,EAAsB;AAC5E,EAAA,uBACED,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAA,EACZ,QAAA,EAAA;AAAA,IAAA,IAAA,oBAAQC,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,qBAAqB,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,oBACnDD,IAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,yBAAA,EACd,QAAA,EAAA;AAAA,sBAAAC,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,oBAAA,EAAsB,QAAA,EAAS,CAAA;AAAA,MAC9C,4BAAYA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,yBAAyB,QAAA,EAAA,QAAA,EAAS;AAAA,KAAA,EACjE;AAAA,GAAA,EACF,CAAA;AAEJ;AAWO,SAAS,cAAA,CAAe;AAAA,EAC7B,KAAA;AAAA,EACA,WAAA,GAAc,KAAA;AAAA,EACd,WAAA,GAAc,IAAA;AAAA,EACd;AACF,CAAA,EAAwB;AACtB,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,WAAW,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,eAAe,CAAC,IAAA;AAE/B,EAAA,uBACED,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,sBAAA,EACZ,QAAA,EAAA;AAAA,IAAA,KAAA,KACE,8BACCA,IAAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QACC,IAAA,EAAK,QAAA;AAAA,QACL,SAAA,EAAU,6BAAA;AAAA,QACV,SAAS,MAAM,OAAA,CAAQ,CAAC,CAAA,KAAM,CAAC,CAAC,CAAA;AAAA,QAChC,eAAA,EAAe,IAAA;AAAA,QAEf,QAAA,EAAA;AAAA,0BAAAC,GAAAA,CAAC,UAAM,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,0BACbA,GAAAA;AAAA,YAAC,WAAA;AAAA,YAAA;AAAA,cACC,IAAA,EAAM,EAAA;AAAA,cACN,SAAA,EAAW,EAAA;AAAA,gBACT,8BAAA;AAAA,gBACA,IAAA,IAAQ;AAAA;AACV;AAAA;AACF;AAAA;AAAA,wBAGFA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8BAA8B,QAAA,EAAA,KAAA,EAAM,CAAA,CAAA;AAAA,IAEtD,CAAC,MAAA,oBAAUA,IAAC,KAAA,EAAA,EAAI,SAAA,EAAU,sBAAsB,QAAA,EAAS;AAAA,GAAA,EAC5D,CAAA;AAEJ;AAWO,SAAS,YAAY,EAAE,MAAA,EAAQ,MAAM,OAAA,EAAS,OAAA,EAAS,UAAS,EAAqB;AAC1F,EAAA,MAAM,CAAC,GAAA,EAAK,MAAM,CAAA,GAAI,SAA+C,IAAI,CAAA;AAEzE,EAAA,MAAM,OAAA,GAAU,WAAA,CAAY,CAAC,CAAA,KAAqC;AAChE,IAAA,MAAM,CAAA,GAAI,CAAA,CAAE,aAAA,CAAc,qBAAA,EAAsB;AAChD,IAAA,MAAA,CAAO,EAAE,GAAA,EAAK,CAAA,CAAE,GAAA,GAAM,CAAA,CAAE,MAAA,GAAS,CAAA,EAAG,IAAA,EAAM,CAAA,CAAE,KAAA,GAAQ,CAAA,EAAG,CAAA;AAAA,EACzD,CAAA,EAAG,EAAE,CAAA;AACL,EAAA,MAAM,UAAU,WAAA,CAAY,MAAM,OAAO,IAAI,CAAA,EAAG,EAAE,CAAA;AAElD,EAAA,uBACED,IAAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACL,SAAA,EAAW,EAAA,CAAG,mBAAA,EAAqB,MAAA,IAAU,2BAA2B,CAAA;AAAA,MACxE,cAAA,EAAc,SAAS,MAAA,GAAS,MAAA;AAAA,MAChC,OAAA;AAAA,MACA,YAAA,EAAc,UAAU,OAAA,GAAU,MAAA;AAAA,MAClC,YAAA,EAAc,UAAU,OAAA,GAAU,MAAA;AAAA,MAEjC,QAAA,EAAA;AAAA,QAAA,IAAA,oBAAQC,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,0BAA0B,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,wBACxDA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,2BAA2B,QAAA,EAAS,CAAA;AAAA,QACnD,WACC,GAAA,IACA,YAAA;AAAA,0BACEA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAuB,KAAA,EAAO,EAAE,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,IAAA,EAAM,GAAA,CAAI,IAAA,IACpE,QAAA,EAAA,OAAA,EACH,CAAA;AAAA,UACA,QAAA,CAAS;AAAA;AACX;AAAA;AAAA,GACJ;AAEJ;AClJO,SAAS,MAAA,CAAO;AAAA,EACrB,UAAA;AAAA,EACA,IAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAgB;AACd,EAAA,MAAM,IAAA,GACJ,QAAA,IAAY,IAAA,KAAS,KAAA,IAAS,IAAA,mBAAOA,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,mBAAA,EAAqB,QAAA,EAAA,KAAA,EAAM,CAAA,GAAQ,IAAA,CAAA;AAExF,EAAA,uBACED,IAAAA,CAAC,QAAA,EAAA,EAAO,WAAW,EAAA,CAAG,YAAA,EAAc,SAAS,CAAA,EAC1C,QAAA,EAAA;AAAA,IAAA,UAAA,oBAAcC,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAoB,QAAA,EAAA,UAAA,EAAW,CAAA;AAAA,IAC5D,wBAAQA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAoB,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,IAChD,uBAAOA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mBAAmB,QAAA,EAAA,GAAA,EAAI,CAAA;AAAA,IAC7C,2BAAWA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uBAAuB,QAAA,EAAA,OAAA,EAAQ;AAAA,GAAA,EAC5D,CAAA;AAEJ;ACxCA,IAAM,WAAA,GAAc,eAAA;AAEpB,SAAS,UAAA,GAA2B;AAClC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,IAAA;AAC1C,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA;AACjD,IAAA,OAAO,CAAA,KAAM,OAAA,IAAW,CAAA,KAAM,MAAA,GAAS,CAAA,GAAI,IAAA;AAAA,EAC7C,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAaO,SAAS,QAAA,CAAS,EAAE,YAAA,GAAe,OAAA,EAAS,UAAU,IAAA,EAAK,GAAqB,EAAC,EAAG;AACzF,EAAA,MAAM,CAAC,OAAO,aAAa,CAAA,GAAIC,SAAgB,MAAM,UAAA,MAAgB,YAAY,CAAA;AAEjF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,eAAA,CAAgB,YAAA,CAAa,YAAA,EAAc,KAAK,CAAA;AAAA,IAC3D;AACA,IAAA,IAAI,OAAA,IAAW,OAAO,MAAA,KAAW,WAAA,EAAa;AAC5C,MAAA,IAAI;AACF,QAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAA,EAAa,KAAK,CAAA;AAAA,MAChD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,KAAA,EAAO,OAAO,CAAC,CAAA;AAEnB,EAAA,MAAM,QAAA,GAAWC,YAAY,CAAC,IAAA,KAAgB,cAAc,IAAI,CAAA,EAAG,EAAE,CAAA;AACrE,EAAA,MAAM,WAAA,GAAcA,WAAAA;AAAA,IAClB,MAAM,aAAA,CAAc,CAAC,MAAO,CAAA,KAAM,OAAA,GAAU,SAAS,OAAQ,CAAA;AAAA,IAC7D;AAAC,GACH;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,WAAA,EAAY;AACxC;AC/BO,SAAS,WAAA,CAAY;AAAA,EAC1B,KAAA,EAAO,UAAA;AAAA,EACP,QAAA;AAAA,EACA,UAAA,mBAAaF,GAAAA,CAAC,IAAA,EAAA,EAAK,MAAM,EAAA,EAAI,CAAA;AAAA,EAC7B,SAAA,mBAAYA,GAAAA,CAAC,GAAA,EAAA,EAAI,MAAM,EAAA,EAAI;AAC7B,CAAA,EAAqB;AACnB,EAAA,MAAM,WAAW,QAAA,EAAS;AAC1B,EAAA,MAAM,KAAA,GAAQ,cAAc,QAAA,CAAS,KAAA;AACrC,EAAA,MAAM,MAAA,GAAS,YAAY,QAAA,CAAS,WAAA;AAEpC,EAAA,uBACEA,GAAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,SAAQ,IAAA,EAAK,IAAA,EAAK,OAAA,EAAS,MAAA,EAAQ,YAAA,EAAW,cAAA,EAC3D,QAAA,EAAA,KAAA,KAAU,OAAA,GAAU,aAAa,SAAA,EACpC,CAAA;AAEJ;AClBO,SAAS,OAAO,EAAE,OAAA,EAAS,UAAU,KAAA,EAAO,QAAA,EAAU,WAAU,EAAgB;AACrF,EAAA,MAAM,KAAK,KAAA,EAAM;AACjB,EAAA,uBACED,IAAAA,CAAC,OAAA,EAAA,EAAM,OAAA,EAAS,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,YAAA,EAAc,QAAA,IAAY,sBAAA,EAAwB,SAAS,CAAA,EAC3F,QAAA,EAAA;AAAA,oBAAAC,GAAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,EAAA;AAAA,QACA,IAAA,EAAK,UAAA;AAAA,QACL,IAAA,EAAK,QAAA;AAAA,QACL,cAAA,EAAc,OAAA;AAAA,QACd,SAAA,EAAU,mBAAA;AAAA,QACV,OAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,OAAO,OAAO;AAAA;AAAA,KAC5C;AAAA,oBACAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mBAAA,EAAoB,aAAA,EAAY,MAAA,EAC9C,QAAA,kBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mBAAA,EAAoB,CAAA,EACtC,CAAA;AAAA,IACC,SAAS,IAAA,oBAAQA,IAAC,MAAA,EAAA,EAAK,SAAA,EAAU,qBAAqB,QAAA,EAAA,KAAA,EAAM;AAAA,GAAA,EAC/D,CAAA;AAEJ;;;AC5BO,IAAM,OAAA,GAAU","file":"index.js","sourcesContent":["/** Minimal classnames joiner — no dependency. */\nexport function cx(...parts: Array<string | false | null | undefined>): string {\n return parts.filter(Boolean).join(' ');\n}\n","import { type ButtonHTMLAttributes, type ReactNode, forwardRef } from 'react';\nimport { cx } from '../util/cx';\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';\nexport type ButtonSize = 'sm' | 'md';\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n iconLeft?: ReactNode;\n iconRight?: ReactNode;\n /** stretch to the container width */\n block?: boolean;\n}\n\n/**\n * Button — the one primitive seeded deliberately (used everywhere). Everything else\n * is extracted from real usage as it appears.\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'secondary',\n size = 'md',\n iconLeft,\n iconRight,\n block,\n className,\n children,\n type,\n ...rest\n },\n ref,\n) {\n return (\n <button\n ref={ref}\n type={type ?? 'button'}\n className={cx(\n 'sft-button',\n `sft-button--${variant}`,\n `sft-button--${size}`,\n block && 'sft-button--block',\n className,\n )}\n {...rest}\n >\n {iconLeft && <span className=\"sft-button__icon\">{iconLeft}</span>}\n {children != null && <span className=\"sft-button__label\">{children}</span>}\n {iconRight && <span className=\"sft-button__icon\">{iconRight}</span>}\n </button>\n );\n});\n","import type { CSSProperties, ReactNode } from 'react';\nimport { cx } from '../util/cx';\n\nexport interface AppShellProps {\n /** left sidebar content (usually <Sidebar>) */\n sidebar: ReactNode;\n /** top bar content (usually <Header>) */\n header?: ReactNode;\n /** main content area */\n children: ReactNode;\n /** collapse the sidebar to an icon-only rail (controlled) */\n collapsed?: boolean;\n /** expanded sidebar width in px. Default 224. */\n sidebarWidth?: number;\n /** on mobile: whether the sidebar drawer is open (controlled) */\n sidebarOpen?: boolean;\n /** on mobile: called when the backdrop is tapped to close the drawer */\n onSidebarClose?: () => void;\n className?: string;\n}\n\n/**\n * AppShell — the docs/admin layout: a fixed left sidebar + a scrolling body with a\n * sticky top header. Ported from the akron-ui shell:\n * - desktop: sidebar can collapse to an icon-only rail (`collapsed`)\n * - tablet: sidebar auto-collapses to the rail\n * - mobile: sidebar becomes an off-canvas drawer (`sidebarOpen` + backdrop)\n */\nexport function AppShell({\n sidebar,\n header,\n children,\n collapsed,\n sidebarWidth = 224,\n sidebarOpen = false,\n onSidebarClose,\n className,\n}: AppShellProps) {\n const vars = { '--sft-sidebar-width': `${sidebarWidth}px` } as CSSProperties;\n\n return (\n <div\n className={cx('sft-app sft-shell', className)}\n style={vars}\n data-collapsed={collapsed || undefined}\n data-drawer-open={sidebarOpen || undefined}\n >\n <aside className=\"sft-shell__sidebar\">{sidebar}</aside>\n {sidebarOpen && onSidebarClose && (\n <div className=\"sft-shell__overlay\" onClick={onSidebarClose} aria-hidden=\"true\" />\n )}\n <div className=\"sft-shell__main\">\n {header && <div className=\"sft-shell__header\">{header}</div>}\n <main className=\"sft-shell__content\">{children}</main>\n </div>\n </div>\n );\n}\n","import { ChevronDown, ChevronLeft } from 'lucide-react';\nimport { type MouseEvent, type ReactNode, useCallback, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cx } from '../util/cx';\n\nexport interface SidebarProps {\n /** brand / logo shown in the sidebar's own header row */\n header?: ReactNode;\n /** pinned content at the very bottom (hidden while collapsed) */\n footer?: ReactNode;\n /** icon-only rail (controlled) */\n collapsed?: boolean;\n /** called by the built-in toggle to collapse (shown when expanded) */\n onCollapse?: () => void;\n /** called by the built-in toggle to expand (shown when collapsed) */\n onExpand?: () => void;\n /** grouped nav (usually <SidebarSection>) */\n children: ReactNode;\n className?: string;\n}\n\n/**\n * Sidebar — vertical nav container with an optional brand header, a built-in\n * collapse toggle (→ icon-only rail), scrollable grouped nav, and an optional\n * pinned footer. Ported from akron-ui's LayoutSidebar.\n */\nexport function Sidebar({\n header,\n footer,\n collapsed = false,\n onCollapse,\n onExpand,\n children,\n className,\n}: SidebarProps) {\n const toggle = collapsed ? onExpand : onCollapse;\n\n return (\n <div className={cx('sft-sidebar', collapsed && 'sft-sidebar--collapsed', className)}>\n {(header || toggle) && (\n <div className=\"sft-sidebar__header\">\n {header && <div className=\"sft-sidebar__header-content\">{header}</div>}\n {toggle && (\n <button\n type=\"button\"\n className=\"sft-sidebar__collapse\"\n onClick={toggle}\n aria-label={collapsed ? 'expand sidebar' : 'collapse sidebar'}\n >\n <ChevronLeft\n size={16}\n className={cx(\n 'sft-sidebar__collapse-icon',\n collapsed && 'sft-sidebar__collapse-icon--flipped',\n )}\n />\n </button>\n )}\n </div>\n )}\n <nav className=\"sft-sidebar__content\">{children}</nav>\n {footer && <div className=\"sft-sidebar__footer\">{footer}</div>}\n </div>\n );\n}\n\nexport interface SidebarBrandProps {\n /** logo / mark slot */\n logo?: ReactNode;\n children: ReactNode;\n /** small text under the title (e.g. version) */\n subtitle?: ReactNode;\n}\n\nexport function SidebarBrand({ logo, children, subtitle }: SidebarBrandProps) {\n return (\n <div className=\"sft-sidebar__brand\">\n {logo && <span className=\"sft-sidebar__logo\">{logo}</span>}\n <span className=\"sft-sidebar__brand-text\">\n <span className=\"sft-sidebar__title\">{children}</span>\n {subtitle && <span className=\"sft-sidebar__subtitle\">{subtitle}</span>}\n </span>\n </div>\n );\n}\n\nexport interface SidebarSectionProps {\n label?: ReactNode;\n /** make the group header a toggle that shows/hides its items */\n collapsible?: boolean;\n /** initial open state when collapsible. Default true. */\n defaultOpen?: boolean;\n children: ReactNode;\n}\n\nexport function SidebarSection({\n label,\n collapsible = false,\n defaultOpen = true,\n children,\n}: SidebarSectionProps) {\n const [open, setOpen] = useState(defaultOpen);\n const hidden = collapsible && !open;\n\n return (\n <div className=\"sft-sidebar__section\">\n {label &&\n (collapsible ? (\n <button\n type=\"button\"\n className=\"sft-sidebar__section-toggle\"\n onClick={() => setOpen((v) => !v)}\n aria-expanded={open}\n >\n <span>{label}</span>\n <ChevronDown\n size={12}\n className={cx(\n 'sft-sidebar__section-chevron',\n open && 'sft-sidebar__section-chevron--open',\n )}\n />\n </button>\n ) : (\n <div className=\"sft-sidebar__section-label\">{label}</div>\n ))}\n {!hidden && <div className=\"sft-sidebar__items\">{children}</div>}\n </div>\n );\n}\n\nexport interface SidebarItemProps {\n active?: boolean;\n icon?: ReactNode;\n /** label shown as a floating tooltip while the sidebar is collapsed */\n tooltip?: string;\n onClick?: () => void;\n children: ReactNode;\n}\n\nexport function SidebarItem({ active, icon, tooltip, onClick, children }: SidebarItemProps) {\n const [tip, setTip] = useState<{ top: number; left: number } | null>(null);\n\n const onEnter = useCallback((e: MouseEvent<HTMLButtonElement>) => {\n const r = e.currentTarget.getBoundingClientRect();\n setTip({ top: r.top + r.height / 2, left: r.right + 8 });\n }, []);\n const onLeave = useCallback(() => setTip(null), []);\n\n return (\n <button\n type=\"button\"\n className={cx('sft-sidebar__item', active && 'sft-sidebar__item--active')}\n aria-current={active ? 'page' : undefined}\n onClick={onClick}\n onMouseEnter={tooltip ? onEnter : undefined}\n onMouseLeave={tooltip ? onLeave : undefined}\n >\n {icon && <span className=\"sft-sidebar__item-icon\">{icon}</span>}\n <span className=\"sft-sidebar__item-label\">{children}</span>\n {tooltip &&\n tip &&\n createPortal(\n <div className=\"sft-sidebar__tooltip\" style={{ top: tip.top, left: tip.left }}>\n {tooltip}\n </div>,\n document.body,\n )}\n </button>\n );\n}\n","import type { ReactNode } from 'react';\nimport { cx } from '../util/cx';\n\nexport interface HeaderProps {\n /** far-left slot shown only on mobile (a hamburger that opens the drawer) */\n menuButton?: ReactNode;\n /** left side — brand / breadcrumb. Ignored if `children` is provided. */\n logo?: ReactNode;\n /** left side — plain title. Ignored if `logo` or `children` is provided. */\n title?: ReactNode;\n /** left side override (takes precedence over `logo` / `title`) */\n children?: ReactNode;\n /** center nav slot */\n nav?: ReactNode;\n /** right side — actions (search, theme toggle, links, …) */\n actions?: ReactNode;\n className?: string;\n}\n\n/**\n * Header — the top bar. A mobile menu button, then a brand/breadcrumb on the left,\n * an optional center nav, and an actions cluster pushed to the right. Ported from\n * akron-ui's Header.\n */\nexport function Header({\n menuButton,\n logo,\n title,\n children,\n nav,\n actions,\n className,\n}: HeaderProps) {\n const left =\n children ?? logo ?? (title != null ? <h1 className=\"sft-header__title\">{title}</h1> : null);\n\n return (\n <header className={cx('sft-header', className)}>\n {menuButton && <div className=\"sft-header__menu\">{menuButton}</div>}\n {left && <div className=\"sft-header__left\">{left}</div>}\n {nav && <nav className=\"sft-header__nav\">{nav}</nav>}\n {actions && <div className=\"sft-header__actions\">{actions}</div>}\n </header>\n );\n}\n","import { useCallback, useEffect, useState } from 'react';\n\nexport type Theme = 'light' | 'dark';\n\nconst STORAGE_KEY = 'softium.theme';\n\nfunction readStored(): Theme | null {\n if (typeof window === 'undefined') return null;\n try {\n const v = window.localStorage.getItem(STORAGE_KEY);\n return v === 'light' || v === 'dark' ? v : null;\n } catch {\n return null;\n }\n}\n\nexport interface UseThemeOptions {\n /** theme to use when nothing is stored. Default 'light'. */\n defaultTheme?: Theme;\n /** persist the choice to localStorage. Default true. */\n persist?: boolean;\n}\n\n/**\n * useTheme — applies `data-theme` to <html> and (optionally) persists the choice.\n * The actual re-skin is pure CSS (token swap), so this hook only flips the attribute.\n */\nexport function useTheme({ defaultTheme = 'light', persist = true }: UseThemeOptions = {}) {\n const [theme, setThemeState] = useState<Theme>(() => readStored() ?? defaultTheme);\n\n useEffect(() => {\n if (typeof document !== 'undefined') {\n document.documentElement.setAttribute('data-theme', theme);\n }\n if (persist && typeof window !== 'undefined') {\n try {\n window.localStorage.setItem(STORAGE_KEY, theme);\n } catch {\n // best-effort\n }\n }\n }, [theme, persist]);\n\n const setTheme = useCallback((next: Theme) => setThemeState(next), []);\n const toggleTheme = useCallback(\n () => setThemeState((t) => (t === 'light' ? 'dark' : 'light')),\n [],\n );\n\n return { theme, setTheme, toggleTheme };\n}\n","import { Moon, Sun } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { type Theme, useTheme } from '../hooks/useTheme';\nimport { Button } from './Button';\n\nexport interface ThemeToggleProps {\n /** optional controlled value; when omitted the component manages its own theme */\n theme?: Theme;\n onToggle?: () => void;\n /** shown in light mode (click → dark). Defaults to a moon icon. */\n labelLight?: ReactNode;\n /** shown in dark mode (click → light). Defaults to a sun icon. */\n labelDark?: ReactNode;\n}\n\n/**\n * ThemeToggle — flips light/dark. Uncontrolled by default (manages + persists its\n * own theme); pass `theme` + `onToggle` to control it from outside.\n */\nexport function ThemeToggle({\n theme: controlled,\n onToggle,\n labelLight = <Moon size={16} />,\n labelDark = <Sun size={16} />,\n}: ThemeToggleProps) {\n const internal = useTheme();\n const theme = controlled ?? internal.theme;\n const toggle = onToggle ?? internal.toggleTheme;\n\n return (\n <Button variant=\"ghost\" size=\"sm\" onClick={toggle} aria-label=\"Toggle theme\">\n {theme === 'light' ? labelLight : labelDark}\n </Button>\n );\n}\n","import { type ReactNode, useId } from 'react';\nimport { cx } from '../util/cx';\n\nexport interface SwitchProps {\n checked: boolean;\n onChange: (checked: boolean) => void;\n /** text shown next to the switch */\n label?: ReactNode;\n disabled?: boolean;\n className?: string;\n}\n\n/**\n * Switch — an independent on/off toggle. Built on a real checkbox (role=\"switch\")\n * so it's keyboard- and screen-reader-accessible.\n */\nexport function Switch({ checked, onChange, label, disabled, className }: SwitchProps) {\n const id = useId();\n return (\n <label htmlFor={id} className={cx('sft-switch', disabled && 'sft-switch--disabled', className)}>\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n aria-checked={checked}\n className=\"sft-switch__input\"\n checked={checked}\n disabled={disabled}\n onChange={(e) => onChange(e.target.checked)}\n />\n <span className=\"sft-switch__track\" aria-hidden=\"true\">\n <span className=\"sft-switch__thumb\" />\n </span>\n {label != null && <span className=\"sft-switch__label\">{label}</span>}\n </label>\n );\n}\n","/**\n * @softium/ui\n *\n * General React primitives + app-shell layout for softium-ui.\n * Remember to import the stylesheet once:\n * import '@softium/ui/styles.css';\n */\n\nexport const VERSION = '0.0.0';\n\nexport { Button } from './components/Button';\nexport type { ButtonProps, ButtonSize, ButtonVariant } from './components/Button';\n\nexport { AppShell } from './components/AppShell';\nexport type { AppShellProps } from './components/AppShell';\n\nexport {\n Sidebar,\n SidebarBrand,\n SidebarItem,\n SidebarSection,\n} from './components/Sidebar';\nexport type {\n SidebarBrandProps,\n SidebarItemProps,\n SidebarProps,\n SidebarSectionProps,\n} from './components/Sidebar';\n\nexport { Header } from './components/Header';\nexport type { HeaderProps } from './components/Header';\n\nexport { ThemeToggle } from './components/ThemeToggle';\nexport type { ThemeToggleProps } from './components/ThemeToggle';\n\nexport { Switch } from './components/Switch';\nexport type { SwitchProps } from './components/Switch';\n\nexport { useTheme } from './hooks/useTheme';\nexport type { Theme, UseThemeOptions } from './hooks/useTheme';\n"]}