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.
- package/CHANGELOG.md +5 -0
- package/dist/button-CJqMhTPQ.mjs +621 -0
- package/dist/index-CpGciYPW.d.mts +267 -0
- package/dist/index.d.mts +11 -253
- package/dist/index.mjs +32 -587
- package/{src → dist}/styles.css +0 -2
- package/dist/wip.d.mts +133 -15
- package/dist/wip.mjs +289 -16
- package/package.json +7 -10
- package/dist/styled-props-B3SNekJg.d.mts +0 -16
- package/dist/use-theme-Y_h1DKll.mjs +0 -34
- package/src/components/avatar/avatar.tsx +0 -375
- package/src/components/avatar/index.ts +0 -17
- package/src/components/badge/badge.tsx +0 -393
- package/src/components/badge/index.ts +0 -2
- package/src/components/banner/banner.tsx +0 -269
- package/src/components/banner/index.ts +0 -8
- package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
- package/src/components/bottom-sheet/index.ts +0 -22
- package/src/components/button/button.tsx +0 -497
- package/src/components/button/index.ts +0 -2
- package/src/components/checkbox/checkbox-indicator.tsx +0 -128
- package/src/components/checkbox/checkbox.tsx +0 -350
- package/src/components/checkbox/index.ts +0 -2
- package/src/components/combobox/combobox.tsx +0 -603
- package/src/components/combobox/index.ts +0 -52
- package/src/components/context-menu/context-menu.tsx +0 -380
- package/src/components/context-menu/index.ts +0 -34
- package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
- package/src/components/conversation-preview-card/index.ts +0 -32
- package/src/components/description-list/description-list.tsx +0 -116
- package/src/components/description-list/index.ts +0 -14
- package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
- package/src/components/dropdown-menu/index.ts +0 -38
- package/src/components/empty-state/empty-state.tsx +0 -177
- package/src/components/empty-state/index.ts +0 -16
- package/src/components/field/field.tsx +0 -125
- package/src/components/field/index.ts +0 -18
- package/src/components/inline-code/index.ts +0 -2
- package/src/components/inline-code/inline-code.tsx +0 -35
- package/src/components/input/index.ts +0 -2
- package/src/components/input/input.tsx +0 -76
- package/src/components/input-group/index.ts +0 -7
- package/src/components/input-group/input-group.tsx +0 -215
- package/src/components/kbd/index.ts +0 -2
- package/src/components/kbd/kbd.tsx +0 -43
- package/src/components/label/index.ts +0 -7
- package/src/components/label/label.tsx +0 -99
- package/src/components/lightbox/index.ts +0 -24
- package/src/components/lightbox/lightbox.tsx +0 -245
- package/src/components/list-item/index.ts +0 -20
- package/src/components/list-item/list-item.tsx +0 -263
- package/src/components/media-list-item/index.ts +0 -20
- package/src/components/media-list-item/media-list-item.tsx +0 -169
- package/src/components/notification-center/index.ts +0 -39
- package/src/components/notification-center/notification-center.tsx +0 -558
- package/src/components/panel-header/index.ts +0 -22
- package/src/components/panel-header/panel-header.tsx +0 -199
- package/src/components/popover/index.ts +0 -30
- package/src/components/popover/popover.tsx +0 -248
- package/src/components/preview-shell/index.ts +0 -2
- package/src/components/preview-shell/preview-shell.tsx +0 -51
- package/src/components/quoted-message-card/index.ts +0 -36
- package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
- package/src/components/radio-group/index.ts +0 -6
- package/src/components/radio-group/radio-group.tsx +0 -119
- package/src/components/section-header/index.ts +0 -16
- package/src/components/section-header/section-header.tsx +0 -117
- package/src/components/section-label/index.ts +0 -2
- package/src/components/section-label/section-label.tsx +0 -38
- package/src/components/segmented-control/index.ts +0 -12
- package/src/components/segmented-control/segmented-control.tsx +0 -129
- package/src/components/select/index.ts +0 -40
- package/src/components/select/select.tsx +0 -382
- package/src/components/spinner/index.ts +0 -2
- package/src/components/spinner/spinner.tsx +0 -228
- package/src/components/status/index.ts +0 -2
- package/src/components/status/status.tsx +0 -152
- package/src/components/tabs/index.ts +0 -24
- package/src/components/tabs/tabs.tsx +0 -451
- package/src/components/text/index.ts +0 -2
- package/src/components/text/text.tsx +0 -131
- package/src/components/textarea/index.ts +0 -2
- package/src/components/textarea/textarea.tsx +0 -136
- package/src/components/toggle-group/index.ts +0 -12
- package/src/components/toggle-group/toggle-group.tsx +0 -111
- package/src/components/tooltip/index.ts +0 -14
- package/src/components/tooltip/tooltip.tsx +0 -212
- package/src/index.ts +0 -426
- package/src/legacy/AttentionDot.tsx +0 -28
- package/src/legacy/AvatarListRow.tsx +0 -87
- package/src/legacy/AvatarSlot.tsx +0 -157
- package/src/legacy/Badge.tsx +0 -95
- package/src/legacy/Banner.tsx +0 -74
- package/src/legacy/BottomSheet.tsx +0 -76
- package/src/legacy/CheckMarker.tsx +0 -72
- package/src/legacy/Checkbox.tsx +0 -81
- package/src/legacy/ConversationPreviewCard.tsx +0 -169
- package/src/legacy/DismissBackdrop.tsx +0 -68
- package/src/legacy/EmptyState.tsx +0 -63
- package/src/legacy/ExperimentalBadge.tsx +0 -19
- package/src/legacy/FormField.tsx +0 -106
- package/src/legacy/KeyValueRow.tsx +0 -67
- package/src/legacy/Lightbox.tsx +0 -119
- package/src/legacy/NotificationCenter.tsx +0 -217
- package/src/legacy/PanelHeader.tsx +0 -94
- package/src/legacy/PreviewShell.tsx +0 -40
- package/src/legacy/QuotedMessageCard.tsx +0 -181
- package/src/legacy/ReorderablePanelTabs.tsx +0 -156
- package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
- package/src/legacy/SearchInput.tsx +0 -23
- package/src/legacy/SectionEyebrow.tsx +0 -60
- package/src/legacy/SectionHeader.tsx +0 -68
- package/src/legacy/SegmentedControl.tsx +0 -74
- package/src/legacy/SlugInput.tsx +0 -29
- package/src/legacy/Spinner.tsx +0 -89
- package/src/legacy/StatusDot.tsx +0 -51
- package/src/legacy/SurfaceListItem.tsx +0 -32
- package/src/legacy/Textarea.tsx +0 -137
- package/src/legacy/button.tsx +0 -100
- package/src/legacy/context-menu-divider.tsx +0 -12
- package/src/legacy/menu-item.tsx +0 -66
- package/src/legacy/selection-popover.tsx +0 -212
- package/src/legacy/server-switcher-menu.tsx +0 -203
- package/src/legacy/source.css +0 -240
- package/src/lib/cn.ts +0 -1
- package/src/lib/styled-props.ts +0 -11
- package/src/lib/theme/theme-context.ts +0 -28
- package/src/lib/theme/theme-provider.tsx +0 -181
- package/src/lib/theme/use-theme.ts +0 -12
- package/src/lib/tv.ts +0 -29
- package/src/lib/without-children.ts +0 -6
- package/src/wip.ts +0 -233
package/src/legacy/Spinner.tsx
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { ComponentProps } from "react";
|
|
2
|
-
import { cn } from "../lib/cn.ts";
|
|
3
|
-
|
|
4
|
-
type SpinnerSize = "sm" | "lg";
|
|
5
|
-
|
|
6
|
-
type SpinnerProps = ComponentProps<"span"> & {
|
|
7
|
-
size?: SpinnerSize;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const SPINNER_STYLE = `
|
|
11
|
-
@keyframes sitefire-braille-spinner {
|
|
12
|
-
0%,
|
|
13
|
-
9.99% {
|
|
14
|
-
content: "⠋";
|
|
15
|
-
}
|
|
16
|
-
10%,
|
|
17
|
-
19.99% {
|
|
18
|
-
content: "⠙";
|
|
19
|
-
}
|
|
20
|
-
20%,
|
|
21
|
-
29.99% {
|
|
22
|
-
content: "⠹";
|
|
23
|
-
}
|
|
24
|
-
30%,
|
|
25
|
-
39.99% {
|
|
26
|
-
content: "⠸";
|
|
27
|
-
}
|
|
28
|
-
40%,
|
|
29
|
-
49.99% {
|
|
30
|
-
content: "⠼";
|
|
31
|
-
}
|
|
32
|
-
50%,
|
|
33
|
-
59.99% {
|
|
34
|
-
content: "⠴";
|
|
35
|
-
}
|
|
36
|
-
60%,
|
|
37
|
-
69.99% {
|
|
38
|
-
content: "⠦";
|
|
39
|
-
}
|
|
40
|
-
70%,
|
|
41
|
-
79.99% {
|
|
42
|
-
content: "⠧";
|
|
43
|
-
}
|
|
44
|
-
80%,
|
|
45
|
-
89.99% {
|
|
46
|
-
content: "⠇";
|
|
47
|
-
}
|
|
48
|
-
90%,
|
|
49
|
-
100% {
|
|
50
|
-
content: "⠏";
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
[data-slot="spinner"] [data-spinner-glyph]::before {
|
|
55
|
-
content: "⠋";
|
|
56
|
-
animation: sitefire-braille-spinner 800ms steps(1, end) infinite;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
@media (prefers-reduced-motion: reduce) {
|
|
60
|
-
[data-slot="spinner"] [data-spinner-glyph]::before {
|
|
61
|
-
animation-duration: 1600ms;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
export default function Spinner({
|
|
67
|
-
className,
|
|
68
|
-
size = "sm",
|
|
69
|
-
"aria-label": ariaLabel = "Loading",
|
|
70
|
-
...props
|
|
71
|
-
}: SpinnerProps) {
|
|
72
|
-
return (
|
|
73
|
-
<span
|
|
74
|
-
data-slot="spinner"
|
|
75
|
-
data-size={size}
|
|
76
|
-
role="status"
|
|
77
|
-
aria-label={ariaLabel}
|
|
78
|
-
className={cn(
|
|
79
|
-
"relative inline-flex shrink-0 items-center justify-center font-mono leading-none text-current tabular-nums",
|
|
80
|
-
size === "sm" ? "h-4 text-base" : "h-6 text-xl",
|
|
81
|
-
className,
|
|
82
|
-
)}
|
|
83
|
-
{...props}
|
|
84
|
-
>
|
|
85
|
-
<style>{SPINNER_STYLE}</style>
|
|
86
|
-
<span data-spinner-glyph="" aria-hidden="true" className="inline-block min-w-[1ch]" />
|
|
87
|
-
</span>
|
|
88
|
-
);
|
|
89
|
-
}
|
package/src/legacy/StatusDot.tsx
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes } from "react";
|
|
2
|
-
|
|
3
|
-
type AgentActivity = "online" | "thinking" | "working" | "error" | "offline";
|
|
4
|
-
|
|
5
|
-
type SpanPassthroughProps = HTMLAttributes<HTMLSpanElement> &
|
|
6
|
-
Record<`data-${string}`, string | undefined>;
|
|
7
|
-
|
|
8
|
-
export interface StatusDotProps extends Omit<SpanPassthroughProps, "children"> {
|
|
9
|
-
activity?: AgentActivity;
|
|
10
|
-
tone?: string;
|
|
11
|
-
size?: "sm" | "md" | "lg";
|
|
12
|
-
pulse?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const SIZE_CLASS: Record<NonNullable<StatusDotProps["size"]>, string> = {
|
|
16
|
-
sm: "size-2",
|
|
17
|
-
md: "size-2.5",
|
|
18
|
-
lg: "size-[11px]",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function getActivityDotClass(activity: AgentActivity): string {
|
|
22
|
-
switch (activity) {
|
|
23
|
-
case "online":
|
|
24
|
-
return "bg-brutal-lime";
|
|
25
|
-
case "thinking":
|
|
26
|
-
case "working":
|
|
27
|
-
return "bg-brutal-yellow animate-pulse";
|
|
28
|
-
case "error":
|
|
29
|
-
return "bg-brutal-orange";
|
|
30
|
-
case "offline":
|
|
31
|
-
default:
|
|
32
|
-
return "bg-gray-400";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default function StatusDot({
|
|
37
|
-
activity,
|
|
38
|
-
tone,
|
|
39
|
-
size = "md",
|
|
40
|
-
pulse = false,
|
|
41
|
-
className,
|
|
42
|
-
...rest
|
|
43
|
-
}: StatusDotProps) {
|
|
44
|
-
const colorClass = activity ? getActivityDotClass(activity) : (tone ?? "bg-gray-400");
|
|
45
|
-
return (
|
|
46
|
-
<span
|
|
47
|
-
{...rest}
|
|
48
|
-
className={`inline-block shrink-0 rounded-full border border-black ${SIZE_CLASS[size]} ${colorClass} ${pulse ? "animate-pulse" : ""} ${className ?? ""}`}
|
|
49
|
-
/>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
import { cn } from "../lib/cn.ts";
|
|
3
|
-
|
|
4
|
-
export default function SurfaceListItem({
|
|
5
|
-
children,
|
|
6
|
-
selected = false,
|
|
7
|
-
interactive = true,
|
|
8
|
-
className = "",
|
|
9
|
-
...props
|
|
10
|
-
}: HTMLAttributes<HTMLDivElement> & {
|
|
11
|
-
children: ReactNode;
|
|
12
|
-
selected?: boolean;
|
|
13
|
-
interactive?: boolean;
|
|
14
|
-
}) {
|
|
15
|
-
return (
|
|
16
|
-
<div
|
|
17
|
-
className={cn(
|
|
18
|
-
"border-2 px-4 py-3 transition-colors",
|
|
19
|
-
selected
|
|
20
|
-
? "border-black bg-brutal-cyan/15 shadow-brutal-sm"
|
|
21
|
-
: [
|
|
22
|
-
"border-black/30 bg-white",
|
|
23
|
-
interactive ? "hover:border-black hover:shadow-brutal-sm" : "",
|
|
24
|
-
].join(" "),
|
|
25
|
-
className,
|
|
26
|
-
)}
|
|
27
|
-
{...props}
|
|
28
|
-
>
|
|
29
|
-
{children}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
package/src/legacy/Textarea.tsx
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type ReactNode, type TextareaHTMLAttributes } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Canonical brutal-style textarea primitive with error + counter support.
|
|
5
|
-
*
|
|
6
|
-
* Replaces bare `<textarea className="input-brutal" />` + ad-hoc error
|
|
7
|
-
* state + character-counter spans that drift across forms (SettingsPanel,
|
|
8
|
-
* CreateChannelDialog, ReportIssueDialog, PreJoinAgreementSection, etc).
|
|
9
|
-
*
|
|
10
|
-
* cindyz 2026-05-12 #engineering:89667b54 msg=dcb9d29b asked for this
|
|
11
|
-
* after cross's pre-join agreement PR introduced a feature-local
|
|
12
|
-
* red-border textarea error state that didn't exist anywhere else in the
|
|
13
|
-
* app. This primitive locks the error visual so future callsites don't
|
|
14
|
-
* re-invent it.
|
|
15
|
-
*
|
|
16
|
-
* **Visual contract:**
|
|
17
|
-
* - Default: `input-brutal` (border-2 black, white bg, shadow-brutal-sm, focus shadow-brutal)
|
|
18
|
-
* - Error: red border + red ring + coral-tinted bg (bg-brutal-red/5)
|
|
19
|
-
* - Counter: bottom-right, red when over limit
|
|
20
|
-
* - Message: below the field, red when error
|
|
21
|
-
*
|
|
22
|
-
* **Why not just a CSS variant on `.input-brutal`?**
|
|
23
|
-
* Textarea callsites almost always need counter + error message together;
|
|
24
|
-
* coordinating those through className alone means every callsite
|
|
25
|
-
* re-writes the counter/error layout. Wrapping in a component is cheaper.
|
|
26
|
-
*
|
|
27
|
-
* Inputs (not textarea) follow the same error visual but are usually
|
|
28
|
-
* small enough that a dedicated `BrutalInput` wrapper is overkill — use
|
|
29
|
-
* `className={error ? "input-brutal !border-brutal-red ring-2 ring-brutal-red/60" : "input-brutal"}`
|
|
30
|
-
* inline, or extend this primitive if a second form-field callsite
|
|
31
|
-
* emerges.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* <Textarea
|
|
36
|
-
* value={body}
|
|
37
|
-
* onChange={(e) => setBody(e.target.value)}
|
|
38
|
-
* maxLength={500}
|
|
39
|
-
* error={body.length > 500 ? "Agreement body must be 500 characters or fewer" : undefined}
|
|
40
|
-
* rows={6}
|
|
41
|
-
* placeholder="Type your server's join agreement…"
|
|
42
|
-
* />
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
export interface TextareaProps extends Omit<
|
|
47
|
-
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
48
|
-
"children"
|
|
49
|
-
> {
|
|
50
|
-
/** Error message — renders red border + ring + message below. When
|
|
51
|
-
* empty string or undefined, field is in normal state. */
|
|
52
|
-
error?: string | null;
|
|
53
|
-
/** Show a character counter at the bottom-right. Uses `maxLength` if
|
|
54
|
-
* not explicitly set. Counter turns red once current length exceeds
|
|
55
|
-
* maxLength (or when `error` is set). */
|
|
56
|
-
showCounter?: boolean;
|
|
57
|
-
/** Optional hint / helper text shown below field when not in error
|
|
58
|
-
* state. */
|
|
59
|
-
hint?: ReactNode;
|
|
60
|
-
/** Additional classes — appended to the `<textarea>` element. Use for
|
|
61
|
-
* layout tweaks (e.g. `!min-h-32`). */
|
|
62
|
-
className?: string;
|
|
63
|
-
/** Additional classes on the outer wrapper `<div>`. Use for margin /
|
|
64
|
-
* width tweaks. */
|
|
65
|
-
wrapperClassName?: string;
|
|
66
|
-
/** Custom length function — defaults to `value.length` (UTF-16 code
|
|
67
|
-
* units). Pass `(v) => Array.from(v).length` for Unicode code-point
|
|
68
|
-
* counting (correct for emoji / surrogate pairs). */
|
|
69
|
-
getLength?: (value: string) => number;
|
|
70
|
-
/** Override the displayed count directly (e.g. when the parent already
|
|
71
|
-
* computes a code-point count). Takes precedence over `getLength`. */
|
|
72
|
-
count?: number;
|
|
73
|
-
/** Counter display limit — falls back to native `maxLength` when not
|
|
74
|
-
* set. Use when you want soft validation (counter turns red over
|
|
75
|
-
* limit, but no native browser truncation). Common with
|
|
76
|
-
* `getLength`/`count` for Unicode-correct enforcement. */
|
|
77
|
-
limit?: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
|
|
81
|
-
{
|
|
82
|
-
error,
|
|
83
|
-
showCounter,
|
|
84
|
-
hint,
|
|
85
|
-
className = "",
|
|
86
|
-
wrapperClassName = "",
|
|
87
|
-
maxLength,
|
|
88
|
-
value,
|
|
89
|
-
defaultValue,
|
|
90
|
-
getLength,
|
|
91
|
-
count,
|
|
92
|
-
limit,
|
|
93
|
-
...rest
|
|
94
|
-
},
|
|
95
|
-
ref,
|
|
96
|
-
) {
|
|
97
|
-
const hasError = !!error;
|
|
98
|
-
const rawValue =
|
|
99
|
-
typeof value === "string" ? value : typeof defaultValue === "string" ? defaultValue : "";
|
|
100
|
-
const length = count !== undefined ? count : getLength ? getLength(rawValue) : rawValue.length;
|
|
101
|
-
const counterLimit = limit ?? maxLength;
|
|
102
|
-
const overLimit = counterLimit !== undefined && length > counterLimit;
|
|
103
|
-
|
|
104
|
-
const stateClass = hasError ? "!border-brutal-red ring-2 ring-brutal-red/60 bg-brutal-red/5" : "";
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className={`flex flex-col gap-1 ${wrapperClassName}`.trim()}>
|
|
108
|
-
<textarea
|
|
109
|
-
ref={ref}
|
|
110
|
-
value={value}
|
|
111
|
-
defaultValue={defaultValue}
|
|
112
|
-
maxLength={maxLength}
|
|
113
|
-
aria-invalid={hasError || undefined}
|
|
114
|
-
className={`input-brutal w-full ${stateClass} ${className}`.trim()}
|
|
115
|
-
{...rest}
|
|
116
|
-
/>
|
|
117
|
-
<div className="flex items-start justify-between gap-3 text-xs">
|
|
118
|
-
<div
|
|
119
|
-
className={`min-w-0 flex-1 ${hasError ? "font-bold text-brutal-red" : "text-black/60"}`}
|
|
120
|
-
>
|
|
121
|
-
{hasError ? error : hint}
|
|
122
|
-
</div>
|
|
123
|
-
{showCounter && counterLimit !== undefined && (
|
|
124
|
-
<div
|
|
125
|
-
className={`shrink-0 font-mono tabular-nums ${
|
|
126
|
-
overLimit || hasError ? "font-bold text-brutal-red" : "text-black/50"
|
|
127
|
-
}`}
|
|
128
|
-
>
|
|
129
|
-
{length}/{counterLimit}
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
export default Textarea;
|
package/src/legacy/button.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
4
|
-
type ButtonShape = "icon" | "text" | "iconText";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Button tones map to the brutal palette. Callers say `tone="pink"`,
|
|
8
|
-
* never `className="bg-brutal-pink"`. Adding a new tone means adding a
|
|
9
|
-
* row here, not a one-off override at the callsite.
|
|
10
|
-
*
|
|
11
|
-
* Hidden context (PR #1905 / #proj-theme:ac79cf20 stdrc msg=6ce4d8bf):
|
|
12
|
-
* the prior API exposed size+shape but left bg/color to caller
|
|
13
|
-
* className. Tightening the boundary makes theme swaps possible without
|
|
14
|
-
* re-auditing every callsite.
|
|
15
|
-
*/
|
|
16
|
-
export const BUTTON_TONE_CLASSES = {
|
|
17
|
-
white: "bg-white text-black",
|
|
18
|
-
yellow: "bg-brutal-yellow text-black",
|
|
19
|
-
pink: "bg-brutal-pink text-black",
|
|
20
|
-
cyan: "bg-brutal-cyan text-black",
|
|
21
|
-
lavender: "bg-brutal-lavender text-black",
|
|
22
|
-
orange: "bg-brutal-orange text-black",
|
|
23
|
-
lime: "bg-brutal-lime text-black",
|
|
24
|
-
red: "bg-brutal-red text-black",
|
|
25
|
-
stone: "bg-brutal-stone text-black",
|
|
26
|
-
} as const;
|
|
27
|
-
|
|
28
|
-
export type ButtonTone = keyof typeof BUTTON_TONE_CLASSES;
|
|
29
|
-
|
|
30
|
-
const SIZE_SHAPE_CLASSES: Record<ButtonSize, Record<ButtonShape, string>> = {
|
|
31
|
-
xs: {
|
|
32
|
-
icon: "size-6 text-[11px]",
|
|
33
|
-
text: "h-6 px-2 text-[11px]",
|
|
34
|
-
iconText: "h-6 gap-1 px-2 text-[11px]",
|
|
35
|
-
},
|
|
36
|
-
sm: {
|
|
37
|
-
icon: "size-7 text-xs",
|
|
38
|
-
text: "h-7 px-2.5 text-xs",
|
|
39
|
-
iconText: "h-7 gap-1.5 px-2.5 text-xs",
|
|
40
|
-
},
|
|
41
|
-
md: {
|
|
42
|
-
icon: "size-8 text-sm",
|
|
43
|
-
text: "h-8 px-3 text-sm",
|
|
44
|
-
iconText: "h-8 gap-2 px-3 text-sm",
|
|
45
|
-
},
|
|
46
|
-
lg: {
|
|
47
|
-
icon: "size-10 text-sm",
|
|
48
|
-
text: "h-10 px-4 text-sm",
|
|
49
|
-
iconText: "h-10 gap-2 px-4 text-sm",
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export function brutalButtonClassName({
|
|
54
|
-
size = "sm",
|
|
55
|
-
shape = "text",
|
|
56
|
-
tone = "white",
|
|
57
|
-
className = "",
|
|
58
|
-
}: {
|
|
59
|
-
size?: ButtonSize;
|
|
60
|
-
shape?: ButtonShape;
|
|
61
|
-
tone?: ButtonTone;
|
|
62
|
-
className?: string;
|
|
63
|
-
} = {}) {
|
|
64
|
-
return [
|
|
65
|
-
"btn-brutal-sm inline-flex shrink-0 items-center justify-center font-bold leading-none",
|
|
66
|
-
BUTTON_TONE_CLASSES[tone],
|
|
67
|
-
SIZE_SHAPE_CLASSES[size][shape],
|
|
68
|
-
className,
|
|
69
|
-
]
|
|
70
|
-
.filter(Boolean)
|
|
71
|
-
.join(" ");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
75
|
-
size?: ButtonSize;
|
|
76
|
-
shape?: ButtonShape;
|
|
77
|
-
/** Background tone from the brutal palette. Defaults to `white`. */
|
|
78
|
-
tone?: ButtonTone;
|
|
79
|
-
children?: ReactNode;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export default function Button({
|
|
83
|
-
size = "sm",
|
|
84
|
-
shape = "text",
|
|
85
|
-
tone,
|
|
86
|
-
className = "",
|
|
87
|
-
type = "button",
|
|
88
|
-
children,
|
|
89
|
-
...props
|
|
90
|
-
}: ButtonProps) {
|
|
91
|
-
return (
|
|
92
|
-
<button
|
|
93
|
-
type={type}
|
|
94
|
-
className={brutalButtonClassName({ size, shape, tone, className })}
|
|
95
|
-
{...props}
|
|
96
|
-
>
|
|
97
|
-
{children}
|
|
98
|
-
</button>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical divider between groups of context-menu items. Per CLAUDE.md
|
|
3
|
-
* "Menus & Dropdowns": `border-t-2 border-black` with no margin — spacing
|
|
4
|
-
* between items is controlled by item padding, not divider margins.
|
|
5
|
-
*
|
|
6
|
-
* Centralized so context menus (Sidebar channel / DM / agent menus today,
|
|
7
|
-
* future surfaces) don't re-roll the same `<div className="border-t-2
|
|
8
|
-
* border-black" />` literal. stdrc 2026-05-24 audit follow-up.
|
|
9
|
-
*/
|
|
10
|
-
export default function ContextMenuDivider() {
|
|
11
|
-
return <div className="border-t-2 border-black" />;
|
|
12
|
-
}
|
package/src/legacy/menu-item.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Pure-action menu row primitive.
|
|
5
|
-
*
|
|
6
|
-
* Shared by the family of "click → run an action" menus across the app:
|
|
7
|
-
* - Right-click context menus (Sidebar channel/DM/agent, MessageItem,
|
|
8
|
-
* SavedPanel, ThreadsInbox)
|
|
9
|
-
* - Sidebar add-popover menus (Add channel → Join existing / Create new …)
|
|
10
|
-
* - Mark-as-read / pin / archive / delete rows
|
|
11
|
-
*
|
|
12
|
-
* Origin: stdrc 2026-05-25 #proj-theme:ac79cf20 msg=dae4c262 +
|
|
13
|
-
* msg=245a96f0 + msg=8fdf9cba. Inventory pass found ~30 hand-rolled
|
|
14
|
-
* `flex w-full … hover:bg-brutal-yellow transition-colors` rows that
|
|
15
|
-
* had drifted on a few axes (`uppercase tracking-wide` on Sidebar
|
|
16
|
-
* sort/add popovers, density modifiers in some places but not others).
|
|
17
|
-
*
|
|
18
|
-
* Distinct from `SelectionPopover` — that primitive is for single-select
|
|
19
|
-
* dropdowns where the trailing ✓ marks the chosen option. This row is
|
|
20
|
-
* pure action: no ✓, no selected-state fill. When state-changing items
|
|
21
|
-
* like "Mark as Read ↔ Mark as Unread" or "Pin ↔ Unpin" toggle their
|
|
22
|
-
* label based on current state, that's just label state, not a toggle
|
|
23
|
-
* UI affordance (stdrc msg=245a96f0).
|
|
24
|
-
*
|
|
25
|
-
* Hover token: `bg-brutal-yellow/30` matches SegmentedControl unselected
|
|
26
|
-
* hover + SelectionPopover row hover, so all selection-family and
|
|
27
|
-
* action-menu surfaces share a single hover color. Right-click menus
|
|
28
|
-
* intentionally do NOT differentiate destructive vs non-destructive at
|
|
29
|
-
* the row level — every irreversible action is gated by a
|
|
30
|
-
* `ConfirmDialog`, so the menu itself stays visually uniform (stdrc
|
|
31
|
-
* msg=60e789d1: "Agent 的 Stop 和 Restart 菜单也没有特别的 hover 样式
|
|
32
|
-
* … 因为它反正会弹出一个窗口提示,所以不需要在菜单上做特别呈现").
|
|
33
|
-
*/
|
|
34
|
-
export type MenuItemProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
35
|
-
/** Optional leading icon. Conventionally a lucide icon at `size={14}`. */
|
|
36
|
-
icon?: ReactNode;
|
|
37
|
-
/** Optional right-aligned slot (e.g. shortcut hint). */
|
|
38
|
-
trailing?: ReactNode;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export default function MenuItem({
|
|
42
|
-
icon,
|
|
43
|
-
trailing,
|
|
44
|
-
className = "",
|
|
45
|
-
type = "button",
|
|
46
|
-
role = "menuitem",
|
|
47
|
-
children,
|
|
48
|
-
...rest
|
|
49
|
-
}: MenuItemProps) {
|
|
50
|
-
// Compact-viewport density (`[@media(max-height:600px)]:py-1`) is
|
|
51
|
-
// applied unconditionally — harmless on tall viewports and keeps
|
|
52
|
-
// Sidebar / MessageItem / SavedPanel context menus visually uniform
|
|
53
|
-
// in small viewports without per-callsite opt-in.
|
|
54
|
-
const base =
|
|
55
|
-
"flex w-full items-center gap-2 px-3 py-2 [@media(max-height:600px)]:py-1 text-sm text-left transition-colors";
|
|
56
|
-
const tone =
|
|
57
|
-
"font-medium text-black hover:bg-brutal-yellow/30 disabled:cursor-not-allowed disabled:text-black/30";
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<button type={type} role={role} className={`${base} ${tone} ${className}`} {...rest}>
|
|
61
|
-
{icon}
|
|
62
|
-
<span className="min-w-0 flex-1 truncate">{children}</span>
|
|
63
|
-
{trailing}
|
|
64
|
-
</button>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import type { KeyboardEvent, ReactNode } from "react";
|
|
2
|
-
import { Button as BaseButton } from "@base-ui/react/button";
|
|
3
|
-
import { Check } from "lucide-react";
|
|
4
|
-
import { tv } from "../lib/tv.ts";
|
|
5
|
-
import { useThemeFamily } from "../lib/theme/use-theme.ts";
|
|
6
|
-
|
|
7
|
-
// Single-select popover content: a header, an optional search box, and a list of
|
|
8
|
-
// checkable rows (the trailing ✓ is the selected indicator; hover is the shared
|
|
9
|
-
// selection-family yellow/30). Presentational — the original had no portal/effect,
|
|
10
|
-
// so place it inside a base-ui Popover.Positioner at the callsite. `shadow-brutal`
|
|
11
|
-
// maps to --shadow-hover; the search box reuses the brutal field skin (--shadow).
|
|
12
|
-
const selectionPopover = tv({
|
|
13
|
-
slots: {
|
|
14
|
-
root: ["min-w-[220px]"],
|
|
15
|
-
header: ["flex items-center justify-between border-b border-black px-3 py-2"],
|
|
16
|
-
title: ["text-[10px] font-bold uppercase tracking-wide text-black/60"],
|
|
17
|
-
clear: ["text-[10px] font-bold uppercase tracking-wide text-black/50 hover:text-black"],
|
|
18
|
-
searchWrap: ["border-b border-black px-2 py-2"],
|
|
19
|
-
searchInput: [
|
|
20
|
-
"w-full border-2 border-black bg-white px-2 py-1",
|
|
21
|
-
"font-mono text-xs shadow-[var(--shadow)]",
|
|
22
|
-
"focus:shadow-[var(--shadow-hover)] focus:outline-none",
|
|
23
|
-
],
|
|
24
|
-
list: ["max-h-64 overflow-y-auto"],
|
|
25
|
-
empty: ["px-3 py-3 text-center font-mono text-[11px] text-black/40"],
|
|
26
|
-
row: [
|
|
27
|
-
"flex h-9 w-full items-center justify-between gap-2 overflow-hidden px-3",
|
|
28
|
-
"border-b text-left text-xs font-bold last:border-b-0",
|
|
29
|
-
],
|
|
30
|
-
rowMain: ["flex min-w-0 items-center gap-2"],
|
|
31
|
-
leading: ["flex size-5 shrink-0 items-center justify-center"],
|
|
32
|
-
label: ["truncate leading-6"],
|
|
33
|
-
check: ["shrink-0"],
|
|
34
|
-
},
|
|
35
|
-
variants: {
|
|
36
|
-
theme: {
|
|
37
|
-
brutal: {
|
|
38
|
-
root: ["border-2 border-black bg-white shadow-[var(--shadow-hover)]"],
|
|
39
|
-
row: ["border-black/10"],
|
|
40
|
-
},
|
|
41
|
-
// Elegant styling is deferred; see the component TODO.
|
|
42
|
-
elegant: {},
|
|
43
|
-
},
|
|
44
|
-
disabled: {
|
|
45
|
-
true: {
|
|
46
|
-
row: "cursor-not-allowed bg-white text-black/30",
|
|
47
|
-
},
|
|
48
|
-
false: {
|
|
49
|
-
row: "bg-white text-black hover:bg-brutal-yellow/30",
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
italic: {
|
|
53
|
-
true: {
|
|
54
|
-
label: "-mx-1 whitespace-nowrap px-1 italic text-black/70",
|
|
55
|
-
},
|
|
56
|
-
false: {
|
|
57
|
-
label: "",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
defaultVariants: {
|
|
62
|
-
disabled: false,
|
|
63
|
-
italic: false,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
export type SelectionPopoverOption = {
|
|
68
|
-
key: string;
|
|
69
|
-
checked: boolean;
|
|
70
|
-
label: string;
|
|
71
|
-
onClick: () => void;
|
|
72
|
-
disabled?: boolean;
|
|
73
|
-
italic?: boolean;
|
|
74
|
-
avatar?: ReactNode;
|
|
75
|
-
reserveLeadingSlot?: boolean;
|
|
76
|
-
leading?: ReactNode;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
type SelectionPopoverBaseProps = {
|
|
80
|
-
title: string;
|
|
81
|
-
options: SelectionPopoverOption[];
|
|
82
|
-
showClear?: boolean;
|
|
83
|
-
onClear?: () => void;
|
|
84
|
-
showHeader?: boolean;
|
|
85
|
-
emptyLabel?: string;
|
|
86
|
-
className?: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
type SearchableProps = SelectionPopoverBaseProps & {
|
|
90
|
-
searchable: true;
|
|
91
|
-
search: string;
|
|
92
|
-
onSearchChange: (value: string) => void;
|
|
93
|
-
searchPlaceholder?: string;
|
|
94
|
-
onInputKeyDown?: (
|
|
95
|
-
event: KeyboardEvent<HTMLInputElement>,
|
|
96
|
-
options: SelectionPopoverOption[],
|
|
97
|
-
) => void;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
type PlainProps = SelectionPopoverBaseProps & {
|
|
101
|
-
searchable?: false;
|
|
102
|
-
search?: never;
|
|
103
|
-
onSearchChange?: never;
|
|
104
|
-
searchPlaceholder?: never;
|
|
105
|
-
onInputKeyDown?: never;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export type SelectionPopoverProps = SearchableProps | PlainProps;
|
|
109
|
-
|
|
110
|
-
function SelectionPopoverOptionRow({
|
|
111
|
-
checked,
|
|
112
|
-
onClick,
|
|
113
|
-
label,
|
|
114
|
-
disabled = false,
|
|
115
|
-
italic = false,
|
|
116
|
-
avatar = null,
|
|
117
|
-
reserveLeadingSlot = false,
|
|
118
|
-
leading = null,
|
|
119
|
-
}: Omit<SelectionPopoverOption, "key">) {
|
|
120
|
-
const theme = useThemeFamily();
|
|
121
|
-
const leadingContent = leading ?? avatar;
|
|
122
|
-
const {
|
|
123
|
-
row,
|
|
124
|
-
rowMain,
|
|
125
|
-
leading: leadingSlot,
|
|
126
|
-
label: labelSlot,
|
|
127
|
-
check,
|
|
128
|
-
} = selectionPopover({
|
|
129
|
-
theme,
|
|
130
|
-
disabled,
|
|
131
|
-
italic,
|
|
132
|
-
});
|
|
133
|
-
return (
|
|
134
|
-
<BaseButton
|
|
135
|
-
type="button"
|
|
136
|
-
disabled={disabled}
|
|
137
|
-
onClick={onClick}
|
|
138
|
-
data-slot="selection-popover-row"
|
|
139
|
-
className={row()}
|
|
140
|
-
>
|
|
141
|
-
<span className={rowMain()}>
|
|
142
|
-
{(reserveLeadingSlot || leadingContent) && (
|
|
143
|
-
<span className={leadingSlot()}>{leadingContent}</span>
|
|
144
|
-
)}
|
|
145
|
-
<span className={labelSlot()}>{label}</span>
|
|
146
|
-
</span>
|
|
147
|
-
{checked && <Check size={12} className={check()} />}
|
|
148
|
-
</BaseButton>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function SelectionPopover(props: SelectionPopoverProps) {
|
|
153
|
-
const theme = useThemeFamily();
|
|
154
|
-
const {
|
|
155
|
-
title,
|
|
156
|
-
options,
|
|
157
|
-
showClear = false,
|
|
158
|
-
onClear,
|
|
159
|
-
showHeader = true,
|
|
160
|
-
emptyLabel = "No matches",
|
|
161
|
-
className,
|
|
162
|
-
} = props;
|
|
163
|
-
const {
|
|
164
|
-
root,
|
|
165
|
-
header,
|
|
166
|
-
title: titleSlot,
|
|
167
|
-
clear,
|
|
168
|
-
searchWrap,
|
|
169
|
-
searchInput,
|
|
170
|
-
list,
|
|
171
|
-
empty,
|
|
172
|
-
} = selectionPopover({ theme });
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div data-slot="selection-popover" className={root({ className })}>
|
|
176
|
-
{showHeader && (
|
|
177
|
-
<div className={header()}>
|
|
178
|
-
<span className={titleSlot()}>{title}</span>
|
|
179
|
-
{showClear && onClear && (
|
|
180
|
-
<BaseButton type="button" onClick={onClear} className={clear()}>
|
|
181
|
-
Clear
|
|
182
|
-
</BaseButton>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
186
|
-
{props.searchable && (
|
|
187
|
-
<div className={searchWrap()}>
|
|
188
|
-
{/* Reuses the brutal field skin (input-brutal → --shadow / --shadow-hover). */}
|
|
189
|
-
<input
|
|
190
|
-
type="text"
|
|
191
|
-
aria-label={props.searchPlaceholder ?? title}
|
|
192
|
-
value={props.search}
|
|
193
|
-
onChange={(event) => props.onSearchChange(event.target.value)}
|
|
194
|
-
onKeyDown={(event) => {
|
|
195
|
-
event.stopPropagation();
|
|
196
|
-
props.onInputKeyDown?.(event, options);
|
|
197
|
-
}}
|
|
198
|
-
placeholder={props.searchPlaceholder ?? "Search…"}
|
|
199
|
-
className={searchInput()}
|
|
200
|
-
/>
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
<div className={list()}>
|
|
204
|
-
{options.length === 0 ? (
|
|
205
|
-
<div className={empty()}>{emptyLabel}</div>
|
|
206
|
-
) : (
|
|
207
|
-
options.map(({ key, ...option }) => <SelectionPopoverOptionRow key={key} {...option} />)
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
);
|
|
212
|
-
}
|