raft-ui 0.0.5 → 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 (133) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/button-CJqMhTPQ.mjs +621 -0
  3. package/dist/index-CpGciYPW.d.mts +267 -0
  4. package/dist/index.d.mts +11 -253
  5. package/dist/index.mjs +32 -587
  6. package/{src → dist}/styles.css +0 -2
  7. package/dist/wip.d.mts +133 -15
  8. package/dist/wip.mjs +289 -16
  9. package/package.json +7 -10
  10. package/dist/styled-props-B3SNekJg.d.mts +0 -16
  11. package/dist/use-theme-Y_h1DKll.mjs +0 -34
  12. package/src/components/avatar/avatar.tsx +0 -375
  13. package/src/components/avatar/index.ts +0 -17
  14. package/src/components/badge/badge.tsx +0 -393
  15. package/src/components/badge/index.ts +0 -2
  16. package/src/components/banner/banner.tsx +0 -269
  17. package/src/components/banner/index.ts +0 -8
  18. package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
  19. package/src/components/bottom-sheet/index.ts +0 -22
  20. package/src/components/button/button.tsx +0 -497
  21. package/src/components/button/index.ts +0 -2
  22. package/src/components/checkbox/checkbox-indicator.tsx +0 -128
  23. package/src/components/checkbox/checkbox.tsx +0 -350
  24. package/src/components/checkbox/index.ts +0 -2
  25. package/src/components/combobox/combobox.tsx +0 -603
  26. package/src/components/combobox/index.ts +0 -52
  27. package/src/components/context-menu/context-menu.tsx +0 -380
  28. package/src/components/context-menu/index.ts +0 -34
  29. package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
  30. package/src/components/conversation-preview-card/index.ts +0 -32
  31. package/src/components/description-list/description-list.tsx +0 -116
  32. package/src/components/description-list/index.ts +0 -14
  33. package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
  34. package/src/components/dropdown-menu/index.ts +0 -38
  35. package/src/components/empty-state/empty-state.tsx +0 -177
  36. package/src/components/empty-state/index.ts +0 -16
  37. package/src/components/field/field.tsx +0 -125
  38. package/src/components/field/index.ts +0 -18
  39. package/src/components/inline-code/index.ts +0 -2
  40. package/src/components/inline-code/inline-code.tsx +0 -35
  41. package/src/components/input/index.ts +0 -2
  42. package/src/components/input/input.tsx +0 -76
  43. package/src/components/input-group/index.ts +0 -7
  44. package/src/components/input-group/input-group.tsx +0 -215
  45. package/src/components/kbd/index.ts +0 -2
  46. package/src/components/kbd/kbd.tsx +0 -43
  47. package/src/components/label/index.ts +0 -7
  48. package/src/components/label/label.tsx +0 -99
  49. package/src/components/lightbox/index.ts +0 -24
  50. package/src/components/lightbox/lightbox.tsx +0 -245
  51. package/src/components/list-item/index.ts +0 -20
  52. package/src/components/list-item/list-item.tsx +0 -263
  53. package/src/components/media-list-item/index.ts +0 -20
  54. package/src/components/media-list-item/media-list-item.tsx +0 -169
  55. package/src/components/notification-center/index.ts +0 -39
  56. package/src/components/notification-center/notification-center.tsx +0 -558
  57. package/src/components/panel-header/index.ts +0 -22
  58. package/src/components/panel-header/panel-header.tsx +0 -199
  59. package/src/components/popover/index.ts +0 -30
  60. package/src/components/popover/popover.tsx +0 -248
  61. package/src/components/preview-shell/index.ts +0 -2
  62. package/src/components/preview-shell/preview-shell.tsx +0 -51
  63. package/src/components/quoted-message-card/index.ts +0 -36
  64. package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
  65. package/src/components/radio-group/index.ts +0 -6
  66. package/src/components/radio-group/radio-group.tsx +0 -119
  67. package/src/components/section-header/index.ts +0 -16
  68. package/src/components/section-header/section-header.tsx +0 -117
  69. package/src/components/section-label/index.ts +0 -2
  70. package/src/components/section-label/section-label.tsx +0 -38
  71. package/src/components/segmented-control/index.ts +0 -12
  72. package/src/components/segmented-control/segmented-control.tsx +0 -129
  73. package/src/components/select/index.ts +0 -40
  74. package/src/components/select/select.tsx +0 -382
  75. package/src/components/spinner/index.ts +0 -2
  76. package/src/components/spinner/spinner.tsx +0 -228
  77. package/src/components/status/index.ts +0 -2
  78. package/src/components/status/status.tsx +0 -152
  79. package/src/components/tabs/index.ts +0 -24
  80. package/src/components/tabs/tabs.tsx +0 -451
  81. package/src/components/text/index.ts +0 -2
  82. package/src/components/text/text.tsx +0 -131
  83. package/src/components/textarea/index.ts +0 -2
  84. package/src/components/textarea/textarea.tsx +0 -136
  85. package/src/components/toggle-group/index.ts +0 -12
  86. package/src/components/toggle-group/toggle-group.tsx +0 -111
  87. package/src/components/tooltip/index.ts +0 -14
  88. package/src/components/tooltip/tooltip.tsx +0 -212
  89. package/src/index.ts +0 -426
  90. package/src/legacy/AttentionDot.tsx +0 -28
  91. package/src/legacy/AvatarListRow.tsx +0 -87
  92. package/src/legacy/AvatarSlot.tsx +0 -157
  93. package/src/legacy/Badge.tsx +0 -95
  94. package/src/legacy/Banner.tsx +0 -74
  95. package/src/legacy/BottomSheet.tsx +0 -76
  96. package/src/legacy/CheckMarker.tsx +0 -72
  97. package/src/legacy/Checkbox.tsx +0 -81
  98. package/src/legacy/ConversationPreviewCard.tsx +0 -169
  99. package/src/legacy/DismissBackdrop.tsx +0 -68
  100. package/src/legacy/EmptyState.tsx +0 -63
  101. package/src/legacy/ExperimentalBadge.tsx +0 -19
  102. package/src/legacy/FormField.tsx +0 -106
  103. package/src/legacy/KeyValueRow.tsx +0 -67
  104. package/src/legacy/Lightbox.tsx +0 -119
  105. package/src/legacy/NotificationCenter.tsx +0 -217
  106. package/src/legacy/PanelHeader.tsx +0 -94
  107. package/src/legacy/PreviewShell.tsx +0 -40
  108. package/src/legacy/QuotedMessageCard.tsx +0 -181
  109. package/src/legacy/ReorderablePanelTabs.tsx +0 -156
  110. package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
  111. package/src/legacy/SearchInput.tsx +0 -23
  112. package/src/legacy/SectionEyebrow.tsx +0 -60
  113. package/src/legacy/SectionHeader.tsx +0 -68
  114. package/src/legacy/SegmentedControl.tsx +0 -74
  115. package/src/legacy/SlugInput.tsx +0 -29
  116. package/src/legacy/Spinner.tsx +0 -89
  117. package/src/legacy/StatusDot.tsx +0 -51
  118. package/src/legacy/SurfaceListItem.tsx +0 -32
  119. package/src/legacy/Textarea.tsx +0 -137
  120. package/src/legacy/button.tsx +0 -100
  121. package/src/legacy/context-menu-divider.tsx +0 -12
  122. package/src/legacy/menu-item.tsx +0 -66
  123. package/src/legacy/selection-popover.tsx +0 -212
  124. package/src/legacy/server-switcher-menu.tsx +0 -203
  125. package/src/legacy/source.css +0 -240
  126. package/src/lib/cn.ts +0 -1
  127. package/src/lib/styled-props.ts +0 -11
  128. package/src/lib/theme/theme-context.ts +0 -28
  129. package/src/lib/theme/theme-provider.tsx +0 -181
  130. package/src/lib/theme/use-theme.ts +0 -12
  131. package/src/lib/tv.ts +0 -29
  132. package/src/lib/without-children.ts +0 -6
  133. package/src/wip.ts +0 -233
