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,95 +0,0 @@
1
- import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
2
-
3
- /**
4
- * Badge tones map to the brutal palette. The primitive owns the bg →
5
- * caller never writes `bg-brutal-*` via className. Adding a new tone
6
- * means adding a row here, not a one-off override at the callsite.
7
- *
8
- * Hidden context:
9
- * - PR #1905 / #proj-theme:ac79cf20 stdrc msg=6ce4d8bf — colors /
10
- * uppercase / tracking all used to leak through caller className;
11
- * the primitive now owns them.
12
- * - stdrc msg=6db77368 — "Badges and task status 都应该大写(内容
13
- * 应该是 Title case ... css 在 Brutal 主题下全大写(将来我们会有
14
- * 别的主题))". Content stays Title Case (e.g. "In Progress"); the
15
- * brutal theme's CSS does the visual uppercase + tracking-wide
16
- * transform here in the primitive. A future non-brutal skin can
17
- * render the same Title Case content without uppercase.
18
- * - stdrc msg=ce25da45 (option B msg=65681d15) — status badges that
19
- * sit next to case-sensitive identifiers (e.g. `task #N @assignee`)
20
- * must opt out of uppercase, because `@bugen` ≠ `@BUGEN`. Hence
21
- * `uppercase?: boolean` axis: default true preserves the brutal
22
- * contract for label-only badges; status callsites pass `false`.
23
- */
24
- export const BADGE_TONE_CLASSES = {
25
- white: "bg-white text-black",
26
- yellow: "bg-brutal-yellow text-black",
27
- pink: "bg-brutal-pink text-black",
28
- cyan: "bg-brutal-cyan text-black",
29
- lavender: "bg-brutal-lavender text-black",
30
- orange: "bg-brutal-orange text-black",
31
- lime: "bg-brutal-lime text-black",
32
- red: "bg-brutal-red text-black",
33
- stone: "bg-brutal-stone text-black",
34
- } as const;
35
-
36
- export type BadgeTone = keyof typeof BADGE_TONE_CLASSES;
37
-
38
- interface BadgeStyleProps {
39
- /** Background tone from the brutal palette. Defaults to `white`. */
40
- tone?: BadgeTone;
41
- /**
42
- * Apply the brutal uppercase + tracking-wide transform. Defaults to
43
- * true (label-only badges). Status badges pass `false` because they
44
- * mix label content with case-sensitive identifiers (`@assignee`).
45
- */
46
- uppercase?: boolean;
47
- }
48
-
49
- export function brutalBadgeClassName({
50
- tone = "white",
51
- uppercase = true,
52
- className = "",
53
- }: BadgeStyleProps & { className?: string } = {}) {
54
- return [
55
- "inline-flex h-5 min-w-0 max-w-full items-center gap-1 overflow-hidden whitespace-nowrap border border-black px-1.5 text-[10px] font-bold leading-none",
56
- uppercase ? "uppercase tracking-wide" : null,
57
- BADGE_TONE_CLASSES[tone],
58
- className,
59
- ]
60
- .filter(Boolean)
61
- .join(" ");
62
- }
63
-
64
- export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, BadgeStyleProps {
65
- children?: ReactNode;
66
- }
67
-
68
- export function Badge({ tone, uppercase, className = "", children, ...props }: BadgeProps) {
69
- return (
70
- <span className={brutalBadgeClassName({ tone, uppercase, className })} {...props}>
71
- {children}
72
- </span>
73
- );
74
- }
75
-
76
- export interface BadgeButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, BadgeStyleProps {
77
- children?: ReactNode;
78
- }
79
-
80
- export function BadgeButton({
81
- tone,
82
- uppercase,
83
- className = "",
84
- type = "button",
85
- children,
86
- ...props
87
- }: BadgeButtonProps) {
88
- return (
89
- <button type={type} className={brutalBadgeClassName({ tone, uppercase, className })} {...props}>
90
- {children}
91
- </button>
92
- );
93
- }
94
-
95
- export default Badge;
@@ -1,74 +0,0 @@
1
- import { AlertTriangle, CheckCircle2, CircleAlert, Info } from "lucide-react";
2
- import type { HTMLAttributes, ReactNode } from "react";
3
-
4
- export type BannerIntent = "destructive" | "warning" | "info" | "success";
5
- export type BannerDensity = "sm" | "md" | "lg";
6
-
7
- const INTENT_SURFACE: Record<BannerIntent, string> = {
8
- destructive: "bg-brutal-red/20",
9
- warning: "bg-brutal-orange/20",
10
- info: "bg-brutal-yellow/30",
11
- success: "bg-brutal-lime/20",
12
- };
13
-
14
- const INTENT_DEFAULT_ICON: Record<BannerIntent, ReactNode> = {
15
- destructive: <AlertTriangle size={18} className="mt-0.5 shrink-0 text-black" />,
16
- warning: <CircleAlert size={18} className="mt-0.5 shrink-0 text-black" />,
17
- info: <Info size={18} className="mt-0.5 shrink-0 text-black" />,
18
- success: <CheckCircle2 size={18} className="mt-0.5 shrink-0 text-black" />,
19
- };
20
-
21
- const DENSITY_PADDING: Record<BannerDensity, string> = {
22
- sm: "p-2",
23
- md: "p-3",
24
- lg: "p-4 shadow-brutal-sm",
25
- };
26
-
27
- const DENSITY_TEXT: Record<BannerDensity, string> = {
28
- sm: "text-xs",
29
- md: "text-sm",
30
- lg: "text-sm",
31
- };
32
-
33
- const DENSITY_GAP: Record<BannerDensity, string> = {
34
- sm: "gap-2",
35
- md: "gap-3",
36
- lg: "gap-3",
37
- };
38
-
39
- export interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
40
- intent: BannerIntent;
41
- density?: BannerDensity;
42
- title?: ReactNode;
43
- children?: ReactNode;
44
- withIcon?: boolean;
45
- icon?: ReactNode;
46
- actions?: ReactNode;
47
- }
48
-
49
- export default function Banner({
50
- intent,
51
- density = "md",
52
- title,
53
- children,
54
- icon,
55
- withIcon = false,
56
- actions,
57
- className = "",
58
- ...rest
59
- }: BannerProps) {
60
- const resolvedIcon = icon !== undefined ? icon : withIcon ? INTENT_DEFAULT_ICON[intent] : null;
61
- return (
62
- <div
63
- {...rest}
64
- className={`flex items-start ${DENSITY_GAP[density]} border-2 border-black ${INTENT_SURFACE[intent]} ${DENSITY_PADDING[density]} ${className}`}
65
- >
66
- {resolvedIcon}
67
- <div className={`min-w-0 flex-1 ${DENSITY_TEXT[density]} text-black`}>
68
- {title && <div className="mb-0.5 font-bold">{title}</div>}
69
- {children}
70
- </div>
71
- {actions && <div className="shrink-0">{actions}</div>}
72
- </div>
73
- );
74
- }
@@ -1,76 +0,0 @@
1
- import { type HTMLAttributes, type ReactNode, useEffect } from "react";
2
- import { createPortal } from "react-dom";
3
-
4
- export interface BottomSheetProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
5
- children: ReactNode;
6
- onClose: () => void;
7
- /** CSS z-index. Default 50 to match modal-layer surfaces. */
8
- zIndex?: number;
9
- /** Click outside the sheet closes it. Default true. */
10
- dismissOnBackdrop?: boolean;
11
- /** Lock body scroll while mounted. Default true. */
12
- lockBodyScroll?: boolean;
13
- /** Width class for the sheet card. */
14
- widthClass?: string;
15
- /** Extra classes on the sheet card. */
16
- sheetClassName?: string;
17
- }
18
-
19
- /**
20
- * Mobile bottom sheet primitive.
21
- *
22
- * Unlike Lightbox, this has no dim backdrop. The sheet itself carries the
23
- * dialog affordance via brutal border + shadow while staying anchored to the
24
- * bottom safe area.
25
- */
26
- export default function BottomSheet({
27
- children,
28
- onClose,
29
- zIndex = 50,
30
- dismissOnBackdrop = true,
31
- lockBodyScroll = true,
32
- widthClass = "max-w-md",
33
- sheetClassName = "",
34
- className = "",
35
- ...rest
36
- }: BottomSheetProps) {
37
- useEffect(() => {
38
- const onKey = (e: KeyboardEvent) => {
39
- if (e.key === "Escape") onClose();
40
- };
41
- document.addEventListener("keydown", onKey);
42
- return () => document.removeEventListener("keydown", onKey);
43
- }, [onClose]);
44
-
45
- useEffect(() => {
46
- if (!lockBodyScroll) return;
47
- const prevBodyOverflow = document.body.style.overflow;
48
- document.body.style.overflow = "hidden";
49
- return () => {
50
- document.body.style.overflow = prevBodyOverflow;
51
- };
52
- }, [lockBodyScroll]);
53
-
54
- return createPortal(
55
- <div
56
- {...rest}
57
- className={`fixed inset-x-0 bottom-0 p-3 pb-[max(12px,env(safe-area-inset-bottom))] ${className}`}
58
- style={{ zIndex, ...rest.style }}
59
- onClick={
60
- dismissOnBackdrop
61
- ? (e) => {
62
- if (e.target === e.currentTarget) onClose();
63
- }
64
- : undefined
65
- }
66
- >
67
- <div
68
- className={`mx-auto border-2 border-black bg-white shadow-brutal-lg ${widthClass} ${sheetClassName}`}
69
- onClick={(event) => event.stopPropagation()}
70
- >
71
- {children}
72
- </div>
73
- </div>,
74
- document.body,
75
- );
76
- }
@@ -1,72 +0,0 @@
1
- import { Check } from "lucide-react";
2
- import type { HTMLAttributes } from "react";
3
-
4
- export type CheckMarkerShape = "square" | "circle";
5
- export type CheckMarkerSize = "sm" | "md" | "lg";
6
- export type CheckMarkerTone = "black-fill" | "yellow-fill";
7
-
8
- const SIZE_CLASS: Record<CheckMarkerSize, string> = {
9
- sm: "size-3.5",
10
- md: "size-4",
11
- lg: "size-5",
12
- };
13
-
14
- const ICON_PX: Record<CheckMarkerSize, number> = {
15
- sm: 10,
16
- md: 12,
17
- lg: 13,
18
- };
19
-
20
- const STROKE_WIDTH: Record<CheckMarkerSize, number> = {
21
- sm: 4,
22
- md: 4,
23
- lg: 3,
24
- };
25
-
26
- export interface CheckMarkerProps extends Omit<HTMLAttributes<HTMLSpanElement>, "size"> {
27
- checked: boolean;
28
- shape?: CheckMarkerShape;
29
- size?: CheckMarkerSize;
30
- tone?: CheckMarkerTone;
31
- disabled?: boolean;
32
- previewOnHover?: boolean;
33
- className?: string;
34
- }
35
-
36
- export default function CheckMarker({
37
- checked,
38
- shape = "square",
39
- size = "sm",
40
- tone = "black-fill",
41
- disabled = false,
42
- previewOnHover = false,
43
- className = "",
44
- ...props
45
- }: CheckMarkerProps) {
46
- const checkedClasses =
47
- tone === "yellow-fill" ? "bg-brutal-yellow text-black" : "bg-black text-white";
48
- const uncheckedClasses = previewOnHover
49
- ? "bg-white text-transparent group-hover:text-black/20"
50
- : "bg-white text-transparent";
51
-
52
- return (
53
- <span
54
- {...props}
55
- aria-hidden
56
- className={[
57
- "inline-flex shrink-0 items-center justify-center border-2 border-black transition-colors",
58
- SIZE_CLASS[size],
59
- shape === "circle" ? "rounded-full" : "",
60
- checked ? checkedClasses : uncheckedClasses,
61
- disabled ? "opacity-50" : "",
62
- className,
63
- ]
64
- .filter(Boolean)
65
- .join(" ")}
66
- >
67
- {(checked || previewOnHover) && (
68
- <Check size={ICON_PX[size]} strokeWidth={STROKE_WIDTH[size]} />
69
- )}
70
- </span>
71
- );
72
- }
@@ -1,81 +0,0 @@
1
- // Neo-Brutalist form checkbox primitive — sr-only native input + shared
2
- // CheckMarker visual, matching the form checkbox places already hand-rolled
3
- // before extraction (#proj-permission:1414ca65 msg=dabbb00e):
4
- //
5
- // • ReportIssueDialog — 4 options + consent
6
- // • OwnerOnboardingModal — opt-out
7
- // • MessageInput — "As Task" toggle
8
- // • AgentScopesPanel — permission rows (was native `accent-brutal-pink`)
9
- //
10
- // Why a custom span over native styling: the brutal design system uses
11
- // `border-2 border-black` everywhere; native `accent-color` ignores border
12
- // width and gives a soft fill. The `sr-only` input keeps a11y/keyboard/form
13
- // behavior intact while letting us paint the visual.
14
- //
15
- // API stays minimal — same shape as a native `<input type="checkbox">` plus a
16
- // `size` prop. Selection-only markers use CheckMarker directly so decorative
17
- // row selection does not inherit form semantics.
18
- //
19
- // The `<label>` wrapper that owns hover / cursor / disabled-opacity stays
20
- // the caller's job — different consumers compose this checkbox into
21
- // different row layouts (inline vs stacked, with description vs without),
22
- // and forcing one row layout would just push complexity into props.
23
-
24
- import type { InputHTMLAttributes } from "react";
25
- import CheckMarker, { type CheckMarkerSize } from "./CheckMarker";
26
-
27
- type Size = Extract<CheckMarkerSize, "sm" | "md">;
28
-
29
- export interface CheckboxProps extends Omit<
30
- InputHTMLAttributes<HTMLInputElement>,
31
- "type" | "size"
32
- > {
33
- /** Visual size of the checkbox box. Default `sm` (14px) matches the
34
- * existing inline callsites; use `md` (16px) for permission rows and
35
- * other settings-style lists. */
36
- size?: Size;
37
- }
38
-
39
- // Inline sr-only equivalent so the native input stays visually hidden
40
- // regardless of whether the consumer's Tailwind has the `sr-only`
41
- // utility class generated. ui-inventory hit this — its Tailwind only
42
- // scans its own src, so primitive-internal class strings weren't being
43
- // generated and the OS-default checkbox visibly bled through.
44
- const SR_ONLY_STYLE = {
45
- position: "absolute",
46
- width: 1,
47
- height: 1,
48
- padding: 0,
49
- margin: -1,
50
- overflow: "hidden",
51
- clip: "rect(0,0,0,0)",
52
- whiteSpace: "nowrap",
53
- borderWidth: 0,
54
- } as const;
55
-
56
- export default function Checkbox({
57
- size = "sm",
58
- checked,
59
- disabled,
60
- className = "",
61
- ...rest
62
- }: CheckboxProps) {
63
- return (
64
- <span className="relative inline-flex shrink-0">
65
- <input
66
- type="checkbox"
67
- checked={checked}
68
- disabled={disabled}
69
- style={SR_ONLY_STYLE}
70
- {...rest}
71
- />
72
- <CheckMarker
73
- checked={Boolean(checked)}
74
- disabled={disabled}
75
- size={size}
76
- tone="black-fill"
77
- className={className}
78
- />
79
- </span>
80
- );
81
- }
@@ -1,169 +0,0 @@
1
- import { forwardRef } from "react";
2
- import type { MouseEvent, ReactNode } from "react";
3
- import AvatarSlot from "./AvatarSlot";
4
-
5
- type ConversationAuthor =
6
- | {
7
- kind: "agent";
8
- name: string;
9
- avatarUrl?: string | null;
10
- subtitle?: string | null;
11
- }
12
- | {
13
- kind: "user";
14
- name: string;
15
- avatarUrl?: string | null;
16
- gravatarHash?: string | null;
17
- subtitle?: string | null;
18
- };
19
-
20
- export interface ConversationPreviewCardProps {
21
- channelLabel: string;
22
- author?: ConversationAuthor | null;
23
- timestamp?: string | null;
24
- preview: string;
25
- previewAuthor?: string | null;
26
- previewLeading?: ReactNode;
27
- previewLineClampClassName?: "line-clamp-2" | "line-clamp-3";
28
- previewClassName?: string;
29
- secondaryPreview?: ReactNode;
30
- secondaryPreviewClassName?: string;
31
- ariaLabel?: string;
32
- title?: string;
33
- testId?: string;
34
- marker?: ReactNode;
35
- footer?: ReactNode;
36
- action?: ReactNode;
37
- emphasized?: boolean;
38
- focused?: boolean;
39
- active?: boolean;
40
- onClick?: () => void;
41
- onContextMenu?: (event: MouseEvent) => void;
42
- }
43
-
44
- const ConversationPreviewCard = forwardRef<HTMLButtonElement, ConversationPreviewCardProps>(
45
- function ConversationPreviewCard(
46
- {
47
- channelLabel,
48
- author,
49
- timestamp,
50
- preview,
51
- previewAuthor,
52
- previewLeading,
53
- previewLineClampClassName = "line-clamp-3",
54
- previewClassName,
55
- secondaryPreview,
56
- secondaryPreviewClassName,
57
- ariaLabel,
58
- title,
59
- testId,
60
- marker,
61
- footer,
62
- action,
63
- emphasized = false,
64
- focused = false,
65
- active = false,
66
- onClick,
67
- onContextMenu,
68
- },
69
- ref,
70
- ) {
71
- const interactive = Boolean(onClick || onContextMenu);
72
- const className = `relative flex w-full items-start gap-3 border-2 p-3 text-left transition-colors hover:border-black hover:shadow-brutal-sm ${
73
- interactive ? "active:border-black active:shadow-brutal-sm" : "cursor-default"
74
- } ${
75
- focused
76
- ? "border-black bg-brutal-cyan/25 shadow-brutal"
77
- : active
78
- ? "border-black bg-white shadow-brutal-sm"
79
- : "border-black/30 bg-white"
80
- }`;
81
- const content = (
82
- <>
83
- <div className="min-w-0 flex-1">
84
- <div className="mb-1 flex flex-wrap items-center gap-x-2.5 gap-y-1 text-xs leading-4">
85
- <span className="font-bold text-black/50">{channelLabel}</span>
86
- {author && !previewAuthor ? (
87
- <span className="inline-flex min-w-0 items-center gap-1 font-bold text-black">
88
- {author.kind === "agent" ? (
89
- <AvatarSlot
90
- context="preview-mini"
91
- type="agent"
92
- agentAvatarUrl={author.avatarUrl ?? null}
93
- />
94
- ) : (
95
- <AvatarSlot
96
- context="preview-mini"
97
- type="human"
98
- humanAvatarUrl={author.avatarUrl ?? null}
99
- gravatarHash={author.gravatarHash ?? null}
100
- />
101
- )}
102
- <span className="truncate">{author.name}</span>
103
- {author.subtitle ? (
104
- <span className="font-mono text-[10px] text-black/40">{author.subtitle}</span>
105
- ) : null}
106
- </span>
107
- ) : null}
108
- {marker}
109
- {timestamp ? (
110
- <span className="font-mono text-xs leading-4 text-black/40">{timestamp}</span>
111
- ) : null}
112
- </div>
113
- <p
114
- className={`${previewLineClampClassName} text-sm ${emphasized ? "font-bold" : ""} ${previewClassName ?? ""}`}
115
- >
116
- {previewLeading ? (
117
- <span className="mr-1 inline-flex align-[-1px]">{previewLeading}</span>
118
- ) : null}
119
- {previewAuthor ? (
120
- <span className="font-bold text-black/70">{previewAuthor}: </span>
121
- ) : null}
122
- <span>{preview}</span>
123
- </p>
124
- {secondaryPreview ? (
125
- <p
126
- className={`mt-1 line-clamp-1 text-xs ${secondaryPreviewClassName ?? "text-black/45"}`}
127
- >
128
- {secondaryPreview}
129
- </p>
130
- ) : null}
131
- {footer ? <div className="mt-1 flex items-center gap-3">{footer}</div> : null}
132
- </div>
133
- {action ? <div className="shrink-0">{action}</div> : null}
134
- </>
135
- );
136
-
137
- if (!interactive) {
138
- return (
139
- <div
140
- aria-label={ariaLabel}
141
- title={title}
142
- data-testid={testId}
143
- data-focused={focused ? "true" : undefined}
144
- className={className}
145
- >
146
- {content}
147
- </div>
148
- );
149
- }
150
-
151
- return (
152
- <button
153
- ref={ref}
154
- type="button"
155
- onClick={onClick}
156
- onContextMenu={onContextMenu}
157
- aria-label={ariaLabel}
158
- title={title}
159
- data-testid={testId}
160
- data-focused={focused ? "true" : undefined}
161
- className={className}
162
- >
163
- {content}
164
- </button>
165
- );
166
- },
167
- );
168
-
169
- export default ConversationPreviewCard;
@@ -1,68 +0,0 @@
1
- import type { MouseEvent } from "react";
2
- import { Dialog } from "@base-ui/react/dialog";
3
- import { tv } from "../lib/tv";
4
-
5
- const dismissBackdrop = tv({
6
- slots: {
7
- popup: ["fixed inset-0"],
8
- },
9
- variants: {
10
- theme: {
11
- brutal: {},
12
- elegant: {},
13
- },
14
- },
15
- defaultVariants: {
16
- theme: "brutal",
17
- },
18
- });
19
-
20
- export type DismissBackdropProps = {
21
- onDismiss: () => void;
22
- zIndex?: number;
23
- trapContextMenu?: boolean;
24
- stopPropagation?: boolean;
25
- };
26
-
27
- export function DismissBackdrop({
28
- onDismiss,
29
- zIndex = 50,
30
- trapContextMenu = false,
31
- stopPropagation = false,
32
- }: DismissBackdropProps) {
33
- const { popup } = dismissBackdrop();
34
-
35
- return (
36
- <Dialog.Root
37
- open
38
- modal={false}
39
- onOpenChange={(open) => {
40
- if (!open) onDismiss();
41
- }}
42
- >
43
- <Dialog.Portal>
44
- <Dialog.Popup
45
- data-slot="dismiss-backdrop"
46
- className={popup()}
47
- style={{ zIndex }}
48
- onClick={
49
- stopPropagation
50
- ? (event: MouseEvent<HTMLDivElement>) => {
51
- event.stopPropagation();
52
- event.preventDefault();
53
- }
54
- : onDismiss
55
- }
56
- onContextMenu={
57
- trapContextMenu
58
- ? (event: MouseEvent<HTMLDivElement>) => {
59
- event.preventDefault();
60
- onDismiss();
61
- }
62
- : undefined
63
- }
64
- />
65
- </Dialog.Portal>
66
- </Dialog.Root>
67
- );
68
- }
@@ -1,63 +0,0 @@
1
- import type { HTMLAttributes, ReactNode } from "react";
2
-
3
- /**
4
- * Canonical empty-state primitive — used everywhere a surface needs to say
5
- * "nothing here yet" (ThreadPanel, SavedPanel, ChannelFilesPanel, TasksPanel,
6
- * NotificationCenter, AgentDetailPanel, AgentActivityLog, MobileComputersPanel,
7
- * ThreadsInbox, ChatPanel).
8
- *
9
- * Design contract locked in 2026-05-15 #proj-uiux:4e20fa91 (task #246) —
10
- * cindyz raised the screenshot, Joy + 跳虎 + Bugen aligned on B + C:
11
- *
12
- * 1. Title is sentence case. Callers pass the literal string they want
13
- * rendered. This primitive does not force case CSS. Per CLAUDE.md
14
- * "Text Styles", case-mangling is reserved for section labels (12px)
15
- * and dialog titles. Empty-state titles are informational hints,
16
- * neither category.
17
- *
18
- * 2. No icon frame. The previous wrapper rendered a soft-gray box that
19
- * read as OS placeholder default, visually jarring next to
20
- * brutal-bordered sibling surfaces. Removed. Icons render directly
21
- * inside a `text-black/40` wrapper, letting the empty state recede
22
- * naturally as a quiet info hint.
23
- *
24
- * 3. Icon size = 36 at the callsite. With the frame gone the icon loses
25
- * visual mass; size 36 (up from 28) keeps empty states from looking
26
- * deflated. Muted color carries the weight without needing a box.
27
- *
28
- * 4. Title color is muted, not pure black. The "No …" line should
29
- * sit back as empty-state body copy instead of reading like a primary
30
- * heading.
31
- *
32
- * Contract test: `packages/web/tests/emptyState.test.ts` reverse-greps the
33
- * source for the banned classes so the soft-frame look can't be
34
- * resurrected accidentally.
35
- */
36
- export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
37
- icon: ReactNode;
38
- title: ReactNode;
39
- description?: ReactNode;
40
- action?: ReactNode;
41
- }
42
-
43
- export default function EmptyState({
44
- icon,
45
- title,
46
- description,
47
- action,
48
- className = "",
49
- ...props
50
- }: EmptyStateProps) {
51
- return (
52
- <div {...props} className={["px-6 py-12 text-center", className].filter(Boolean).join(" ")}>
53
- <div className="mb-4 inline-flex items-center justify-center text-black/40">{icon}</div>
54
- <div className="mb-2 text-lg font-heading font-semibold text-black/60">{title}</div>
55
- {description ? (
56
- <div className="mx-auto max-w-[32ch] text-sm leading-relaxed text-black/60">
57
- {description}
58
- </div>
59
- ) : null}
60
- {action ? <div className="mt-5">{action}</div> : null}
61
- </div>
62
- );
63
- }