raft-ui 0.0.6 → 0.0.7

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +2 -8
  2. package/{src → dist}/styles.css +0 -2
  3. package/package.json +3 -4
  4. package/src/components/alert-dialog/alert-dialog.tsx +0 -215
  5. package/src/components/alert-dialog/index.ts +0 -30
  6. package/src/components/avatar/avatar.tsx +0 -375
  7. package/src/components/avatar/index.ts +0 -17
  8. package/src/components/badge/badge.tsx +0 -393
  9. package/src/components/badge/index.ts +0 -2
  10. package/src/components/banner/banner.tsx +0 -269
  11. package/src/components/banner/index.ts +0 -8
  12. package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
  13. package/src/components/bottom-sheet/index.ts +0 -22
  14. package/src/components/button/button.tsx +0 -499
  15. package/src/components/button/index.ts +0 -2
  16. package/src/components/checkbox/checkbox-indicator.tsx +0 -128
  17. package/src/components/checkbox/checkbox.tsx +0 -350
  18. package/src/components/checkbox/index.ts +0 -2
  19. package/src/components/combobox/combobox.tsx +0 -603
  20. package/src/components/combobox/index.ts +0 -52
  21. package/src/components/context-menu/context-menu.tsx +0 -380
  22. package/src/components/context-menu/index.ts +0 -34
  23. package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
  24. package/src/components/conversation-preview-card/index.ts +0 -32
  25. package/src/components/description-list/description-list.tsx +0 -116
  26. package/src/components/description-list/index.ts +0 -14
  27. package/src/components/dialog/dialog.tsx +0 -152
  28. package/src/components/dialog/index.ts +0 -26
  29. package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
  30. package/src/components/dropdown-menu/index.ts +0 -38
  31. package/src/components/empty-state/empty-state.tsx +0 -177
  32. package/src/components/empty-state/index.ts +0 -16
  33. package/src/components/field/field.tsx +0 -125
  34. package/src/components/field/index.ts +0 -18
  35. package/src/components/inline-code/index.ts +0 -2
  36. package/src/components/inline-code/inline-code.tsx +0 -35
  37. package/src/components/input/index.ts +0 -2
  38. package/src/components/input/input.tsx +0 -76
  39. package/src/components/input-group/index.ts +0 -7
  40. package/src/components/input-group/input-group.tsx +0 -215
  41. package/src/components/kbd/index.ts +0 -2
  42. package/src/components/kbd/kbd.tsx +0 -43
  43. package/src/components/label/index.ts +0 -2
  44. package/src/components/label/label.tsx +0 -99
  45. package/src/components/lightbox/index.ts +0 -24
  46. package/src/components/lightbox/lightbox.tsx +0 -245
  47. package/src/components/list-item/index.ts +0 -20
  48. package/src/components/list-item/list-item.tsx +0 -263
  49. package/src/components/media-list-item/index.ts +0 -20
  50. package/src/components/media-list-item/media-list-item.tsx +0 -169
  51. package/src/components/notification-center/index.ts +0 -39
  52. package/src/components/notification-center/notification-center.tsx +0 -558
  53. package/src/components/panel-header/index.ts +0 -22
  54. package/src/components/panel-header/panel-header.tsx +0 -199
  55. package/src/components/popover/index.ts +0 -30
  56. package/src/components/popover/popover.tsx +0 -248
  57. package/src/components/preview-shell/index.ts +0 -2
  58. package/src/components/preview-shell/preview-shell.tsx +0 -51
  59. package/src/components/quoted-message-card/index.ts +0 -36
  60. package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
  61. package/src/components/radio-group/index.ts +0 -2
  62. package/src/components/radio-group/radio-group.tsx +0 -119
  63. package/src/components/section-header/index.ts +0 -16
  64. package/src/components/section-header/section-header.tsx +0 -117
  65. package/src/components/section-label/index.ts +0 -2
  66. package/src/components/section-label/section-label.tsx +0 -38
  67. package/src/components/segmented-control/index.ts +0 -12
  68. package/src/components/segmented-control/segmented-control.tsx +0 -129
  69. package/src/components/select/index.ts +0 -40
  70. package/src/components/select/select.tsx +0 -382
  71. package/src/components/separator/index.ts +0 -2
  72. package/src/components/separator/separator.tsx +0 -40
  73. package/src/components/spinner/index.ts +0 -2
  74. package/src/components/spinner/spinner.tsx +0 -228
  75. package/src/components/status/index.ts +0 -2
  76. package/src/components/status/status.tsx +0 -152
  77. package/src/components/tabs/index.ts +0 -24
  78. package/src/components/tabs/tabs.tsx +0 -451
  79. package/src/components/text/index.ts +0 -2
  80. package/src/components/text/text.tsx +0 -131
  81. package/src/components/textarea/index.ts +0 -2
  82. package/src/components/textarea/textarea.tsx +0 -136
  83. package/src/components/toggle-group/index.ts +0 -7
  84. package/src/components/toggle-group/toggle-group.tsx +0 -111
  85. package/src/components/tooltip/index.ts +0 -8
  86. package/src/components/tooltip/tooltip.tsx +0 -212
  87. package/src/index.ts +0 -429
  88. package/src/legacy/AttentionDot.tsx +0 -28
  89. package/src/legacy/AvatarListRow.tsx +0 -87
  90. package/src/legacy/AvatarSlot.tsx +0 -157
  91. package/src/legacy/Badge.tsx +0 -95
  92. package/src/legacy/Banner.tsx +0 -74
  93. package/src/legacy/BottomSheet.tsx +0 -76
  94. package/src/legacy/CheckMarker.tsx +0 -72
  95. package/src/legacy/Checkbox.tsx +0 -81
  96. package/src/legacy/ConversationPreviewCard.tsx +0 -169
  97. package/src/legacy/DismissBackdrop.tsx +0 -68
  98. package/src/legacy/EmptyState.tsx +0 -63
  99. package/src/legacy/ExperimentalBadge.tsx +0 -19
  100. package/src/legacy/FormField.tsx +0 -106
  101. package/src/legacy/KeyValueRow.tsx +0 -67
  102. package/src/legacy/Lightbox.tsx +0 -119
  103. package/src/legacy/NotificationCenter.tsx +0 -217
  104. package/src/legacy/PanelHeader.tsx +0 -94
  105. package/src/legacy/PreviewShell.tsx +0 -40
  106. package/src/legacy/QuotedMessageCard.tsx +0 -181
  107. package/src/legacy/ReorderablePanelTabs.tsx +0 -156
  108. package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
  109. package/src/legacy/SearchInput.tsx +0 -23
  110. package/src/legacy/SectionEyebrow.tsx +0 -60
  111. package/src/legacy/SectionHeader.tsx +0 -68
  112. package/src/legacy/SegmentedControl.tsx +0 -74
  113. package/src/legacy/SlugInput.tsx +0 -29
  114. package/src/legacy/Spinner.tsx +0 -89
  115. package/src/legacy/StatusDot.tsx +0 -51
  116. package/src/legacy/SurfaceListItem.tsx +0 -32
  117. package/src/legacy/Textarea.tsx +0 -137
  118. package/src/legacy/button.tsx +0 -100
  119. package/src/legacy/context-menu-divider.tsx +0 -12
  120. package/src/legacy/menu-item.tsx +0 -66
  121. package/src/legacy/selection-popover.tsx +0 -212
  122. package/src/legacy/server-switcher-menu.tsx +0 -203
  123. package/src/legacy/source.css +0 -240
  124. package/src/lib/cn.ts +0 -1
  125. package/src/lib/styled-props.ts +0 -11
  126. package/src/lib/theme/theme-context.ts +0 -28
  127. package/src/lib/theme/theme-provider.tsx +0 -181
  128. package/src/lib/theme/use-theme.ts +0 -12
  129. package/src/lib/tv.ts +0 -29
  130. package/src/lib/without-children.ts +0 -6
  131. package/src/wip.ts +0 -291
