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,181 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { Hash, MessageSquare } from "lucide-react";
3
- import AvatarSlot from "./AvatarSlot";
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";
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
- }