raft-ui 0.0.6 → 0.0.8

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 (138) hide show
  1. package/CHANGELOG.md +7 -5
  2. package/dist/index.d.mts +618 -2
  3. package/dist/index.mjs +1205 -12
  4. package/{src → dist}/styles.css +0 -2
  5. package/{src/lib/styled-props.ts → dist/tv-UUMwNowu.d.mts} +6 -1
  6. package/dist/use-theme-Y_h1DKll.mjs +34 -0
  7. package/dist/wip.d.mts +15 -133
  8. package/dist/wip.mjs +23 -296
  9. package/package.json +4 -4
  10. package/dist/button-CJqMhTPQ.mjs +0 -621
  11. package/dist/index-CpGciYPW.d.mts +0 -267
  12. package/src/components/alert-dialog/alert-dialog.tsx +0 -215
  13. package/src/components/alert-dialog/index.ts +0 -30
  14. package/src/components/avatar/avatar.tsx +0 -375
  15. package/src/components/avatar/index.ts +0 -17
  16. package/src/components/badge/badge.tsx +0 -393
  17. package/src/components/badge/index.ts +0 -2
  18. package/src/components/banner/banner.tsx +0 -269
  19. package/src/components/banner/index.ts +0 -8
  20. package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
  21. package/src/components/bottom-sheet/index.ts +0 -22
  22. package/src/components/button/button.tsx +0 -499
  23. package/src/components/button/index.ts +0 -2
  24. package/src/components/checkbox/checkbox-indicator.tsx +0 -128
  25. package/src/components/checkbox/checkbox.tsx +0 -350
  26. package/src/components/checkbox/index.ts +0 -2
  27. package/src/components/combobox/combobox.tsx +0 -603
  28. package/src/components/combobox/index.ts +0 -52
  29. package/src/components/context-menu/context-menu.tsx +0 -380
  30. package/src/components/context-menu/index.ts +0 -34
  31. package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
  32. package/src/components/conversation-preview-card/index.ts +0 -32
  33. package/src/components/description-list/description-list.tsx +0 -116
  34. package/src/components/description-list/index.ts +0 -14
  35. package/src/components/dialog/dialog.tsx +0 -152
  36. package/src/components/dialog/index.ts +0 -26
  37. package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
  38. package/src/components/dropdown-menu/index.ts +0 -38
  39. package/src/components/empty-state/empty-state.tsx +0 -177
  40. package/src/components/empty-state/index.ts +0 -16
  41. package/src/components/field/field.tsx +0 -125
  42. package/src/components/field/index.ts +0 -18
  43. package/src/components/inline-code/index.ts +0 -2
  44. package/src/components/inline-code/inline-code.tsx +0 -35
  45. package/src/components/input/index.ts +0 -2
  46. package/src/components/input/input.tsx +0 -76
  47. package/src/components/input-group/index.ts +0 -7
  48. package/src/components/input-group/input-group.tsx +0 -215
  49. package/src/components/kbd/index.ts +0 -2
  50. package/src/components/kbd/kbd.tsx +0 -43
  51. package/src/components/label/index.ts +0 -2
  52. package/src/components/label/label.tsx +0 -99
  53. package/src/components/lightbox/index.ts +0 -24
  54. package/src/components/lightbox/lightbox.tsx +0 -245
  55. package/src/components/list-item/index.ts +0 -20
  56. package/src/components/list-item/list-item.tsx +0 -263
  57. package/src/components/media-list-item/index.ts +0 -20
  58. package/src/components/media-list-item/media-list-item.tsx +0 -169
  59. package/src/components/notification-center/index.ts +0 -39
  60. package/src/components/notification-center/notification-center.tsx +0 -558
  61. package/src/components/panel-header/index.ts +0 -22
  62. package/src/components/panel-header/panel-header.tsx +0 -199
  63. package/src/components/popover/index.ts +0 -30
  64. package/src/components/popover/popover.tsx +0 -248
  65. package/src/components/preview-shell/index.ts +0 -2
  66. package/src/components/preview-shell/preview-shell.tsx +0 -51
  67. package/src/components/quoted-message-card/index.ts +0 -36
  68. package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
  69. package/src/components/radio-group/index.ts +0 -2
  70. package/src/components/radio-group/radio-group.tsx +0 -119
  71. package/src/components/section-header/index.ts +0 -16
  72. package/src/components/section-header/section-header.tsx +0 -117
  73. package/src/components/section-label/index.ts +0 -2
  74. package/src/components/section-label/section-label.tsx +0 -38
  75. package/src/components/segmented-control/index.ts +0 -12
  76. package/src/components/segmented-control/segmented-control.tsx +0 -129
  77. package/src/components/select/index.ts +0 -40
  78. package/src/components/select/select.tsx +0 -382
  79. package/src/components/separator/index.ts +0 -2
  80. package/src/components/separator/separator.tsx +0 -40
  81. package/src/components/spinner/index.ts +0 -2
  82. package/src/components/spinner/spinner.tsx +0 -228
  83. package/src/components/status/index.ts +0 -2
  84. package/src/components/status/status.tsx +0 -152
  85. package/src/components/tabs/index.ts +0 -24
  86. package/src/components/tabs/tabs.tsx +0 -451
  87. package/src/components/text/index.ts +0 -2
  88. package/src/components/text/text.tsx +0 -131
  89. package/src/components/textarea/index.ts +0 -2
  90. package/src/components/textarea/textarea.tsx +0 -136
  91. package/src/components/toggle-group/index.ts +0 -7
  92. package/src/components/toggle-group/toggle-group.tsx +0 -111
  93. package/src/components/tooltip/index.ts +0 -8
  94. package/src/components/tooltip/tooltip.tsx +0 -212
  95. package/src/index.ts +0 -429
  96. package/src/legacy/AttentionDot.tsx +0 -28
  97. package/src/legacy/AvatarListRow.tsx +0 -87
  98. package/src/legacy/AvatarSlot.tsx +0 -157
  99. package/src/legacy/Badge.tsx +0 -95
  100. package/src/legacy/Banner.tsx +0 -74
  101. package/src/legacy/BottomSheet.tsx +0 -76
  102. package/src/legacy/CheckMarker.tsx +0 -72
  103. package/src/legacy/Checkbox.tsx +0 -81
  104. package/src/legacy/ConversationPreviewCard.tsx +0 -169
  105. package/src/legacy/DismissBackdrop.tsx +0 -68
  106. package/src/legacy/EmptyState.tsx +0 -63
  107. package/src/legacy/ExperimentalBadge.tsx +0 -19
  108. package/src/legacy/FormField.tsx +0 -106
  109. package/src/legacy/KeyValueRow.tsx +0 -67
  110. package/src/legacy/Lightbox.tsx +0 -119
  111. package/src/legacy/NotificationCenter.tsx +0 -217
  112. package/src/legacy/PanelHeader.tsx +0 -94
  113. package/src/legacy/PreviewShell.tsx +0 -40
  114. package/src/legacy/QuotedMessageCard.tsx +0 -181
  115. package/src/legacy/ReorderablePanelTabs.tsx +0 -156
  116. package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
  117. package/src/legacy/SearchInput.tsx +0 -23
  118. package/src/legacy/SectionEyebrow.tsx +0 -60
  119. package/src/legacy/SectionHeader.tsx +0 -68
  120. package/src/legacy/SegmentedControl.tsx +0 -74
  121. package/src/legacy/SlugInput.tsx +0 -29
  122. package/src/legacy/Spinner.tsx +0 -89
  123. package/src/legacy/StatusDot.tsx +0 -51
  124. package/src/legacy/SurfaceListItem.tsx +0 -32
  125. package/src/legacy/Textarea.tsx +0 -137
  126. package/src/legacy/button.tsx +0 -100
  127. package/src/legacy/context-menu-divider.tsx +0 -12
  128. package/src/legacy/menu-item.tsx +0 -66
  129. package/src/legacy/selection-popover.tsx +0 -212
  130. package/src/legacy/server-switcher-menu.tsx +0 -203
  131. package/src/legacy/source.css +0 -240
  132. package/src/lib/cn.ts +0 -1
  133. package/src/lib/theme/theme-context.ts +0 -28
  134. package/src/lib/theme/theme-provider.tsx +0 -181
  135. package/src/lib/theme/use-theme.ts +0 -12
  136. package/src/lib/tv.ts +0 -29
  137. package/src/lib/without-children.ts +0 -6
  138. 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
- }