@@ -1,19 +0,0 @@
1
- import type { HTMLAttributes } from "react";
2
- import { Badge } from "./Badge";
3
-
4
- export interface ExperimentalBadgeProps extends HTMLAttributes<HTMLSpanElement> {
5
- // Callers can add positional tweaks (e.g. `-translate-y-px` inside a
6
- // PanelHeader title row) — but the visual identity (lime tone +
7
- // brutal Badge's label typography) is owned by Badge so every
8
- // Experimental surface stays visually identical.
9
- }
10
-
11
- export function ExperimentalBadge({ className = "", ...props }: ExperimentalBadgeProps) {
12
- return (
13
- <Badge tone="lime" className={className} {...props}>
14
- Experimental
15
- </Badge>
16
- );
17
- }
18
-
19
- export default ExperimentalBadge;
@@ -1,106 +0,0 @@
1
- /**
2
- * Canonical form-field row — label + control + optional helper / error.
3
- *
4
- * Replaces the inline `<div><label className="mb-1 block text-sm font-bold
5
- * text-black uppercase tracking-wide">…</label><input className="input-brutal
6
- * …" /></div>` pattern that appears 30+ times across auth pages, dialogs, and
7
- * settings panels.
8
- *
9
- * Background: stdrc 2026-05-12 #wg-theme:a987f888 asked for the strong-signal
10
- * primitives to be lifted into one PR. FormField is the highest-leverage of
11
- * the three (FormField / KeyValueRow / SectionHeader) — every multi-field
12
- * dialog and every auth page is a stack of these.
13
- *
14
- * Slots:
15
- * - `label` — the field label text (rendered as `<label>` and tied to
16
- * children via the optional `htmlFor`)
17
- * - `required` — true → render the `*` marker in `text-brutal-pink`
18
- * - `optional` — true → render `(optional)` in `text-black/40 normal-case`
19
- * (mutually exclusive with `required`)
20
- * - `hint` — rendered below the control, `text-xs text-black/50`
21
- * - `error` — rendered below the control, `text-xs text-brutal-red`,
22
- * takes precedence over hint
23
- * - `htmlFor` — forwarded to the `<label htmlFor>` attribute
24
- * - `labelStyle`— `"uppercase"` (default, matches dialog convention) or
25
- * `"plain"` (matches auth-page convention — sentence/title case)
26
- * - `size` — `"default"` (default, `text-sm` + full-opacity black) or
27
- * `"compact"` (`text-xs` + `text-black/60`). Compact mirrors
28
- * the dense Settings form-row label that uses
29
- * `mb-1 block text-xs font-bold text-black/60`. Use compact
30
- * inside Settings forms to preserve the existing density;
31
- * use default inside dialogs / auth pages.
32
- * - `className` — additional classes appended to the outer wrapper (e.g.
33
- * `mt-2`)
34
- *
35
- * The control itself is `children`. FormField does NOT render the `<input>` /
36
- * `<textarea>` / `<SegmentedControl>` etc. — pass it explicitly. This keeps
37
- * FormField agnostic of the control library and avoids prop-explosion.
38
- */
39
-
40
- import type { ReactNode, LabelHTMLAttributes } from "react";
41
-
42
- const LABEL_BASE_DEFAULT = "mb-1 block text-sm font-bold text-black";
43
- const LABEL_BASE_COMPACT = "mb-1 block text-xs font-bold text-black/60";
44
- const LABEL_UPPERCASE = "uppercase tracking-wide";
45
-
46
- export type FormFieldProps = {
47
- /** Field label text. */
48
- label: ReactNode;
49
- /** Show a `*` required marker after the label. */
50
- required?: boolean;
51
- /** Show `(optional)` in muted case after the label. Mutually exclusive
52
- * with `required`. */
53
- optional?: boolean;
54
- /** Helper text rendered below the control. */
55
- hint?: ReactNode;
56
- /** Error text rendered below the control (takes precedence over `hint`). */
57
- error?: ReactNode;
58
- /** `<label htmlFor>` — point at the inner control's `id`. */
59
- htmlFor?: LabelHTMLAttributes<HTMLLabelElement>["htmlFor"];
60
- /** Label case style. `"uppercase"` (default) matches the dialog/settings
61
- * convention. `"plain"` matches the auth-page convention. */
62
- labelStyle?: "uppercase" | "plain";
63
- /** Label size. `"default"` (default) is `text-sm` + full-opacity black —
64
- * matches dialogs / auth pages. `"compact"` is `text-xs` + `text-black/60`
65
- * — matches the dense Settings form-row labels. */
66
- size?: "default" | "compact";
67
- /** Additional classes appended to the outer `<div>`. */
68
- className?: string;
69
- /** The actual control — `<input>`, `<textarea>`, `<SegmentedControl>`,
70
- * `<select>`, custom group, etc. */
71
- children: ReactNode;
72
- };
73
-
74
- export default function FormField({
75
- label,
76
- required,
77
- optional,
78
- hint,
79
- error,
80
- htmlFor,
81
- labelStyle = "uppercase",
82
- size = "default",
83
- className,
84
- children,
85
- }: FormFieldProps) {
86
- const base = size === "compact" ? LABEL_BASE_COMPACT : LABEL_BASE_DEFAULT;
87
- const labelCls = `${base} ${labelStyle === "uppercase" ? LABEL_UPPERCASE : ""}`.trim();
88
- const wrapperCls = className ? className : undefined;
89
- return (
90
- <div className={wrapperCls}>
91
- <label className={labelCls} htmlFor={htmlFor}>
92
- {label}
93
- {required ? <span className="ml-1 text-brutal-pink">*</span> : null}
94
- {optional ? <span className="ml-1 text-black/40 normal-case">(optional)</span> : null}
95
- </label>
96
- {children}
97
- {error ? (
98
- <p className="mt-1 text-xs font-bold text-brutal-red" role="alert">
99
- {error}
100
- </p>
101
- ) : hint ? (
102
- <p className="mt-1 text-xs text-black/50">{hint}</p>
103
- ) : null}
104
- </div>
105
- );
106
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Canonical key/value row — small label above value.
3
- *
4
- * Replaces the inline `<div><div className="text-xs text-black/50 mb-1">Foo</div><span className="text-sm text-black font-mono">bar</span></div>` pattern that appears 30+ times across detail panels (HumanDetailPanel, AgentDetailPanel, MachineDetailPanel) and the SettingsPanel body.
5
- *
6
- * Background: stdrc 2026-05-12 #wg-theme:a987f888 asked for strong-signal
7
- * primitives to be lifted in one PR. KeyValueRow is the second of the three.
8
- *
9
- * Slots:
10
- * - `label` — the muted label text (string or node)
11
- * - `value` — the value content (string or node, often code/mono)
12
- * - `mono` — true → render the value cell in `font-mono`
13
- * - `breakAll` — true → render the value cell with `break-all` (long
14
- * emails, hostnames, etc.)
15
- * - `action` — optional inline trailing element rendered next to the
16
- * label (e.g. an edit pencil button)
17
- * - `className` — additional classes on the outer wrapper
18
- * - `valueClassName` — additional classes on the value cell
19
- *
20
- * Typography defaults match the canonical detail-panel KV convention: label
21
- * is `text-xs text-black/50 mb-1`, value is `text-sm text-black`. Pass
22
- * `mono` for fields like email / dates / IDs that are conventionally rendered
23
- * in monospace.
24
- *
25
- * If your row needs an inline-editable value (with Save/Cancel and per-row
26
- * loading state), this primitive is not the right fit — it intentionally
27
- * stays a presentational primitive. Inline-edit shells like the role / avatar
28
- * editors in HumanDetailPanel keep their own structure.
29
- */
30
-
31
- import type { ReactNode } from "react";
32
-
33
- export type KeyValueRowProps = {
34
- label: ReactNode;
35
- value: ReactNode;
36
- mono?: boolean;
37
- breakAll?: boolean;
38
- action?: ReactNode;
39
- className?: string;
40
- valueClassName?: string;
41
- };
42
-
43
- export default function KeyValueRow({
44
- label,
45
- value,
46
- mono = false,
47
- breakAll = false,
48
- action,
49
- className,
50
- valueClassName,
51
- }: KeyValueRowProps) {
52
- const wrapperCls = className ? className : undefined;
53
- const valueClsParts = ["text-sm text-black"];
54
- if (mono) valueClsParts.push("font-mono");
55
- if (breakAll) valueClsParts.push("break-all");
56
- if (valueClassName) valueClsParts.push(valueClassName);
57
- const valueCls = valueClsParts.join(" ");
58
- return (
59
- <div className={wrapperCls}>
60
- <div className="mb-1 flex items-center gap-2">
61
- <div className="text-xs text-black/50">{label}</div>
62
- {action ? <div className="shrink-0">{action}</div> : null}
63
- </div>
64
- <div className={valueCls}>{value}</div>
65
- </div>
66
- );
67
- }
@@ -1,119 +0,0 @@
1
- import { type HTMLAttributes, type ReactNode, useEffect, useRef } from "react";
2
- import { createPortal } from "react-dom";
3
-
4
- /**
5
- * Full-screen dark-backdrop overlay for lightboxes and bottom sheets.
6
- *
7
- * Replaces the recurring pattern:
8
- * <div className="fixed inset-0 z-[70] flex flex-col bg-black/75" onClick={...}>
9
- *
10
- * Used in: ImageLightbox, SelectShareLightbox, MessageItem mobile reaction sheet.
11
- *
12
- * Distinct from <Modal> (centered card) — this primitive is content-agnostic:
13
- * callers control layout via `className` on children. Modal stays for
14
- * centered-card dialogs (ConfirmDialog, CreateChannelDialog, etc.).
15
- *
16
- * Body scroll lock is applied while mounted.
17
- *
18
- * Extra HTML attributes (e.g. `data-testid`) are forwarded to the backdrop div.
19
- */
20
- interface LightboxProps extends Omit<
21
- HTMLAttributes<HTMLDivElement>,
22
- "onClick" | "className" | "children"
23
- > {
24
- onClose: () => void;
25
- children: ReactNode;
26
- /** CSS z-index. Default 70 (above Modal's z-50/z-[60]). */
27
- zIndex?: number;
28
- /** Tailwind bg class for the backdrop. Default "bg-black/75". */
29
- backdropClass?: string;
30
- /** Click on backdrop closes. Default true. */
31
- dismissOnBackdrop?: boolean;
32
- /** Extra classes on the backdrop div (e.g. "flex flex-col"). */
33
- className?: string;
34
- /** Positioning class for the backdrop. Default fixed viewport overlay. */
35
- positionClass?: string;
36
- /** Lock body scroll while mounted. Default true. */
37
- lockBodyScroll?: boolean;
38
- /** Scroll the overlay root into view after mount. Useful for document-flow overlays. */
39
- scrollIntoViewOnMount?: boolean;
40
- }
41
-
42
- export default function Lightbox({
43
- onClose,
44
- children,
45
- zIndex = 70,
46
- backdropClass = "bg-black/75",
47
- dismissOnBackdrop = true,
48
- className = "",
49
- positionClass = "fixed inset-0",
50
- lockBodyScroll = true,
51
- scrollIntoViewOnMount = false,
52
- ...rest
53
- }: LightboxProps) {
54
- const rootRef = useRef<HTMLDivElement | null>(null);
55
-
56
- // Close the topmost preview before browser-level close shortcuts can close
57
- // the whole tab/window.
58
- useEffect(() => {
59
- const handler = (e: KeyboardEvent) => {
60
- const closeShortcut =
61
- e.key.toLowerCase() === "w" && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey;
62
- if (e.key !== "Escape" && !closeShortcut) return;
63
- e.preventDefault();
64
- e.stopPropagation();
65
- onClose();
66
- };
67
- document.addEventListener("keydown", handler);
68
- return () => document.removeEventListener("keydown", handler);
69
- }, [onClose]);
70
-
71
- // Body scroll lock
72
- useEffect(() => {
73
- const html = document.documentElement;
74
- const prevHtmlOverflow = html.style.overflow;
75
- const prevHtmlHeight = html.style.height;
76
- const prevBodyOverflow = document.body.style.overflow;
77
- const prevBodyHeight = document.body.style.height;
78
-
79
- if (lockBodyScroll) {
80
- document.body.style.overflow = "hidden";
81
- } else {
82
- html.style.overflow = "auto";
83
- html.style.height = "auto";
84
- document.body.style.overflow = "auto";
85
- document.body.style.height = "auto";
86
- }
87
-
88
- return () => {
89
- html.style.overflow = prevHtmlOverflow;
90
- html.style.height = prevHtmlHeight;
91
- document.body.style.overflow = prevBodyOverflow;
92
- document.body.style.height = prevBodyHeight;
93
- };
94
- }, [lockBodyScroll]);
95
-
96
- useEffect(() => {
97
- if (!scrollIntoViewOnMount) return;
98
- rootRef.current?.scrollIntoView({ block: "start" });
99
- }, [scrollIntoViewOnMount]);
100
-
101
- return createPortal(
102
- <div
103
- ref={rootRef}
104
- {...rest}
105
- className={`${positionClass} ${backdropClass} ${className}`}
106
- style={{ zIndex }}
107
- onClick={
108
- dismissOnBackdrop
109
- ? (e) => {
110
- if (e.target === e.currentTarget) onClose();
111
- }
112
- : undefined
113
- }
114
- >
115
- {children}
116
- </div>,
117
- document.body,
118
- );
119
- }
@@ -1,217 +0,0 @@
1
- import { forwardRef, useEffect, type HTMLAttributes, type ReactNode } from "react";
2
- import { AlertTriangle, CheckCircle2, Info } from "lucide-react";
3
- import EmptyState from "./EmptyState";
4
- import SectionEyebrow from "./SectionEyebrow";
5
-
6
- export type NotificationCenterKind = "error" | "warning" | "info" | "success";
7
- export type NotificationCenterViewport = "desktop" | "mobile";
8
- export type NotificationCenterSize = "compact" | "regular" | "large";
9
- export type NotificationCenterActionVariant = "primary" | "secondary";
10
-
11
- export interface NotificationCenterAction {
12
- id?: string;
13
- label: ReactNode;
14
- ariaLabel?: string;
15
- onClick?: () => void;
16
- disabled?: boolean;
17
- variant?: NotificationCenterActionVariant;
18
- }
19
-
20
- export interface NotificationCenterEntry {
21
- id: string;
22
- kind: NotificationCenterKind;
23
- title: ReactNode;
24
- body?: ReactNode;
25
- icon?: ReactNode;
26
- actions?: NotificationCenterAction[];
27
- read?: boolean;
28
- }
29
-
30
- export interface NotificationCenterProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
31
- entries: NotificationCenterEntry[];
32
- /** Desktop uses a stable fixed-height frame. Mobile grows with content until
33
- * its max-height token, then scrolls internally. */
34
- viewport: NotificationCenterViewport;
35
- size?: NotificationCenterSize;
36
- title?: ReactNode;
37
- countLabel?: ReactNode;
38
- emptyState?: ReactNode;
39
- closeOnEscape?: boolean;
40
- onClose?: () => void;
41
- }
42
-
43
- const SIZE_CLASS: Record<NotificationCenterViewport, Record<NotificationCenterSize, string>> = {
44
- desktop: {
45
- compact: "h-56",
46
- regular: "h-72",
47
- large: "h-96",
48
- },
49
- mobile: {
50
- compact: "max-h-[min(64dvh,224px)]",
51
- regular: "max-h-[min(72dvh,288px)]",
52
- large: "max-h-[min(80dvh,384px)]",
53
- },
54
- };
55
-
56
- const WIDTH_CLASS: Record<NotificationCenterViewport, string> = {
57
- desktop: "w-80",
58
- mobile: "w-[min(92vw,360px)]",
59
- };
60
-
61
- const KIND_DOT_BG: Record<NotificationCenterKind, string> = {
62
- error: "bg-brutal-orange",
63
- warning: "bg-brutal-orange",
64
- info: "bg-brutal-yellow",
65
- success: "bg-brutal-lime",
66
- };
67
-
68
- const KIND_LABEL: Record<NotificationCenterKind, string> = {
69
- error: "Error",
70
- warning: "Warning",
71
- info: "Info",
72
- success: "Success",
73
- };
74
-
75
- const DEFAULT_KIND_ICON: Record<NotificationCenterKind, ReactNode> = {
76
- error: <AlertTriangle size={16} className="text-black" />,
77
- warning: <AlertTriangle size={16} className="text-black" />,
78
- info: <Info size={16} className="text-black" />,
79
- success: <CheckCircle2 size={16} className="text-black" />,
80
- };
81
-
82
- /**
83
- * Reusable notification-center primitive.
84
- *
85
- * Layout contract:
86
- * - desktop: fixed-height frame, scrollable list body.
87
- * - mobile: content-height frame capped by a max-height token, then scrollable
88
- * list body.
89
- *
90
- * The component does not know how notifications are sourced or dismissed; apps
91
- * provide entries and action handlers. This keeps product aggregation separate
92
- * from the open-source-ready UI primitive.
93
- */
94
- const NotificationCenter = forwardRef<HTMLDivElement, NotificationCenterProps>(
95
- function NotificationCenter(
96
- {
97
- entries,
98
- viewport,
99
- size = "regular",
100
- title = "Notifications",
101
- countLabel,
102
- emptyState,
103
- closeOnEscape = true,
104
- onClose,
105
- className = "",
106
- ...props
107
- },
108
- ref,
109
- ) {
110
- useEffect(() => {
111
- if (!closeOnEscape || !onClose) return;
112
- const onKey = (e: KeyboardEvent) => {
113
- if (e.key === "Escape") onClose();
114
- };
115
- window.addEventListener("keydown", onKey);
116
- return () => window.removeEventListener("keydown", onKey);
117
- }, [closeOnEscape, onClose]);
118
-
119
- return (
120
- <div
121
- {...props}
122
- ref={ref}
123
- role={props.role ?? "dialog"}
124
- aria-label={props["aria-label"] ?? "Notification center"}
125
- className={[
126
- "card-brutal flex flex-col overflow-hidden bg-white",
127
- WIDTH_CLASS[viewport],
128
- SIZE_CLASS[viewport][size],
129
- className,
130
- ].join(" ")}
131
- >
132
- <div className="flex items-center gap-2 border-b-2 border-black bg-brutal-cream px-3 py-2">
133
- <SectionEyebrow>{title}</SectionEyebrow>
134
- <span className="ml-auto text-xs font-mono text-black/40">
135
- {countLabel ?? defaultCountLabel(entries.length)}
136
- </span>
137
- </div>
138
-
139
- <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
140
- {entries.length === 0 ? (
141
- (emptyState ?? <NotificationCenterEmptyState />)
142
- ) : (
143
- <ul className="divide-y-2 divide-black" aria-label="Notifications">
144
- {entries.map((entry) => (
145
- <NotificationCenterItem key={entry.id} entry={entry} />
146
- ))}
147
- </ul>
148
- )}
149
- </div>
150
- </div>
151
- );
152
- },
153
- );
154
-
155
- export default NotificationCenter;
156
-
157
- export function NotificationCenterEmptyState({
158
- title = "No notifications right now",
159
- body = "You'll see things here that need your attention.",
160
- }: {
161
- title?: ReactNode;
162
- body?: ReactNode;
163
- }) {
164
- return <EmptyState icon={<CheckCircle2 size={36} />} title={title} description={body} />;
165
- }
166
-
167
- function NotificationCenterItem({ entry }: { entry: NotificationCenterEntry }) {
168
- return (
169
- <li className={`px-3 py-3 ${entry.read ? "opacity-70" : ""}`}>
170
- <div className="flex items-start gap-2">
171
- <div
172
- className={`mt-0.5 flex size-6 shrink-0 items-center justify-center border-2 border-black ${
173
- KIND_DOT_BG[entry.kind]
174
- }`}
175
- aria-label={KIND_LABEL[entry.kind]}
176
- >
177
- {entry.icon ?? DEFAULT_KIND_ICON[entry.kind]}
178
- </div>
179
- <div className="min-w-0 flex-1">
180
- <div className="text-sm font-bold text-black break-words">{entry.title}</div>
181
- {entry.body && <div className="mt-0.5 text-sm break-words">{entry.body}</div>}
182
- {entry.actions && entry.actions.length > 0 && (
183
- <div className="mt-2 flex flex-wrap items-center gap-2">
184
- {entry.actions.map((action, index) => (
185
- <NotificationCenterActionButton
186
- key={action.id ?? `${entry.id}-action-${index}`}
187
- action={action}
188
- />
189
- ))}
190
- </div>
191
- )}
192
- </div>
193
- </div>
194
- </li>
195
- );
196
- }
197
-
198
- function NotificationCenterActionButton({ action }: { action: NotificationCenterAction }) {
199
- const isPrimary = action.variant === "primary";
200
- return (
201
- <button
202
- type="button"
203
- onClick={action.onClick}
204
- disabled={action.disabled}
205
- aria-label={action.ariaLabel}
206
- className={`btn-brutal px-3 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-50 ${
207
- isPrimary ? "bg-brutal-pink" : "bg-white"
208
- }`}
209
- >
210
- {action.label}
211
- </button>
212
- );
213
- }
214
-
215
- function defaultCountLabel(count: number): ReactNode {
216
- return count === 0 ? "all clear" : `${count} ${count === 1 ? "item" : "items"}`;
217
- }
@@ -1,94 +0,0 @@
1
- import { ArrowLeft } from "lucide-react";
2
- import type { HTMLAttributes, ReactNode } from "react";
3
- import Button from "./button";
4
-
5
- type DivPassthroughProps = HTMLAttributes<HTMLDivElement> &
6
- Record<`data-${string}`, string | undefined>;
7
- type ButtonPassthroughProps = HTMLAttributes<HTMLButtonElement> &
8
- Record<`data-${string}`, string | undefined>;
9
-
10
- export interface PanelHeaderProps {
11
- title?: ReactNode;
12
- titleSlot?: ReactNode;
13
- titleSuffix?: ReactNode;
14
- subtitle?: ReactNode;
15
- subtitleMultiline?: boolean;
16
- icon?: ReactNode;
17
- iconBg?: string;
18
- iconSlot?: ReactNode;
19
- iconAlwaysVisible?: boolean;
20
- onMobileBack?: () => void;
21
- mobileBreakpoint?: "md" | "lg";
22
- mobileBackProps?: ButtonPassthroughProps;
23
- titleClickProps?: DivPassthroughProps;
24
- containerProps?: DivPassthroughProps;
25
- actions?: ReactNode;
26
- }
27
-
28
- export default function PanelHeader({
29
- title,
30
- titleSlot,
31
- titleSuffix,
32
- subtitle,
33
- subtitleMultiline = false,
34
- icon,
35
- iconBg = "bg-brutal-yellow",
36
- iconSlot,
37
- iconAlwaysVisible = false,
38
- onMobileBack,
39
- mobileBreakpoint = "md",
40
- mobileBackProps,
41
- titleClickProps,
42
- containerProps,
43
- actions,
44
- }: PanelHeaderProps) {
45
- const mobileHiddenClass = mobileBreakpoint === "lg" ? "lg:hidden" : "md:hidden";
46
- const iconVisibilityClass = iconAlwaysVisible
47
- ? "flex"
48
- : mobileBreakpoint === "lg"
49
- ? "hidden lg:flex"
50
- : "hidden md:flex";
51
- const { className: containerExtraClass, ...containerRest } = containerProps ?? {};
52
-
53
- return (
54
- <div
55
- {...containerRest}
56
- className={`flex h-panel-header items-center gap-3 border-b-2 border-black bg-white px-5 ${containerExtraClass ?? ""}`}
57
- >
58
- {onMobileBack && (
59
- <Button
60
- {...mobileBackProps}
61
- onClick={onMobileBack}
62
- shape="icon"
63
- className={`${mobileHiddenClass} ${mobileBackProps?.className ?? ""}`}
64
- >
65
- <ArrowLeft size={14} />
66
- </Button>
67
- )}
68
- {icon && (
69
- <div
70
- className={`${iconVisibilityClass} size-icon-header shrink-0 items-center justify-center border-2 border-black ${iconBg}`}
71
- >
72
- {icon}
73
- </div>
74
- )}
75
- {iconSlot && <div className={`${iconVisibilityClass} shrink-0`}>{iconSlot}</div>}
76
- {titleSlot ? (
77
- <div className="min-w-0 flex-1">{titleSlot}</div>
78
- ) : (
79
- <div {...titleClickProps} className={`min-w-0 flex-1 ${titleClickProps?.className ?? ""}`}>
80
- <div className="flex min-w-0 items-center gap-2">
81
- <h2 className="truncate text-base font-bold leading-tight text-black">{title}</h2>
82
- {titleSuffix && <div className="shrink-0">{titleSuffix}</div>}
83
- </div>
84
- {subtitle && (
85
- <p className={`font-mono text-xs text-black/50 ${subtitleMultiline ? "" : "truncate"}`}>
86
- {subtitle}
87
- </p>
88
- )}
89
- </div>
90
- )}
91
- {actions && <div className="flex shrink-0 items-center gap-1.5">{actions}</div>}
92
- </div>
93
- );
94
- }
@@ -1,40 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export const PREVIEW_SHELL_BORDER = "border border-black/15 hover:border-black/30";
4
-
5
- export const PREVIEW_SHELL_SKIN = `${PREVIEW_SHELL_BORDER} bg-white hover:bg-black/5`;
6
-
7
- export const PREVIEW_SHELL_SKIN_MUTED =
8
- "border-2 border-black/30 bg-black/5 italic text-black/40 hover:shadow-brutal-sm";
9
-
10
- export type PreviewShellVariant = "default" | "muted";
11
-
12
- export interface PreviewShellProps {
13
- variant?: PreviewShellVariant;
14
- onClick?: () => void;
15
- className?: string;
16
- "data-testid"?: string;
17
- title?: string;
18
- "aria-busy"?: "true" | "false";
19
- children: ReactNode;
20
- }
21
-
22
- export default function PreviewShell({
23
- variant = "default",
24
- onClick,
25
- className,
26
- children,
27
- ...rest
28
- }: PreviewShellProps) {
29
- const Root = onClick ? "button" : "div";
30
- const skin = variant === "muted" ? PREVIEW_SHELL_SKIN_MUTED : PREVIEW_SHELL_SKIN;
31
- return (
32
- <Root
33
- {...(onClick ? { type: "button" as const, onClick } : {})}
34
- {...rest}
35
- className={`${skin}${className ? ` ${className}` : ""}`}
36
- >
37
- {children}
38
- </Root>
39
- );
40
- }