najm-kit 2.8.2 → 2.9.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,75 @@
1
+ // src/server/uiBootstrap.ts
2
+ var MISSING = Symbol("najm-kit/server: missing envelope");
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null;
5
+ }
6
+ function selectData(payload) {
7
+ return isRecord(payload) && "data" in payload ? payload.data : MISSING;
8
+ }
9
+ function describeThrown(value) {
10
+ return value instanceof Error ? `${value.name}: ${value.message}` : `non-error thrown: ${typeof value}`;
11
+ }
12
+ function createUiBootstrapLoader(config) {
13
+ const { fetcher, resources, onDiagnostic } = config;
14
+ const selectEnvelope = config.select ?? selectData;
15
+ const names = Object.keys(resources);
16
+ const report = (diagnostic) => {
17
+ if (!onDiagnostic) return;
18
+ try {
19
+ onDiagnostic(diagnostic);
20
+ } catch {
21
+ }
22
+ };
23
+ async function loadOne(name) {
24
+ const resource = resources[name];
25
+ const { path } = resource;
26
+ const select = resource.select ?? selectEnvelope;
27
+ const fail = (reason, detail = {}) => {
28
+ report({ resource: name, reason, path, ...detail });
29
+ return resource.fallback();
30
+ };
31
+ let response;
32
+ try {
33
+ response = await fetcher(path);
34
+ } catch (cause) {
35
+ return fail("fetch-failed", { error: describeThrown(cause) });
36
+ }
37
+ if (!response.ok) return fail("response-not-ok", { status: response.status });
38
+ let payload;
39
+ try {
40
+ payload = await response.json();
41
+ } catch (cause) {
42
+ return fail("invalid-json", { error: describeThrown(cause) });
43
+ }
44
+ let data;
45
+ try {
46
+ data = select(payload);
47
+ } catch (cause) {
48
+ return fail("invalid-envelope", { error: describeThrown(cause) });
49
+ }
50
+ if (data === MISSING) return fail("invalid-envelope");
51
+ try {
52
+ const parsed = resource.parse(data);
53
+ if (parsed !== void 0) return parsed;
54
+ } catch (cause) {
55
+ return fail("invalid-payload", { error: describeThrown(cause) });
56
+ }
57
+ return fail("invalid-payload");
58
+ }
59
+ const loaders = Object.fromEntries(
60
+ names.map((name) => [name, () => loadOne(name)])
61
+ );
62
+ async function load() {
63
+ const pending = names.map((name) => loadOne(name));
64
+ const settled = await Promise.all(pending);
65
+ const snapshot = {};
66
+ names.forEach((name, index) => {
67
+ snapshot[name] = settled[index];
68
+ });
69
+ Object.freeze(snapshot);
70
+ return snapshot;
71
+ }
72
+ return { load, loadResource: (name) => loaders[name](), loaders };
73
+ }
74
+
75
+ export { createUiBootstrapLoader };
@@ -0,0 +1,404 @@
1
+ import { useNajmComponentStyle, cn } from './chunk-KVZACF4G.mjs';
2
+ import { resolveVariantAlias, resolveRadiusValue } from './chunk-TFHWLE7N.mjs';
3
+ import * as React2 from 'react';
4
+ import React2__default, { createContext, useContext } from 'react';
5
+ import * as LucideIcons from 'lucide-react';
6
+ import { LoaderCircleIcon } from 'lucide-react';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+ import { Slot } from '@radix-ui/react-slot';
9
+ import { cva } from 'class-variance-authority';
10
+ import { OverlayScrollbars } from 'overlayscrollbars';
11
+ import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
12
+
13
+ function toPascalCase(value) {
14
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
15
+ }
16
+ function isImageSource(value) {
17
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("http://") || value.startsWith("https://") || value.startsWith("data:image/") || /\.(avif|gif|jpe?g|png|svg|webp)(\?.*)?$/i.test(value);
18
+ }
19
+ function isComponentSource(value) {
20
+ return typeof value === "function" || Boolean(value && typeof value === "object" && "$$typeof" in value);
21
+ }
22
+ var NIcon = ({
23
+ icon = "circle",
24
+ size = 24,
25
+ alt = "",
26
+ className,
27
+ style,
28
+ onClick,
29
+ ...rest
30
+ }) => {
31
+ const dimensionStyle = {
32
+ width: size,
33
+ height: size,
34
+ ...style
35
+ };
36
+ if (React2__default.isValidElement(icon)) {
37
+ return React2__default.cloneElement(icon, {
38
+ className: cn(className, icon.props.className),
39
+ onClick,
40
+ ...rest
41
+ });
42
+ }
43
+ if (icon && typeof icon === "object" && "src" in icon) {
44
+ return /* @__PURE__ */ jsx(
45
+ "img",
46
+ {
47
+ alt: icon.alt ?? alt,
48
+ className: cn("inline-block shrink-0 object-contain", className),
49
+ src: icon.src,
50
+ style: dimensionStyle,
51
+ onClick,
52
+ ...rest
53
+ }
54
+ );
55
+ }
56
+ if (isComponentSource(icon)) {
57
+ const IconComponent = icon;
58
+ return /* @__PURE__ */ jsx(IconComponent, { size, className, onClick, style, ...rest });
59
+ }
60
+ if (typeof icon === "string") {
61
+ if (isImageSource(icon)) {
62
+ return /* @__PURE__ */ jsx(
63
+ "img",
64
+ {
65
+ alt,
66
+ className: cn("inline-block shrink-0 object-contain", className),
67
+ src: icon,
68
+ style: dimensionStyle,
69
+ onClick,
70
+ ...rest
71
+ }
72
+ );
73
+ }
74
+ const pascalCaseIcon = toPascalCase(icon);
75
+ if (LucideIcons[pascalCaseIcon]) {
76
+ const LucideIcon = LucideIcons[pascalCaseIcon];
77
+ return /* @__PURE__ */ jsx(LucideIcon, { size, className, onClick, style, ...rest });
78
+ }
79
+ return null;
80
+ }
81
+ return null;
82
+ };
83
+ NIcon.displayName = "NIcon";
84
+
85
+ // src/theme/borders.ts
86
+ var SURFACE_BORDER_CLASSES = {
87
+ all: "najm-border border-border",
88
+ top: "najm-border-t border-border",
89
+ right: "najm-border-r border-border",
90
+ bottom: "najm-border-b border-border",
91
+ left: "najm-border-l border-border"
92
+ };
93
+ var SIDEBAR_BORDER_CLASSES = {
94
+ all: "najm-border border-sidebar-border",
95
+ top: "najm-border-t border-sidebar-border",
96
+ right: "najm-border-r border-sidebar-border",
97
+ bottom: "najm-border-b border-sidebar-border",
98
+ left: "najm-border-l border-sidebar-border"
99
+ };
100
+ var BORDER_RESET_CLASSES = {
101
+ all: "border-0",
102
+ top: "border-t-0",
103
+ right: "border-r-0",
104
+ bottom: "border-b-0",
105
+ left: "border-l-0"
106
+ };
107
+ function surfaceBorderClasses(bordered = true, side = "all") {
108
+ return bordered === false ? BORDER_RESET_CLASSES[side] : SURFACE_BORDER_CLASSES[side];
109
+ }
110
+ function sidebarBorderClasses(bordered = true, side = "all") {
111
+ return bordered === false ? BORDER_RESET_CLASSES[side] : SIDEBAR_BORDER_CLASSES[side];
112
+ }
113
+ function inputBorderClasses(bordered = true) {
114
+ return bordered === false ? "border-0" : "najm-border border-input";
115
+ }
116
+ var buttonVariants = cva(
117
+ [
118
+ "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-medium outline-none transition-all",
119
+ "disabled:pointer-events-none disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
120
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
121
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
122
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
123
+ ].join(" "),
124
+ {
125
+ variants: {
126
+ variant: {
127
+ default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
128
+ destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
129
+ outline: "border border-input bg-transparent text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground",
130
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
131
+ tertiary: "bg-tertiary text-tertiary-foreground shadow-xs hover:bg-tertiary/80",
132
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
133
+ link: "h-auto p-0 text-primary underline-offset-4 hover:underline",
134
+ success: "bg-emerald-600 text-white shadow-xs hover:bg-emerald-700",
135
+ warning: "bg-amber-400 text-slate-950 shadow-xs hover:bg-amber-500",
136
+ info: "bg-sky-600 text-white shadow-xs hover:bg-sky-700",
137
+ soft: "bg-primary/10 text-primary shadow-none hover:bg-primary/15",
138
+ subtle: "bg-muted text-foreground shadow-none hover:bg-muted/80",
139
+ plain: "bg-transparent shadow-none hover:bg-transparent"
140
+ },
141
+ size: {
142
+ "2xs": "h-5 gap-1 px-1.5 text-[11px] [&_svg:not([class*='size-'])]:size-3",
143
+ xs: "h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
144
+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
145
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
146
+ md: "h-9 px-4 py-2 has-[>svg]:px-3",
147
+ lg: "h-10 px-6 has-[>svg]:px-4",
148
+ xl: "h-12 px-8 text-base [&_svg:not([class*='size-'])]:size-5",
149
+ "2xl": "h-14 px-10 text-base [&_svg:not([class*='size-'])]:size-5",
150
+ icon: "size-9",
151
+ "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
152
+ "icon-sm": "size-8",
153
+ "icon-lg": "size-10 [&_svg:not([class*='size-'])]:size-5",
154
+ "icon-xl": "size-12 [&_svg:not([class*='size-'])]:size-5"
155
+ },
156
+ rounded: {
157
+ none: "rounded-none",
158
+ sm: "rounded-sm",
159
+ default: "rounded-md",
160
+ md: "rounded-md",
161
+ lg: "rounded-lg",
162
+ xl: "rounded-xl",
163
+ "2xl": "rounded-2xl",
164
+ full: "rounded-full"
165
+ },
166
+ fullWidth: {
167
+ true: "w-full",
168
+ false: ""
169
+ }
170
+ },
171
+ defaultVariants: {
172
+ variant: "default",
173
+ size: "default",
174
+ rounded: "default",
175
+ fullWidth: false
176
+ }
177
+ }
178
+ );
179
+ function isPromiseLike(value) {
180
+ return Boolean(value && typeof value.then === "function");
181
+ }
182
+ var defaultLoader = /* @__PURE__ */ jsx(LoaderCircleIcon, { "aria-hidden": "true", className: "animate-spin" });
183
+ function renderIcon(icon) {
184
+ if (!icon) return null;
185
+ return /* @__PURE__ */ jsx(NIcon, { "aria-hidden": "true", icon, className: "shrink-0" });
186
+ }
187
+ var Button = React2.forwardRef(
188
+ ({
189
+ className,
190
+ variant,
191
+ size,
192
+ rounded,
193
+ fullWidth,
194
+ asChild = false,
195
+ loading = false,
196
+ autoLoading = true,
197
+ disabledWhileLoading = true,
198
+ loadingText,
199
+ loader = defaultLoader,
200
+ loaderPosition = "left",
201
+ leftIcon,
202
+ rightIcon,
203
+ bordered,
204
+ disabled,
205
+ children,
206
+ onClick,
207
+ style,
208
+ ...props
209
+ }, ref) => {
210
+ const recipe = useNajmComponentStyle("button");
211
+ const aliased = resolveVariantAlias(
212
+ recipe?.variants,
213
+ variant ?? recipe?.defaultVariant ?? "default"
214
+ );
215
+ const effVariant = variant ?? aliased.variant ?? recipe?.defaultVariant;
216
+ const effSize = size ?? recipe?.defaultSize;
217
+ const recipeRadius = rounded === void 0 ? resolveRadiusValue(recipe?.radius) : void 0;
218
+ const recipeStyle = recipeRadius ? { borderRadius: recipeRadius } : void 0;
219
+ const [pending, setPending] = React2.useState(false);
220
+ const isLoading = loading || pending;
221
+ const isDisabled = disabled || disabledWhileLoading && isLoading;
222
+ const Comp = asChild ? Slot : "button";
223
+ const handleClick = React2.useCallback(
224
+ (event) => {
225
+ if (isDisabled) {
226
+ event.preventDefault();
227
+ return;
228
+ }
229
+ const result = onClick?.(event);
230
+ if (autoLoading && isPromiseLike(result)) {
231
+ setPending(true);
232
+ void result.then(
233
+ () => setPending(false),
234
+ () => setPending(false)
235
+ );
236
+ }
237
+ },
238
+ [autoLoading, isDisabled, onClick]
239
+ );
240
+ const child = asChild ? React2.Children.only(children) : null;
241
+ const childContent = asChild && React2.isValidElement(child) ? child.props.children : children;
242
+ const content = loadingText && isLoading ? loadingText : childContent;
243
+ const loaderNode = isLoading ? loader : null;
244
+ const leftIconNode = renderIcon(leftIcon);
245
+ const rightIconNode = renderIcon(rightIcon);
246
+ const showCenteredLoader = Boolean(loaderNode && loaderPosition === "center");
247
+ const renderedContent = showCenteredLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
248
+ /* @__PURE__ */ jsxs("span", { className: "invisible inline-flex items-center gap-2", children: [
249
+ leftIconNode,
250
+ content,
251
+ rightIconNode
252
+ ] }),
253
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 inline-flex items-center justify-center", children: loaderNode })
254
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
255
+ loaderNode && loaderPosition === "left" ? loaderNode : leftIconNode,
256
+ content,
257
+ loaderNode && loaderPosition === "right" ? loaderNode : rightIconNode
258
+ ] });
259
+ const renderedChild = asChild && React2.isValidElement(child) ? React2.cloneElement(child, void 0, renderedContent) : renderedContent;
260
+ return /* @__PURE__ */ jsx(
261
+ Comp,
262
+ {
263
+ ref,
264
+ "data-slot": "button",
265
+ "data-loading": isLoading || void 0,
266
+ "data-disabled": isDisabled || void 0,
267
+ "data-bordered": effVariant === "outline" ? "true" : bordered === false ? "false" : bordered ? "true" : void 0,
268
+ "aria-busy": isLoading || void 0,
269
+ "aria-disabled": asChild && isDisabled ? true : void 0,
270
+ disabled: !asChild ? isDisabled : void 0,
271
+ style: recipeStyle ? { ...recipeStyle, ...style } : style,
272
+ className: cn(
273
+ buttonVariants({ variant: effVariant, size: effSize, rounded, fullWidth }),
274
+ aliased.style?.className,
275
+ // Outline buttons always carry an input-style border.
276
+ effVariant === "outline" && inputBorderClasses(true),
277
+ // Filled/ghost/plain/link/success/warning/info/soft/subtle buttons only get a
278
+ // border when the consumer explicitly opts in via `bordered`. Global theming
279
+ // should not outline every filled button.
280
+ effVariant !== "outline" && bordered === true && cn(
281
+ "border",
282
+ "border-muted-foreground"
283
+ ),
284
+ className
285
+ ),
286
+ onClick: handleClick,
287
+ ...props,
288
+ children: renderedChild
289
+ }
290
+ );
291
+ }
292
+ );
293
+ Button.displayName = "Button";
294
+ var NButton = Button;
295
+ function assignRef(ref, node) {
296
+ if (!ref) return;
297
+ if (typeof ref === "function") ref(node);
298
+ else ref.current = node;
299
+ }
300
+ function applyViewportLayout(node, axis) {
301
+ node.style.width = "100%";
302
+ node.style.height = "100%";
303
+ node.style.minWidth = "0";
304
+ node.style.minHeight = "0";
305
+ node.style.overflowX = axis === "x" || axis === "both" ? "auto" : "hidden";
306
+ node.style.overflowY = axis === "y" || axis === "both" ? "auto" : "hidden";
307
+ }
308
+ function najmScrollOptions(axis, autoHide, options) {
309
+ return {
310
+ scrollbars: { theme: "os-theme-najm", autoHide, autoHideDelay: 500, clickScroll: true },
311
+ overflow: {
312
+ x: axis === "x" || axis === "both" ? "scroll" : "hidden",
313
+ y: axis === "y" || axis === "both" ? "scroll" : "hidden"
314
+ },
315
+ ...options
316
+ };
317
+ }
318
+ function useNajmScrollViewport({
319
+ axis = "y",
320
+ autoHide = "never",
321
+ options
322
+ } = {}) {
323
+ const hostRef = React2.useRef(null);
324
+ const viewportRef = React2.useRef(null);
325
+ React2.useEffect(() => {
326
+ const target = hostRef.current;
327
+ const viewport = viewportRef.current;
328
+ if (!target || !viewport) return;
329
+ const instance = OverlayScrollbars(
330
+ { target, elements: { viewport } },
331
+ najmScrollOptions(axis, autoHide, options)
332
+ );
333
+ const containWheel = (event) => event.stopPropagation();
334
+ viewport.addEventListener("wheel", containWheel, { passive: true });
335
+ return () => {
336
+ viewport.removeEventListener("wheel", containWheel);
337
+ instance.destroy();
338
+ };
339
+ }, [axis, autoHide, options]);
340
+ return { hostRef, viewportRef };
341
+ }
342
+ function NajmScroll({ className, axis = "y", autoHide = "never", viewportRef, events, options, element, children, style, ...props }) {
343
+ return /* @__PURE__ */ jsx(
344
+ OverlayScrollbarsComponent,
345
+ {
346
+ className: cn(className),
347
+ style: {
348
+ ...style,
349
+ overflow: "hidden",
350
+ minHeight: 0,
351
+ minWidth: 0
352
+ },
353
+ element,
354
+ defer: true,
355
+ options: najmScrollOptions(axis, autoHide, options || void 0),
356
+ events: {
357
+ ...events,
358
+ initialized: (instance, ...rest) => {
359
+ const viewport = instance.elements().viewport;
360
+ applyViewportLayout(viewport, axis);
361
+ assignRef(viewportRef, viewport);
362
+ events?.initialized?.(instance, ...rest);
363
+ },
364
+ updated: (instance, ...rest) => {
365
+ applyViewportLayout(instance.elements().viewport, axis);
366
+ events?.updated?.(instance, ...rest);
367
+ },
368
+ destroyed: (instance, ...rest) => {
369
+ assignRef(viewportRef, null);
370
+ events?.destroyed?.(instance, ...rest);
371
+ }
372
+ },
373
+ ...props,
374
+ children
375
+ }
376
+ );
377
+ }
378
+ var TableStoreContext = createContext(null);
379
+ var useTableStore = { use: {} };
380
+ var handler = {
381
+ get: (_, prop) => () => {
382
+ const store = useContext(TableStoreContext);
383
+ if (!store) throw new Error("useTableStore must be used within NTable");
384
+ return store.use[prop]();
385
+ }
386
+ };
387
+ useTableStore.use = new Proxy({}, handler);
388
+ function formatJsonValue(value) {
389
+ if (typeof value === "string") return value;
390
+ try {
391
+ return JSON.stringify(value ?? null, null, 2) ?? String(value);
392
+ } catch {
393
+ return String(value);
394
+ }
395
+ }
396
+ function NTableJson() {
397
+ const viewMode = useTableStore.use.viewMode();
398
+ const renderJson = useTableStore.use.renderJson();
399
+ const jsonValue = useTableStore.use.jsonValue();
400
+ if (viewMode !== "json") return null;
401
+ return /* @__PURE__ */ jsx("div", { className: "flex-1 flex flex-col min-h-0 overflow-hidden", children: renderJson?.() ?? /* @__PURE__ */ jsx(NajmScroll, { axis: "both", className: "h-full min-h-0 rounded-md border border-border bg-muted/40", children: /* @__PURE__ */ jsx("pre", { className: "p-4 font-mono text-xs leading-relaxed text-foreground", children: formatJsonValue(jsonValue) }) }) });
402
+ }
403
+
404
+ export { Button, NButton, NIcon, NTableJson, NajmScroll, TableStoreContext, buttonVariants, inputBorderClasses, sidebarBorderClasses, surfaceBorderClasses, useNajmScrollViewport, useTableStore };