@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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.cjs +277 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +171 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +265 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +764 -0
- package/package.json +70 -0
- package/src/styles/ui.css +558 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sunghoon Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @softium/ui
|
|
2
|
+
|
|
3
|
+
General React primitives and app-shell layout for softium-ui: `Button`,
|
|
4
|
+
`AppShell` (responsive sidebar + header with a mobile drawer), `Sidebar`,
|
|
5
|
+
`Header`, `ThemeToggle`, and a `useTheme` hook.
|
|
6
|
+
|
|
7
|
+
Part of [softium-ui](https://github.com/HOONY-LEE/softium-ui).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @softium/ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`react` and `react-dom` (`^18` or `^19`) are peer dependencies.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import '@softium/ui/styles.css';
|
|
21
|
+
import { AppShell, Button, ThemeToggle } from '@softium/ui';
|
|
22
|
+
|
|
23
|
+
export function App() {
|
|
24
|
+
return (
|
|
25
|
+
<AppShell header={<ThemeToggle />} sidebar={/* nav */ null}>
|
|
26
|
+
<Button variant="primary">Save</Button>
|
|
27
|
+
</AppShell>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Theming
|
|
33
|
+
|
|
34
|
+
Plain CSS custom properties (`--sft-*`). Dark mode via `prefers-color-scheme`
|
|
35
|
+
or `<html data-theme="dark">`; `ThemeToggle` / `useTheme` flip the explicit
|
|
36
|
+
attribute. The stylesheet inlines the shared tokens, so this import is
|
|
37
|
+
self-contained.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT © Sunghoon Lee
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var lucideReact = require('lucide-react');
|
|
7
|
+
var reactDom = require('react-dom');
|
|
8
|
+
|
|
9
|
+
// src/components/Button.tsx
|
|
10
|
+
|
|
11
|
+
// src/util/cx.ts
|
|
12
|
+
function cx(...parts) {
|
|
13
|
+
return parts.filter(Boolean).join(" ");
|
|
14
|
+
}
|
|
15
|
+
var Button = react.forwardRef(function Button2({
|
|
16
|
+
variant = "secondary",
|
|
17
|
+
size = "md",
|
|
18
|
+
iconLeft,
|
|
19
|
+
iconRight,
|
|
20
|
+
block,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
type,
|
|
24
|
+
...rest
|
|
25
|
+
}, ref) {
|
|
26
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
27
|
+
"button",
|
|
28
|
+
{
|
|
29
|
+
ref,
|
|
30
|
+
type: type ?? "button",
|
|
31
|
+
className: cx(
|
|
32
|
+
"sft-button",
|
|
33
|
+
`sft-button--${variant}`,
|
|
34
|
+
`sft-button--${size}`,
|
|
35
|
+
block && "sft-button--block",
|
|
36
|
+
className
|
|
37
|
+
),
|
|
38
|
+
...rest,
|
|
39
|
+
children: [
|
|
40
|
+
iconLeft && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-button__icon", children: iconLeft }),
|
|
41
|
+
children != null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-button__label", children }),
|
|
42
|
+
iconRight && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-button__icon", children: iconRight })
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
function AppShell({
|
|
48
|
+
sidebar,
|
|
49
|
+
header,
|
|
50
|
+
children,
|
|
51
|
+
collapsed,
|
|
52
|
+
sidebarWidth = 224,
|
|
53
|
+
sidebarOpen = false,
|
|
54
|
+
onSidebarClose,
|
|
55
|
+
className
|
|
56
|
+
}) {
|
|
57
|
+
const vars = { "--sft-sidebar-width": `${sidebarWidth}px` };
|
|
58
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
className: cx("sft-app sft-shell", className),
|
|
62
|
+
style: vars,
|
|
63
|
+
"data-collapsed": collapsed || void 0,
|
|
64
|
+
"data-drawer-open": sidebarOpen || void 0,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsxRuntime.jsx("aside", { className: "sft-shell__sidebar", children: sidebar }),
|
|
67
|
+
sidebarOpen && onSidebarClose && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-shell__overlay", onClick: onSidebarClose, "aria-hidden": "true" }),
|
|
68
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sft-shell__main", children: [
|
|
69
|
+
header && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-shell__header", children: header }),
|
|
70
|
+
/* @__PURE__ */ jsxRuntime.jsx("main", { className: "sft-shell__content", children })
|
|
71
|
+
] })
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function Sidebar({
|
|
77
|
+
header,
|
|
78
|
+
footer,
|
|
79
|
+
collapsed = false,
|
|
80
|
+
onCollapse,
|
|
81
|
+
onExpand,
|
|
82
|
+
children,
|
|
83
|
+
className
|
|
84
|
+
}) {
|
|
85
|
+
const toggle = collapsed ? onExpand : onCollapse;
|
|
86
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("sft-sidebar", collapsed && "sft-sidebar--collapsed", className), children: [
|
|
87
|
+
(header || toggle) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sft-sidebar__header", children: [
|
|
88
|
+
header && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-sidebar__header-content", children: header }),
|
|
89
|
+
toggle && /* @__PURE__ */ jsxRuntime.jsx(
|
|
90
|
+
"button",
|
|
91
|
+
{
|
|
92
|
+
type: "button",
|
|
93
|
+
className: "sft-sidebar__collapse",
|
|
94
|
+
onClick: toggle,
|
|
95
|
+
"aria-label": collapsed ? "expand sidebar" : "collapse sidebar",
|
|
96
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
lucideReact.ChevronLeft,
|
|
98
|
+
{
|
|
99
|
+
size: 16,
|
|
100
|
+
className: cx(
|
|
101
|
+
"sft-sidebar__collapse-icon",
|
|
102
|
+
collapsed && "sft-sidebar__collapse-icon--flipped"
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsxRuntime.jsx("nav", { className: "sft-sidebar__content", children }),
|
|
110
|
+
footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-sidebar__footer", children: footer })
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
function SidebarBrand({ logo, children, subtitle }) {
|
|
114
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sft-sidebar__brand", children: [
|
|
115
|
+
logo && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-sidebar__logo", children: logo }),
|
|
116
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sft-sidebar__brand-text", children: [
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-sidebar__title", children }),
|
|
118
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-sidebar__subtitle", children: subtitle })
|
|
119
|
+
] })
|
|
120
|
+
] });
|
|
121
|
+
}
|
|
122
|
+
function SidebarSection({
|
|
123
|
+
label,
|
|
124
|
+
collapsible = false,
|
|
125
|
+
defaultOpen = true,
|
|
126
|
+
children
|
|
127
|
+
}) {
|
|
128
|
+
const [open, setOpen] = react.useState(defaultOpen);
|
|
129
|
+
const hidden = collapsible && !open;
|
|
130
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sft-sidebar__section", children: [
|
|
131
|
+
label && (collapsible ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
className: "sft-sidebar__section-toggle",
|
|
136
|
+
onClick: () => setOpen((v) => !v),
|
|
137
|
+
"aria-expanded": open,
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
|
|
140
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
141
|
+
lucideReact.ChevronDown,
|
|
142
|
+
{
|
|
143
|
+
size: 12,
|
|
144
|
+
className: cx(
|
|
145
|
+
"sft-sidebar__section-chevron",
|
|
146
|
+
open && "sft-sidebar__section-chevron--open"
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-sidebar__section-label", children: label })),
|
|
153
|
+
!hidden && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-sidebar__items", children })
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
function SidebarItem({ active, icon, tooltip, onClick, children }) {
|
|
157
|
+
const [tip, setTip] = react.useState(null);
|
|
158
|
+
const onEnter = react.useCallback((e) => {
|
|
159
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
160
|
+
setTip({ top: r.top + r.height / 2, left: r.right + 8 });
|
|
161
|
+
}, []);
|
|
162
|
+
const onLeave = react.useCallback(() => setTip(null), []);
|
|
163
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
164
|
+
"button",
|
|
165
|
+
{
|
|
166
|
+
type: "button",
|
|
167
|
+
className: cx("sft-sidebar__item", active && "sft-sidebar__item--active"),
|
|
168
|
+
"aria-current": active ? "page" : void 0,
|
|
169
|
+
onClick,
|
|
170
|
+
onMouseEnter: tooltip ? onEnter : void 0,
|
|
171
|
+
onMouseLeave: tooltip ? onLeave : void 0,
|
|
172
|
+
children: [
|
|
173
|
+
icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-sidebar__item-icon", children: icon }),
|
|
174
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-sidebar__item-label", children }),
|
|
175
|
+
tooltip && tip && reactDom.createPortal(
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-sidebar__tooltip", style: { top: tip.top, left: tip.left }, children: tooltip }),
|
|
177
|
+
document.body
|
|
178
|
+
)
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function Header({
|
|
184
|
+
menuButton,
|
|
185
|
+
logo,
|
|
186
|
+
title,
|
|
187
|
+
children,
|
|
188
|
+
nav,
|
|
189
|
+
actions,
|
|
190
|
+
className
|
|
191
|
+
}) {
|
|
192
|
+
const left = children ?? logo ?? (title != null ? /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "sft-header__title", children: title }) : null);
|
|
193
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("header", { className: cx("sft-header", className), children: [
|
|
194
|
+
menuButton && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-header__menu", children: menuButton }),
|
|
195
|
+
left && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-header__left", children: left }),
|
|
196
|
+
nav && /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "sft-header__nav", children: nav }),
|
|
197
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sft-header__actions", children: actions })
|
|
198
|
+
] });
|
|
199
|
+
}
|
|
200
|
+
var STORAGE_KEY = "softium.theme";
|
|
201
|
+
function readStored() {
|
|
202
|
+
if (typeof window === "undefined") return null;
|
|
203
|
+
try {
|
|
204
|
+
const v = window.localStorage.getItem(STORAGE_KEY);
|
|
205
|
+
return v === "light" || v === "dark" ? v : null;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function useTheme({ defaultTheme = "light", persist = true } = {}) {
|
|
211
|
+
const [theme, setThemeState] = react.useState(() => readStored() ?? defaultTheme);
|
|
212
|
+
react.useEffect(() => {
|
|
213
|
+
if (typeof document !== "undefined") {
|
|
214
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
215
|
+
}
|
|
216
|
+
if (persist && typeof window !== "undefined") {
|
|
217
|
+
try {
|
|
218
|
+
window.localStorage.setItem(STORAGE_KEY, theme);
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}, [theme, persist]);
|
|
223
|
+
const setTheme = react.useCallback((next) => setThemeState(next), []);
|
|
224
|
+
const toggleTheme = react.useCallback(
|
|
225
|
+
() => setThemeState((t) => t === "light" ? "dark" : "light"),
|
|
226
|
+
[]
|
|
227
|
+
);
|
|
228
|
+
return { theme, setTheme, toggleTheme };
|
|
229
|
+
}
|
|
230
|
+
function ThemeToggle({
|
|
231
|
+
theme: controlled,
|
|
232
|
+
onToggle,
|
|
233
|
+
labelLight = /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Moon, { size: 16 }),
|
|
234
|
+
labelDark = /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sun, { size: 16 })
|
|
235
|
+
}) {
|
|
236
|
+
const internal = useTheme();
|
|
237
|
+
const theme = controlled ?? internal.theme;
|
|
238
|
+
const toggle = onToggle ?? internal.toggleTheme;
|
|
239
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", onClick: toggle, "aria-label": "Toggle theme", children: theme === "light" ? labelLight : labelDark });
|
|
240
|
+
}
|
|
241
|
+
function Switch({ checked, onChange, label, disabled, className }) {
|
|
242
|
+
const id = react.useId();
|
|
243
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: id, className: cx("sft-switch", disabled && "sft-switch--disabled", className), children: [
|
|
244
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
245
|
+
"input",
|
|
246
|
+
{
|
|
247
|
+
id,
|
|
248
|
+
type: "checkbox",
|
|
249
|
+
role: "switch",
|
|
250
|
+
"aria-checked": checked,
|
|
251
|
+
className: "sft-switch__input",
|
|
252
|
+
checked,
|
|
253
|
+
disabled,
|
|
254
|
+
onChange: (e) => onChange(e.target.checked)
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-switch__track", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-switch__thumb" }) }),
|
|
258
|
+
label != null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sft-switch__label", children: label })
|
|
259
|
+
] });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/index.ts
|
|
263
|
+
var VERSION = "0.0.0";
|
|
264
|
+
|
|
265
|
+
exports.AppShell = AppShell;
|
|
266
|
+
exports.Button = Button;
|
|
267
|
+
exports.Header = Header;
|
|
268
|
+
exports.Sidebar = Sidebar;
|
|
269
|
+
exports.SidebarBrand = SidebarBrand;
|
|
270
|
+
exports.SidebarItem = SidebarItem;
|
|
271
|
+
exports.SidebarSection = SidebarSection;
|
|
272
|
+
exports.Switch = Switch;
|
|
273
|
+
exports.ThemeToggle = ThemeToggle;
|
|
274
|
+
exports.VERSION = VERSION;
|
|
275
|
+
exports.useTheme = useTheme;
|
|
276
|
+
//# sourceMappingURL=index.cjs.map
|
|
277
|
+
//# sourceMappingURL=index.cjs.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":["forwardRef","Button","jsxs","jsx","ChevronLeft","useState","ChevronDown","useCallback","createPortal","useEffect","Moon","Sun","useId"],"mappings":"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","file":"index.cjs","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|