@nimblebrain/synapse 0.9.0 → 0.10.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.
@@ -43,7 +43,7 @@ interface CardProps extends HTMLAttributes<HTMLDivElement> {
43
43
  }
44
44
  declare function Card({ padding, interactive, style, className, children, ...rest }: CardProps): react_jsx_runtime.JSX.Element;
45
45
 
46
- type Side$1 = "left" | "right";
46
+ type Side$1 = "left" | "right" | "bottom";
47
47
  interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "title" | "onCancel" | "onClick"> {
48
48
  open: boolean;
49
49
  /** Called by the backdrop and the Header close button. */
@@ -52,17 +52,26 @@ interface DrawerProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "tit
52
52
  * (e.g. pop one level of a panel stack before closing). */
53
53
  onEscape?: () => void;
54
54
  side?: Side$1;
55
- /** Panel width. Default 480. */
55
+ /** Panel width (ignored for `side="bottom"`, which is full width). Default 480. */
56
56
  width?: number | string;
57
57
  children?: ReactNode;
58
58
  }
59
59
  declare function DrawerRoot({ open, onClose, onEscape, side, width, style, children, ...rest }: DrawerProps): react_jsx_runtime.JSX.Element;
60
- interface HeaderProps extends HTMLAttributes<HTMLElement> {
60
+ interface HeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
61
+ /**
62
+ * Heading text — rendered as an `<h2>` and wired as the drawer's accessible
63
+ * name. Renders **instead of** `children` (pass one or the other, not both).
64
+ */
65
+ title?: ReactNode;
66
+ /** Show a leading back button (e.g. pop one level of a panel stack). */
67
+ onBack?: () => void;
68
+ /** Trailing controls placed before the close button. */
69
+ actions?: ReactNode;
61
70
  /** Show a close button that calls this. */
62
71
  onClose?: () => void;
63
72
  children?: ReactNode;
64
73
  }
65
- declare function Header$1({ onClose, style, children, ...rest }: HeaderProps): react_jsx_runtime.JSX.Element;
74
+ declare function Header$1({ title, onBack, actions, onClose, style, children, ...rest }: HeaderProps): react_jsx_runtime.JSX.Element;
66
75
  declare function Body$1({ style, children, ...rest }: HTMLAttributes<HTMLDivElement>): react_jsx_runtime.JSX.Element;
67
76
  declare function Footer$1({ style, children, ...rest }: HTMLAttributes<HTMLElement>): react_jsx_runtime.JSX.Element;
68
77
  /** `Drawer` with `.Header`, `.Body`, `.Footer` slots. */
package/dist/ui/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useRef, useEffect, useContext, useState, useLayoutEffect } from 'react';
1
+ import { createContext, useRef, useId, useState, useEffect, useMemo, useContext, useLayoutEffect } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/ui/components/Avatar.tsx
@@ -322,14 +322,25 @@ var RULES3 = `
322
322
  }
323
323
  .nb-drawer--right { margin-left: auto; }
324
324
  .nb-drawer--left { margin-right: auto; }
325
+ .nb-drawer--bottom {
326
+ margin-top: auto; width: 100%; max-width: 100%;
327
+ height: auto; max-height: 92%;
328
+ }
325
329
  .nb-drawer--right[open] { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
326
330
  .nb-drawer--left[open] { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
331
+ .nb-drawer--bottom[open] { animation: nb-drawer-in-bottom 240ms cubic-bezier(0.2, 0, 0, 1); }
327
332
  .nb-drawer::backdrop { background: rgba(0, 0, 0, 0.32); }
328
333
  .nb-drawer[open]::backdrop { animation: nb-drawer-fade 200ms ease; }
329
334
  @keyframes nb-drawer-in-right { from { transform: translateX(100%); } }
330
335
  @keyframes nb-drawer-in-left { from { transform: translateX(-100%); } }
336
+ @keyframes nb-drawer-in-bottom { from { transform: translateY(100%); } }
331
337
  @keyframes nb-drawer-fade { from { opacity: 0; } }
338
+ .nb-drawer-iconbtn { width: 28px; height: 28px; }
339
+ @media (pointer: coarse) {
340
+ .nb-drawer-iconbtn { min-width: 44px; min-height: 44px; }
341
+ }
332
342
  `;