@@ -1,181 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { Hash, MessageSquare } from "lucide-react";
3
- import AvatarSlot from "./AvatarSlot.tsx";
4
-
5
- export interface QuotedMessageCardProps {
6
- channelName: string;
7
- channelKind?: "channel" | "dm";
8
- isThread?: boolean;
9
- isArchived?: boolean;
10
- timestamp: string;
11
- author: {
12
- name: string;
13
- avatar?: string;
14
- gravatarHash?: string;
15
- kind: "agent" | "user";
16
- subtitle?: string;
17
- };
18
- content: string;
19
- attachments?: { label: string }[];
20
- onClick?: () => void;
21
- unavailable?: boolean;
22
- }
23
-
24
- export function Tag({ children, className }: { children: ReactNode; className: string }) {
25
- return (
26
- <span
27
- className={`inline-flex items-center gap-1 border-[1.5px] border-black px-1.5 py-0.5 text-[11px] font-bold leading-none ${className}`}
28
- >
29
- {children}
30
- </span>
31
- );
32
- }
33
-
34
- export function ThreadMarker() {
35
- return (
36
- <span className="inline-flex items-center gap-1 font-bold text-brutal-pink">
37
- <MessageSquare size={10} className="shrink-0" />
38
- Thread
39
- </span>
40
- );
41
- }
42
-
43
- function CompactUserAvatar({ author }: { author: QuotedMessageCardProps["author"] }) {
44
- if (author.kind === "agent") {
45
- return (
46
- <AvatarSlot context="compact-list" type="agent" agentAvatarUrl={author.avatar ?? null} />
47
- );
48
- }
49
-
50
- return (
51
- <AvatarSlot
52
- context="compact-list"
53
- type="human"
54
- humanAvatarUrl={author.avatar ?? null}
55
- gravatarHash={author.gravatarHash ?? null}
56
- />
57
- );
58
- }
59
-
60
- function InlineMarkdownPreview({ markdown }: { markdown: string }) {
61
- return <>{markdown}</>;
62
- }
63
-
64
- type PreviewShellProps = {
65
- variant?: "default" | "muted";
66
- onClick?: () => void;
67
- className?: string;
68
- "data-testid"?: string;
69
- children: ReactNode;
70
- };
71
-
72
- function PreviewShell({
73
- variant = "default",
74
- onClick,
75
- className,
76
- children,
77
- ...rest
78
- }: PreviewShellProps) {
79
- const Root = onClick ? "button" : "div";
80
- const skin =
81
- variant === "muted"
82
- ? "border-2 border-black/30 bg-black/5 italic text-black/40 hover:shadow-brutal-sm"
83
- : "border border-black/15 bg-white hover:border-black/30 hover:bg-black/5";
84
-
85
- return (
86
- <Root
87
- {...(onClick ? { type: "button" as const, onClick } : {})}
88
- {...rest}
89
- className={`${skin}${className ? ` ${className}` : ""}`}
90
- >
91
- {children}
92
- </Root>
93
- );
94
- }
95
-
96
- export default function QuotedMessageCard({
97
- channelName,
98
- channelKind = "channel",
99
- isThread = false,
100
- isArchived = false,
101
- timestamp,
102
- author,
103
- content,
104
- attachments,
105
- onClick,
106
- unavailable = false,
107
- }: QuotedMessageCardProps) {
108
- if (unavailable) {
109
- return (
110
- <PreviewShell
111
- variant="muted"
112
- onClick={onClick}
113
- data-testid="quoted-message-card"
114
- className="group block w-full text-left"
115
- >
116
- <div className="px-2.5 py-2 sm:px-3 sm:py-2.5">
117
- <div className="text-[13px] leading-snug">Message unavailable</div>
118
- </div>
119
- </PreviewShell>
120
- );
121
- }
122
-
123
- return (
124
- <PreviewShell
125
- onClick={onClick}
126
- data-testid="quoted-message-card"
127
- className="group block w-full text-left"
128
- >
129
- <div className="px-2.5 py-2 sm:px-3 sm:py-2.5">
130
- <div className="mb-1 flex items-start gap-2 sm:mb-1.5">
131
- <div className="min-w-0 flex flex-1 flex-wrap items-center gap-x-1.5 gap-y-1 text-xs leading-none">
132
- <span className="inline-flex items-center gap-1 font-bold text-black/70">
133
- {channelKind === "dm" ? null : <Hash size={11} className="shrink-0" />}
134
- {channelKind === "dm" ? `@${channelName}` : channelName}
135
- </span>
136
- <span className="text-black/30">·</span>
137
- <span className="inline-flex min-w-0 items-center gap-1.5">
138
- <CompactUserAvatar author={author} />
139
- <span className="truncate font-bold text-black/70">{author.name}</span>
140
- {author.subtitle ? (
141
- <span className="truncate font-mono text-[10px] text-black/40">
142
- {author.subtitle}
143
- </span>
144
- ) : null}
145
- </span>
146
- {isThread ? (
147
- <>
148
- <span className="text-black/30">·</span>
149
- <ThreadMarker />
150
- </>
151
- ) : null}
152
- {isArchived ? (
153
- <>
154
- <span className="text-black/30">·</span>
155
- <Tag className="bg-brutal-orange/30 text-black">Archived</Tag>
156
- </>
157
- ) : null}
158
- </div>
159
- <span className="shrink-0 font-mono text-[10px] text-black/40">{timestamp}</span>
160
- </div>
161
- <div className="min-w-0">
162
- <div className="line-clamp-2 text-xs leading-snug text-black sm:text-[13px]">
163
- <InlineMarkdownPreview markdown={content} />
164
- </div>
165
- {attachments?.length ? (
166
- <div className="mt-2 flex flex-wrap gap-1.5">
167
- {attachments.map((attachment) => (
168
- <Tag
169
- key={attachment.label}
170
- className="bg-brutal-lavender font-mono text-[10px] font-bold text-black"
171
- >
172
- {attachment.label}
173
- </Tag>
174
- ))}
175
- </div>
176
- ) : null}
177
- </div>
178
- </div>
179
- </PreviewShell>
180
- );
181
- }
@@ -1,156 +0,0 @@
1
- import { useCallback, useMemo, type ComponentType, type RefObject } from "react";
2
- import {
3
- closestCenter,
4
- DndContext,
5
- PointerSensor,
6
- TouchSensor,
7
- useSensor,
8
- useSensors,
9
- type DragEndEvent,
10
- } from "@dnd-kit/core";
11
- import {
12
- arrayMove,
13
- horizontalListSortingStrategy,
14
- SortableContext,
15
- useSortable,
16
- } from "@dnd-kit/sortable";
17
- import { CSS } from "@dnd-kit/utilities";
18
-
19
- export interface PanelTabItem<T extends string> {
20
- id: T;
21
- label: string;
22
- icon: ComponentType<{ size?: number; className?: string }>;
23
- }
24
-
25
- function reconcilePanelTabOrder<T extends string>(
26
- tabs: readonly PanelTabItem<T>[],
27
- order: readonly string[],
28
- ): PanelTabItem<T>[] {
29
- const byId = new Map(tabs.map((tab) => [tab.id, tab]));
30
- const next: PanelTabItem<T>[] = [];
31
- const seen = new Set<string>();
32
-
33
- for (const id of order) {
34
- const tab = byId.get(id as T);
35
- if (!tab || seen.has(tab.id)) continue;
36
- next.push(tab);
37
- seen.add(tab.id);
38
- }
39
-
40
- for (const tab of tabs) {
41
- if (seen.has(tab.id)) continue;
42
- next.push(tab);
43
- seen.add(tab.id);
44
- }
45
-
46
- return next;
47
- }
48
-
49
- export function useOrderedPanelTabs<T extends string>(
50
- tabs: readonly PanelTabItem<T>[],
51
- storedOrder: readonly string[],
52
- ) {
53
- return useMemo(() => reconcilePanelTabOrder(tabs, storedOrder), [tabs, storedOrder]);
54
- }
55
-
56
- function SortablePanelTab<T extends string>({
57
- tab,
58
- active,
59
- activeRef,
60
- onClick,
61
- }: {
62
- tab: PanelTabItem<T>;
63
- active: boolean;
64
- activeRef?: RefObject<HTMLButtonElement | null>;
65
- onClick: () => void;
66
- }) {
67
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
68
- id: tab.id,
69
- });
70
- const Icon = tab.icon;
71
- const horizontalTransform = transform ? { ...transform, y: 0 } : null;
72
- const style = {
73
- transform: CSS.Transform.toString(horizontalTransform),
74
- transition,
75
- zIndex: isDragging ? 20 : undefined,
76
- };
77
-
78
- return (
79
- <button
80
- ref={(node) => {
81
- setNodeRef(node);
82
- if (activeRef) activeRef.current = node;
83
- }}
84
- type="button"
85
- onClick={onClick}
86
- className={`flex shrink-0 touch-manipulation items-center gap-1.5 whitespace-nowrap border-r-2 border-black px-4 py-1.5 text-xs font-bold transition-colors ${
87
- active ? "bg-brutal-yellow" : "bg-white hover:bg-black/5"
88
- }`}
89
- style={style}
90
- data-testid={`panel-tab-${tab.id}`}
91
- {...attributes}
92
- {...listeners}
93
- >
94
- <Icon size={12} />
95
- <span>{tab.label}</span>
96
- </button>
97
- );
98
- }
99
-
100
- export default function ReorderablePanelTabs<T extends string>({
101
- tabs,
102
- activeTab,
103
- activeTabRef,
104
- onSelectTab,
105
- onReorderTabs,
106
- }: {
107
- tabs: readonly PanelTabItem<T>[];
108
- activeTab: T;
109
- activeTabRef?: RefObject<HTMLButtonElement | null>;
110
- onSelectTab: (tab: T) => void;
111
- onReorderTabs: (tabs: T[]) => void;
112
- }) {
113
- const sensors = useSensors(
114
- useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
115
- useSensor(TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
116
- );
117
-
118
- const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]);
119
- const handleDragEnd = useCallback(
120
- ({ active, over }: DragEndEvent) => {
121
- if (!over || active.id === over.id) return;
122
- const activeId = String(active.id) as T;
123
- const overId = String(over.id) as T;
124
- const oldIndex = tabIds.indexOf(activeId);
125
- const newIndex = tabIds.indexOf(overId);
126
- if (oldIndex === -1 || newIndex === -1) return;
127
- onReorderTabs(arrayMove(tabIds, oldIndex, newIndex));
128
- },
129
- [onReorderTabs, tabIds],
130
- );
131
-
132
- return (
133
- <div className="overflow-x-auto border-b-2 border-black bg-white scrollbar-none">
134
- <DndContext
135
- autoScroll={false}
136
- sensors={sensors}
137
- collisionDetection={closestCenter}
138
- onDragEnd={handleDragEnd}
139
- >
140
- <SortableContext items={tabIds} strategy={horizontalListSortingStrategy}>
141
- <div className="flex min-w-max">
142
- {tabs.map((tab) => (
143
- <SortablePanelTab
144
- key={tab.id}
145
- tab={tab}
146
- active={activeTab === tab.id}
147
- activeRef={activeTab === tab.id ? activeTabRef : undefined}
148
- onClick={() => onSelectTab(tab.id)}
149
- />
150
- ))}
151
- </div>
152
- </SortableContext>
153
- </DndContext>
154
- </div>
155
- );
156
- }
@@ -1,59 +0,0 @@
1
- import type { CSSProperties } from "react";
2
-
3
- /**
4
- * Shared isolation primitive for previewing attacker-controlled content
5
- * (anyone — human or agent — can upload a hostile attachment, or author a
6
- * markdown ```mermaid block).
7
- *
8
- * The iframe is the real security boundary: with no `allow-same-origin` in
9
- * `sandbox`, the framed content runs in an opaque origin and cannot touch
10
- * Raft's DOM / cookies / localStorage. This is strictly stronger than
11
- * sanitizing a string and hoping the sanitizer has no gaps.
12
- *
13
- * Two callsites share this one implementation so the isolation config can't
14
- * drift between them:
15
- * - HTML attachment preview: `src` (scoped backend URL) + `sandbox="allow-scripts"`
16
- * (interactive HTML/diagrams need scripts; still no same-origin).
17
- * - Mermaid diagram: `srcDoc` (front-end-generated SVG doc) + empty `sandbox`
18
- * (static SVG needs zero scripts — the most locked-down setting).
19
- *
20
- * @Bugen security review 2026-05-18 #proj-uiux:df4d393b.
21
- */
22
- export interface SandboxedPreviewFrameProps {
23
- /** External (scoped) URL to frame. Mutually exclusive with `srcDoc`. */
24
- src?: string;
25
- /** Inline document to frame. Mutually exclusive with `src`. */
26
- srcDoc?: string;
27
- /**
28
- * iframe `sandbox` token list. Default `""` = maximally locked (no scripts,
29
- * no same-origin). Never pass `allow-same-origin` for untrusted content —
30
- * it dissolves the origin boundary.
31
- */
32
- sandbox?: string;
33
- title: string;
34
- className?: string;
35
- style?: CSSProperties;
36
- referrerPolicy?: React.HTMLAttributeReferrerPolicy;
37
- }
38
-
39
- export default function SandboxedPreviewFrame({
40
- src,
41
- srcDoc,
42
- sandbox = "",
43
- title,
44
- className,
45
- style,
46
- referrerPolicy = "no-referrer",
47
- }: SandboxedPreviewFrameProps) {
48
- return (
49
- <iframe
50
- title={title}
51
- src={src}
52
- srcDoc={srcDoc}
53
- sandbox={sandbox}
54
- referrerPolicy={referrerPolicy}
55
- className={className}
56
- style={style}
57
- />
58
- );
59
- }
@@ -1,23 +0,0 @@
1
- import type { InputHTMLAttributes } from "react";
2
- import { Search } from "lucide-react";
3
-
4
- type SearchInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "className"> & {
5
- className?: string;
6
- inputClassName?: string;
7
- };
8
-
9
- export default function SearchInput({
10
- className = "",
11
- inputClassName = "",
12
- ...props
13
- }: SearchInputProps) {
14
- return (
15
- <div className={`input-brutal flex w-full items-center gap-2 ${className}`.trim()}>
16
- <Search size={15} className="shrink-0 text-black/50" />
17
- <input
18
- {...props}
19
- className={`min-w-0 flex-1 bg-transparent font-heading outline-none ${inputClassName}`.trim()}
20
- />
21
- </div>
22
- );
23
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Canonical section-eyebrow label — replaces the inline
3
- * `text-xs font-bold uppercase text-black/60 tracking-widest` pattern
4
- * that appears 40+ times across panels, dialogs, and list headers.
5
- *
6
- * stdrc 2026-05-11 #wg-theme:93c5897c assigned this to @Wug as the
7
- * next Tier 1 component after PanelHeader / AvatarSlot / AvatarListRow /
8
- * Banner. Per @Duoyu audit msg=cc6f5542 the eyebrow class string was the
9
- * single biggest repeated typographic pattern in the codebase.
10
- *
11
- * The component renders the locked typographic token and accepts:
12
- * - `as` — element type (`"span"` | `"div"` | `"label"`, default `"span"`)
13
- * - `className` — callsite-specific additions (margin, padding, bg, color)
14
- * - `htmlFor` — forwarded when `as="label"`
15
- * - `children` — label text
16
- *
17
- * Do NOT inline new section eyebrows — use this primitive instead.
18
- *
19
- * Default color is `text-black/60`. To get a fully-black eyebrow
20
- * (e.g. Sidebar section toggle, MachineDetailPanel running label),
21
- * pass `className="!text-black"` to override.
22
- */
23
-
24
- import type { LabelHTMLAttributes, ReactNode } from "react";
25
-
26
- const BASE_CLASS = "text-xs font-bold uppercase text-black/60 tracking-widest";
27
-
28
- export type SectionEyebrowProps = {
29
- /** HTML element to render. Default `"span"`. Use `"div"` for block-level
30
- * eyebrows (e.g. with `mb-3`), `"label"` for form field labels. */
31
- as?: "span" | "div" | "label";
32
- /** Additional classes — appended to the base token. Useful for margin
33
- * (`mb-3`), padding (`px-3 py-1.5`), background (`bg-white/50`), or
34
- * color overrides (`!text-black`). */
35
- className?: string;
36
- /** Eyebrow text / nodes. */
37
- children?: ReactNode;
38
- /** When `as="label"`, forward to the `<label htmlFor>` attribute. */
39
- htmlFor?: LabelHTMLAttributes<HTMLLabelElement>["htmlFor"];
40
- };
41
-
42
- export default function SectionEyebrow({
43
- as: Tag = "span",
44
- className = "",
45
- children,
46
- htmlFor,
47
- }: SectionEyebrowProps) {
48
- const cls = `${BASE_CLASS} ${className}`.trim();
49
- if (Tag === "label") {
50
- return (
51
- <label className={cls} htmlFor={htmlFor}>
52
- {children}
53
- </label>
54
- );
55
- }
56
- if (Tag === "div") {
57
- return <div className={cls}>{children}</div>;
58
- }
59
- return <span className={cls}>{children}</span>;
60
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Canonical section header — eyebrow label + optional count badge + optional
3
- * trailing action button.
4
- *
5
- * Generalizes the inline `flex items-center justify-between` + `SectionEyebrow`
6
- * + counter + action button pattern that appears 15+ times across detail
7
- * panels, settings, channel members, agent reminders, etc.
8
- *
9
- * Background: stdrc 2026-05-12 #wg-theme:a987f888 asked for strong-signal
10
- * primitives to be lifted in one PR. SectionHeader is the third of the three.
11
- * It composes the existing `SectionEyebrow` primitive (so the eyebrow token
12
- * stays canonical) rather than duplicating its class string.
13
- *
14
- * Slots:
15
- * - `label` — the eyebrow text
16
- * - `icon` — optional leading icon node (e.g. `<Bell size={16} />`),
17
- * rendered in `text-black/60` next to the eyebrow
18
- * - `count` — optional integer badge rendered after the label in
19
- * `text-black/40 font-mono` (rendered only when defined and
20
- * ≥ 0; pass `undefined` / `null` to hide)
21
- * - `action` — optional right-aligned action node (e.g. `<button>` to add)
22
- * - `htmlFor` — when set, the eyebrow becomes a `<label htmlFor>` for the
23
- * region's main control
24
- * - `className` — additional classes on the outer wrapper (e.g. `mb-2`,
25
- * `border-b-2 border-black pb-2`)
26
- *
27
- * Typography is locked via `SectionEyebrow`; do NOT pass eyebrow class
28
- * overrides here — if a section needs a different style, it shouldn't be a
29
- * SectionHeader.
30
- */
31
-
32
- import type { ReactNode } from "react";
33
- import SectionEyebrow from "./SectionEyebrow.tsx";
34
-
35
- export type SectionHeaderProps = {
36
- label: ReactNode;
37
- icon?: ReactNode;
38
- count?: number | null;
39
- action?: ReactNode;
40
- htmlFor?: string;
41
- className?: string;
42
- };
43
-
44
- export default function SectionHeader({
45
- label,
46
- icon,
47
- count,
48
- action,
49
- htmlFor,
50
- className,
51
- }: SectionHeaderProps) {
52
- const wrapperCls = `flex items-center justify-between gap-2${className ? ` ${className}` : ""}`;
53
- const eyebrowAs: "label" | "div" = htmlFor ? "label" : "div";
54
- return (
55
- <div className={wrapperCls}>
56
- <div className="flex min-w-0 items-center gap-2">
57
- {icon ? <span className="shrink-0 text-black/60">{icon}</span> : null}
58
- <SectionEyebrow as={eyebrowAs} htmlFor={htmlFor}>
59
- {label}
60
- {typeof count === "number" && count >= 0 ? (
61
- <span className="ml-2 font-mono text-black/40">{count}</span>
62
- ) : null}
63
- </SectionEyebrow>
64
- </div>
65
- {action ? <div className="shrink-0">{action}</div> : null}
66
- </div>
67
- );
68
- }
@@ -1,74 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export interface SegmentedControlOption<T extends string> {
4
- value: T;
5
- label: ReactNode;
6
- count?: number;
7
- icon?: ReactNode;
8
- testId?: string;
9
- }
10
-
11
- export interface SegmentedControlProps<T extends string> {
12
- value: T;
13
- options: SegmentedControlOption<T>[];
14
- onChange: (value: T) => void;
15
- ariaLabel?: string;
16
- disabled?: boolean;
17
- className?: string;
18
- }
19
-
20
- export default function SegmentedControl<T extends string>({
21
- value,
22
- options,
23
- onChange,
24
- ariaLabel,
25
- disabled = false,
26
- className = "",
27
- }: SegmentedControlProps<T>) {
28
- return (
29
- <div
30
- role="radiogroup"
31
- aria-label={ariaLabel}
32
- aria-disabled={disabled || undefined}
33
- className={`inline-flex max-w-full flex-wrap items-center gap-1 ${
34
- disabled ? "opacity-50" : ""
35
- } ${className}`}
36
- >
37
- {options.map((option) => {
38
- const active = option.value === value;
39
- return (
40
- <button
41
- key={option.value}
42
- type="button"
43
- role="radio"
44
- aria-checked={active}
45
- data-testid={option.testId}
46
- disabled={disabled}
47
- onClick={() => onChange(option.value)}
48
- className={`flex h-7 min-w-0 shrink-0 items-center gap-1 border-2 border-black px-2 text-xs font-bold transition-colors ${
49
- disabled ? "cursor-not-allowed" : ""
50
- } ${
51
- active
52
- ? "bg-brutal-yellow text-black shadow-brutal-sm"
53
- : disabled
54
- ? "bg-white text-black"
55
- : "bg-white text-black hover:bg-brutal-yellow/30"
56
- }`}
57
- >
58
- {option.icon}
59
- <span className="truncate">{option.label}</span>
60
- {typeof option.count === "number" && option.count > 0 ? (
61
- <span
62
- className={`font-mono text-[10px] leading-none ${
63
- active ? "text-black/60" : "text-black/40"
64
- }`}
65
- >
66
- {option.count}
67
- </span>
68
- ) : null}
69
- </button>
70
- );
71
- })}
72
- </div>
73
- );
74
- }
@@ -1,29 +0,0 @@
1
- import type { InputHTMLAttributes } from "react";
2
-
3
- type SlugInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "className"> & {
4
- className?: string;
5
- inputClassName?: string;
6
- };
7
-
8
- export default function SlugInput({
9
- className = "",
10
- inputClassName = "",
11
- ...props
12
- }: SlugInputProps) {
13
- return (
14
- <div
15
- className={`input-brutal flex w-full items-stretch overflow-hidden px-0 py-0 focus-within:shadow-brutal ${className}`.trim()}
16
- >
17
- <span
18
- aria-hidden="true"
19
- className="flex shrink-0 items-center border-r-2 border-black bg-brutal-yellow/30 px-3 font-heading font-bold text-black/70"
20
- >
21
- /
22
- </span>
23
- <input
24
- {...props}
25
- className={`min-w-0 flex-1 bg-transparent px-3 py-2 font-heading outline-none ${inputClassName}`.trim()}
26
- />
27
- </div>
28
- );
29
- }