343
+ var DrawerContext = createContext(null);
333
344
  function DrawerRoot({
334
345
  open,
335
346
  onClose,
@@ -342,42 +353,51 @@ function DrawerRoot({
342
353
  }) {
343
354
  ensureStyle(STYLE_ID3, RULES3);
344
355
  const dialogRef = useRef(null);
356
+ const labelId = useId();
357
+ const [hasTitle, setHasTitle] = useState(false);
345
358
  useEffect(() => {
346
359
  const dialog = dialogRef.current;
347
360
  if (!dialog || typeof dialog.showModal !== "function") return;
348
361
  if (open && !dialog.open) dialog.showModal();
349
362
  else if (!open && dialog.open) dialog.close();
350
363
  }, [open]);
364
+ const isBottom = side === "bottom";
351
365
  const panelStyle = {
352
- width,
366
+ ...isBottom ? { width: "100%", maxWidth: "100%" } : { width },
353
367
  background: tokens.bg,
354
368
  color: tokens.fg,
355
369
  boxShadow: tokens.shadowLg,
356
- ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : { borderRight: `${tokens.borderWidth} solid ${tokens.border}` },
370
+ ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : side === "left" ? { borderRight: `${tokens.borderWidth} solid ${tokens.border}` } : { borderTop: `${tokens.borderWidth} solid ${tokens.border}` },
357
371
  ...style
358
372
  };
359
- return (
360
- // biome-ignore lint/a11y/useKeyWithClickEvents: backdrop click is a mouse convenience; the dialog's Escape (onCancel) is the keyboard dismissal path.
361
- /* @__PURE__ */ jsx(
362
- "dialog",
363
- {
364
- ref: dialogRef,
365
- className: `nb-drawer nb-drawer--${side}`,
366
- style: panelStyle,
367
- onCancel: (e) => {
368
- e.preventDefault();
369
- (onEscape ?? onClose)();
370
- },
371
- onClick: (e) => {
372
- if (e.target === dialogRef.current) onClose();
373
- },
374
- ...rest,
375
- children
376
- }
377
- )
378
- );
373
+ const ctxValue = useMemo(() => ({ labelId, setHasTitle }), [labelId]);
374
+ return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsx(
375
+ "dialog",
376
+ {
377
+ ref: dialogRef,
378
+ className: `nb-drawer nb-drawer--${side}`,
379
+ style: panelStyle,
380
+ "aria-labelledby": hasTitle ? labelId : void 0,
381
+ onCancel: (e) => {
382
+ e.preventDefault();
383
+ (onEscape ?? onClose)();
384
+ },
385
+ onClick: (e) => {
386
+ if (e.target === dialogRef.current) onClose();
387
+ },
388
+ ...rest,
389
+ children
390
+ }
391
+ ) });
379
392
  }
380
- function Header({ onClose, style, children, ...rest }) {
393
+ function Header({ title, onBack, actions, onClose, style, children, ...rest }) {
394
+ const ctx = useContext(DrawerContext);
395
+ const hasTitle = title != null;
396
+ useEffect(() => {
397
+ if (!ctx || !hasTitle) return;
398
+ ctx.setHasTitle(true);
399
+ return () => ctx.setHasTitle(false);
400
+ }, [ctx, hasTitle]);
381
401
  return /* @__PURE__ */ jsxs(
382
402
  "header",
383
403
  {
@@ -395,41 +415,75 @@ function Header({ onClose, style, children, ...rest }) {
395
415
  },
396
416
  ...rest,
397
417
  children: [
398
- /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0 }, children }),
399
- onClose ? /* @__PURE__ */ jsx(CloseButton, { onClose }) : null
418
+ onBack ? /* @__PURE__ */ jsx(IconButton, { label: "Back", onClick: onBack, children: /* @__PURE__ */ jsx(
419
+ "path",
420
+ {
421
+ d: "M10 3L5 8l5 5",
422
+ stroke: "currentColor",
423
+ strokeWidth: "1.5",
424
+ fill: "none",
425
+ strokeLinecap: "round",
426
+ strokeLinejoin: "round"
427
+ }
428
+ ) }) : null,
429
+ hasTitle ? /* @__PURE__ */ jsx(
430
+ "h2",
431
+ {
432
+ id: ctx?.labelId,
433
+ style: {
434
+ flex: 1,
435
+ minWidth: 0,
436
+ margin: 0,
437
+ fontFamily: tokens.fontSans,
438
+ fontSize: tokens.textBaseSize,
439
+ lineHeight: tokens.textBaseLine,
440
+ fontWeight: tokens.weightSemibold,
441
+ color: tokens.fg,
442
+ whiteSpace: "nowrap",
443
+ overflow: "hidden",
444
+ textOverflow: "ellipsis"
445
+ },
446
+ children: title
447
+ }
448
+ ) : /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0 }, children }),
449
+ actions ? /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: "0.4rem", flexShrink: 0 }, children: actions }) : null,
450
+ onClose ? /* @__PURE__ */ jsx(IconButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ jsx(
451
+ "path",
452
+ {
453
+ d: "M4 4l8 8M12 4l-8 8",
454
+ stroke: "currentColor",
455
+ strokeWidth: "1.5",
456
+ strokeLinecap: "round"
457
+ }
458
+ ) }) : null
400
459
  ]
401
460
  }
402
461
  );
403
462
  }
404
- function CloseButton({ onClose }) {
463
+ function IconButton({
464
+ label,
465
+ onClick,
466
+ children
467
+ }) {
405
468
  return /* @__PURE__ */ jsx(
406
469
  "button",
407
470
  {
408
471
  type: "button",
409
- "aria-label": "Close",
410
- onClick: onClose,
472
+ "aria-label": label,
473
+ onClick,
474
+ className: "nb-drawer-iconbtn",
411
475
  style: {
412
476
  flexShrink: 0,
413
477
  display: "inline-flex",
414
478
  alignItems: "center",
415
479
  justifyContent: "center",
416
- width: 28,
417
- height: 28,
418
480
  border: "none",
419
481
  background: "transparent",
420
482
  color: tokens.fgMuted,
421
483
  cursor: "pointer",
422
484
  borderRadius: tokens.radiusSm
423
485
  },
424
- children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
425
- "path",
426
- {
427
- d: "M4 4l8 8M12 4l-8 8",
428
- stroke: "currentColor",
429
- strokeWidth: "1.5",
430
- strokeLinecap: "round"
431
- }
432
- ) })
486
+ children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children })
433
487
  }
434
488
  );
435
489
  }