@theruntime/components 0.0.0 → 0.0.1

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +113 -4
  3. package/src/accordion.tsx +62 -0
  4. package/src/alert-dialog.tsx +177 -0
  5. package/src/alert.tsx +60 -0
  6. package/src/aspect-ratio.tsx +7 -0
  7. package/src/attachment.tsx +192 -0
  8. package/src/avatar.tsx +94 -0
  9. package/src/badge.tsx +46 -0
  10. package/src/breadcrumb.tsx +102 -0
  11. package/src/bubble.tsx +125 -0
  12. package/src/button-group.tsx +82 -0
  13. package/src/button.tsx +62 -0
  14. package/src/calendar.tsx +178 -0
  15. package/src/card.tsx +75 -0
  16. package/src/carousel.tsx +228 -0
  17. package/src/chart/container.tsx +72 -0
  18. package/src/chart/context.tsx +39 -0
  19. package/src/chart/index.ts +4 -0
  20. package/src/chart/legend.tsx +63 -0
  21. package/src/chart/payload.ts +31 -0
  22. package/src/chart/tooltip.tsx +149 -0
  23. package/src/checkbox.tsx +27 -0
  24. package/src/cn.ts +36 -0
  25. package/src/collapsible.tsx +19 -0
  26. package/src/combobox/chips.tsx +71 -0
  27. package/src/combobox/content.tsx +123 -0
  28. package/src/combobox/index.ts +13 -0
  29. package/src/combobox/input.tsx +39 -0
  30. package/src/combobox/trigger.tsx +44 -0
  31. package/src/command.tsx +153 -0
  32. package/src/context-menu.tsx +222 -0
  33. package/src/dialog.tsx +142 -0
  34. package/src/direction.tsx +18 -0
  35. package/src/drawer.tsx +122 -0
  36. package/src/dropdown-menu.tsx +226 -0
  37. package/src/empty.tsx +94 -0
  38. package/src/field.tsx +232 -0
  39. package/src/form.tsx +153 -0
  40. package/src/hover-card.tsx +36 -0
  41. package/src/index.ts +90 -0
  42. package/src/input-group.tsx +156 -0
  43. package/src/input-otp.tsx +68 -0
  44. package/src/input.tsx +21 -0
  45. package/src/item.tsx +172 -0
  46. package/src/kbd.tsx +28 -0
  47. package/src/label.tsx +19 -0
  48. package/src/marker.tsx +66 -0
  49. package/src/menubar.tsx +250 -0
  50. package/src/message-scroller.tsx +128 -0
  51. package/src/message.tsx +85 -0
  52. package/src/native-select.tsx +56 -0
  53. package/src/navigation-menu.tsx +161 -0
  54. package/src/pagination.tsx +106 -0
  55. package/src/popover.tsx +72 -0
  56. package/src/progress.tsx +26 -0
  57. package/src/questionnaire/actions.tsx +118 -0
  58. package/src/questionnaire/choices.tsx +113 -0
  59. package/src/questionnaire/index.ts +21 -0
  60. package/src/questionnaire/root.tsx +75 -0
  61. package/src/radio-group.tsx +43 -0
  62. package/src/resizable.tsx +45 -0
  63. package/src/scroll-area.tsx +54 -0
  64. package/src/select.tsx +173 -0
  65. package/src/separator.tsx +26 -0
  66. package/src/sheet.tsx +132 -0
  67. package/src/shell/app-tab.tsx +67 -0
  68. package/src/shell/entry-row.tsx +137 -0
  69. package/src/shell/field.tsx +83 -0
  70. package/src/shell/group-head.tsx +19 -0
  71. package/src/shell/scope-chip.tsx +32 -0
  72. package/src/shell/suggest-panel.tsx +92 -0
  73. package/src/sidebar/context.tsx +128 -0
  74. package/src/sidebar/index.ts +23 -0
  75. package/src/sidebar/menu.tsx +239 -0
  76. package/src/sidebar/sections.tsx +118 -0
  77. package/src/sidebar/sidebar.tsx +183 -0
  78. package/src/skeleton.tsx +13 -0
  79. package/src/slider.tsx +56 -0
  80. package/src/sonner.tsx +41 -0
  81. package/src/spinner.tsx +16 -0
  82. package/src/styles.css +149 -0
  83. package/src/switch.tsx +33 -0
  84. package/src/table.tsx +90 -0
  85. package/src/tabs.tsx +79 -0
  86. package/src/textarea.tsx +18 -0
  87. package/src/toggle-group.tsx +81 -0
  88. package/src/toggle.tsx +44 -0
  89. package/src/tooltip.tsx +51 -0
  90. package/src/use-mobile.ts +21 -0
  91. package/README.md +0 -3
package/src/select.tsx ADDED
@@ -0,0 +1,173 @@
1
+ import * as React from "react";
2
+ import { CheckIcon, CaretDownIcon, CaretUpIcon } from "@phosphor-icons/react";
3
+ import { Select as SelectPrimitive } from "radix-ui";
4
+
5
+ import { cn } from "./cn";
6
+
7
+ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
9
+ }
10
+
11
+ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
12
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
13
+ }
14
+
15
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
16
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
17
+ }
18
+
19
+ function SelectTrigger({
20
+ className,
21
+ size = "default",
22
+ children,
23
+ ...props
24
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
25
+ size?: "sm" | "default";
26
+ }) {
27
+ return (
28
+ <SelectPrimitive.Trigger
29
+ data-slot="select-trigger"
30
+ data-size={size}
31
+ className={cn(
32
+ "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ {children}
38
+ <SelectPrimitive.Icon asChild>
39
+ <CaretDownIcon className="size-4 opacity-50" />
40
+ </SelectPrimitive.Icon>
41
+ </SelectPrimitive.Trigger>
42
+ );
43
+ }
44
+
45
+ function SelectContent({
46
+ className,
47
+ children,
48
+ position = "item-aligned",
49
+ align = "center",
50
+ ...props
51
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
52
+ return (
53
+ <SelectPrimitive.Portal>
54
+ <SelectPrimitive.Content
55
+ data-slot="select-content"
56
+ className={cn(
57
+ "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
58
+ position === "popper" &&
59
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
60
+ className,
61
+ )}
62
+ position={position}
63
+ align={align}
64
+ {...props}
65
+ >
66
+ <SelectScrollUpButton />
67
+ <SelectPrimitive.Viewport
68
+ className={cn(
69
+ "p-1",
70
+ position === "popper" &&
71
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
72
+ )}
73
+ >
74
+ {children}
75
+ </SelectPrimitive.Viewport>
76
+ <SelectScrollDownButton />
77
+ </SelectPrimitive.Content>
78
+ </SelectPrimitive.Portal>
79
+ );
80
+ }
81
+
82
+ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
83
+ return (
84
+ <SelectPrimitive.Label
85
+ data-slot="select-label"
86
+ className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
87
+ {...props}
88
+ />
89
+ );
90
+ }
91
+
92
+ function SelectItem({
93
+ className,
94
+ children,
95
+ ...props
96
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
97
+ return (
98
+ <SelectPrimitive.Item
99
+ data-slot="select-item"
100
+ className={cn(
101
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
102
+ className,
103
+ )}
104
+ {...props}
105
+ >
106
+ <span
107
+ data-slot="select-item-indicator"
108
+ className="absolute right-2 flex size-3.5 items-center justify-center"
109
+ >
110
+ <SelectPrimitive.ItemIndicator>
111
+ <CheckIcon className="size-4" />
112
+ </SelectPrimitive.ItemIndicator>
113
+ </span>
114
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
115
+ </SelectPrimitive.Item>
116
+ );
117
+ }
118
+
119
+ function SelectSeparator({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
123
+ return (
124
+ <SelectPrimitive.Separator
125
+ data-slot="select-separator"
126
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ function SelectScrollUpButton({
133
+ className,
134
+ ...props
135
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
136
+ return (
137
+ <SelectPrimitive.ScrollUpButton
138
+ data-slot="select-scroll-up-button"
139
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
140
+ {...props}
141
+ >
142
+ <CaretUpIcon className="size-4" />
143
+ </SelectPrimitive.ScrollUpButton>
144
+ );
145
+ }
146
+
147
+ function SelectScrollDownButton({
148
+ className,
149
+ ...props
150
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
151
+ return (
152
+ <SelectPrimitive.ScrollDownButton
153
+ data-slot="select-scroll-down-button"
154
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
155
+ {...props}
156
+ >
157
+ <CaretDownIcon className="size-4" />
158
+ </SelectPrimitive.ScrollDownButton>
159
+ );
160
+ }
161
+
162
+ export {
163
+ Select,
164
+ SelectContent,
165
+ SelectGroup,
166
+ SelectItem,
167
+ SelectLabel,
168
+ SelectScrollDownButton,
169
+ SelectScrollUpButton,
170
+ SelectSeparator,
171
+ SelectTrigger,
172
+ SelectValue,
173
+ };
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import { Separator as SeparatorPrimitive } from "radix-ui";
3
+
4
+ import { cn } from "./cn";
5
+
6
+ function Separator({
7
+ className,
8
+ orientation = "horizontal",
9
+ decorative = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ return (
13
+ <SeparatorPrimitive.Root
14
+ data-slot="separator"
15
+ decorative={decorative}
16
+ orientation={orientation}
17
+ className={cn(
18
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ export { Separator };
package/src/sheet.tsx ADDED
@@ -0,0 +1,132 @@
1
+ import * as React from "react";
2
+ import { XIcon } from "@phosphor-icons/react";
3
+ import { Dialog as SheetPrimitive } from "radix-ui";
4
+
5
+ import { cn } from "./cn";
6
+
7
+ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
8
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
9
+ }
10
+
11
+ function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
12
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
13
+ }
14
+
15
+ function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
16
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
17
+ }
18
+
19
+ function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
20
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
21
+ }
22
+
23
+ function SheetOverlay({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
27
+ return (
28
+ <SheetPrimitive.Overlay
29
+ data-slot="sheet-overlay"
30
+ className={cn(
31
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function SheetContent({
40
+ className,
41
+ children,
42
+ side = "right",
43
+ showCloseButton = true,
44
+ ...props
45
+ }: React.ComponentProps<typeof SheetPrimitive.Content> & {
46
+ side?: "top" | "right" | "bottom" | "left";
47
+ showCloseButton?: boolean;
48
+ }) {
49
+ return (
50
+ <SheetPortal>
51
+ <SheetOverlay />
52
+ <SheetPrimitive.Content
53
+ data-slot="sheet-content"
54
+ className={cn(
55
+ "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
56
+ side === "right" &&
57
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
58
+ side === "left" &&
59
+ "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
60
+ side === "top" &&
61
+ "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
62
+ side === "bottom" &&
63
+ "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
71
+ <XIcon className="size-4" />
72
+ <span className="sr-only">Close</span>
73
+ </SheetPrimitive.Close>
74
+ )}
75
+ </SheetPrimitive.Content>
76
+ </SheetPortal>
77
+ );
78
+ }
79
+
80
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
81
+ return (
82
+ <div
83
+ data-slot="sheet-header"
84
+ className={cn("flex flex-col gap-1.5 p-4", className)}
85
+ {...props}
86
+ />
87
+ );
88
+ }
89
+
90
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
91
+ return (
92
+ <div
93
+ data-slot="sheet-footer"
94
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
95
+ {...props}
96
+ />
97
+ );
98
+ }
99
+
100
+ function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
101
+ return (
102
+ <SheetPrimitive.Title
103
+ data-slot="sheet-title"
104
+ className={cn("font-semibold text-foreground", className)}
105
+ {...props}
106
+ />
107
+ );
108
+ }
109
+
110
+ function SheetDescription({
111
+ className,
112
+ ...props
113
+ }: React.ComponentProps<typeof SheetPrimitive.Description>) {
114
+ return (
115
+ <SheetPrimitive.Description
116
+ data-slot="sheet-description"
117
+ className={cn("text-sm text-muted-foreground", className)}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ export {
124
+ Sheet,
125
+ SheetTrigger,
126
+ SheetClose,
127
+ SheetContent,
128
+ SheetHeader,
129
+ SheetFooter,
130
+ SheetTitle,
131
+ SheetDescription,
132
+ };
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+
3
+ /* THE APP TAB. The kernel index's dense object: 32px identity mark on a flat
4
+ --muted panel (the colour the textures measured as: dL −0.027, C 1.25x vs
5
+ ground), name over optional caption, meta right. Hover is 5% ink over muted
6
+ (Δ13 levels, --accent's Δ5 vanishes). Promoted from the shell 2026-08-18. */
7
+
8
+ export function AppTab({
9
+ mark,
10
+ icon,
11
+ name,
12
+ caption,
13
+ meta,
14
+ tag,
15
+ onClick,
16
+ onContextMenu,
17
+ }: {
18
+ /** 32px identity square, e.g. the register's slice art on a style prop. */
19
+ mark?: React.CSSProperties;
20
+ /** The app's chosen emoji, drawn INSIDE the mark. The mark is derived
21
+ provenance, a colour that says which project this came from, so the icon
22
+ layers over it rather than replacing it: an app keeps its family resemblance
23
+ and gains an identity of its own. */
24
+ icon?: string;
25
+ name: string;
26
+ /** Dropped when it would restate its context (section, filter, or the name). */
27
+ caption?: string;
28
+ meta?: string;
29
+ tag?: string;
30
+ onClick?: () => void;
31
+ onContextMenu?: (e: React.MouseEvent) => void;
32
+ }) {
33
+ return (
34
+ <button
35
+ data-part={tag}
36
+ type="button"
37
+ onClick={onClick}
38
+ onContextMenu={onContextMenu}
39
+ className="group scale-chrome flex w-full items-center gap-2.5 rounded-[9px] bg-muted px-2.5 py-2.5 text-left transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_5%,var(--muted))]"
40
+ >
41
+ <span
42
+ className="relative grid size-8 shrink-0 place-items-center overflow-hidden rounded-[6px]"
43
+ style={{
44
+ border: "1px solid color-mix(in oklab, var(--foreground) 8%, transparent)",
45
+ ...mark,
46
+ }}
47
+ >
48
+ {icon && <span className="text-[15px] leading-none">{icon}</span>}
49
+ </span>
50
+ <span className="min-w-0 flex-1">
51
+ <span className="block truncate text-body font-medium leading-tight text-foreground">
52
+ {name}
53
+ </span>
54
+ {caption && (
55
+ <span className="block truncate text-caption leading-tight text-muted-foreground">
56
+ {caption}
57
+ </span>
58
+ )}
59
+ </span>
60
+ {meta && (
61
+ <span className="shrink-0 text-caption tabular-nums leading-none text-muted-foreground">
62
+ {meta}
63
+ </span>
64
+ )}
65
+ </button>
66
+ );
67
+ }
@@ -0,0 +1,137 @@
1
+ import * as React from "react";
2
+ import { Button } from "../button";
3
+
4
+ /* THE WORKING-SET ENTRY. The desktop kernel's list row: identity mark (art via
5
+ prop), name, and the three state channels (dim dot = attached, brand dot =
6
+ waiting on you, pulsing mark-ring = agent working). Promoted from the shell
7
+ 2026-08-18; data-part attributes are plain strings (no canvas coupling). */
8
+
9
+ export type Entry = {
10
+ /** The mark's ground, e.g. the register's slice art. Supplied by the app. */
11
+ art?: React.CSSProperties;
12
+ slug: string;
13
+ name: string;
14
+ /** The repo/dir this app lives under, revealed on hover of the mark. */
15
+ root: string;
16
+ /** Content colour. Absent = an INFERRED root: neutral, no colour. */
17
+ mark?: string;
18
+ /** A session is attached to this route. */
19
+ live?: boolean;
20
+ /** An agent is working in it right now. */
21
+ agent?: boolean;
22
+ /** Unread events waiting on you. */
23
+ notes?: number;
24
+ on?: boolean;
25
+ pinned?: boolean;
26
+ };
27
+
28
+ /* A real root wears the user's colour; an inferred one wears a neutral outline.
29
+ The difference is legible without a legend because one has colour and one
30
+ doesn't, in a single 18px square. */
31
+ function RootMark({ entry }: { entry: Entry }) {
32
+ return (
33
+ <span className="relative flex">
34
+ {/* The apps index's hash-sliced art, same source, same treatment. */}
35
+ <span
36
+ className="size-[18px] shrink-0 overflow-hidden rounded-[5px] border border-border"
37
+ style={entry.art ?? undefined}
38
+ />
39
+ {entry.agent && (
40
+ <span
41
+ aria-label="agent working"
42
+ className="pointer-events-none absolute -inset-[3px] animate-pulse rounded-[8px] border border-[var(--brand)]"
43
+ />
44
+ )}
45
+ </span>
46
+ );
47
+ }
48
+
49
+ export function EntryRow({
50
+ entry,
51
+ forceHover,
52
+ showPinSlot,
53
+ }: {
54
+ entry: Entry;
55
+ forceHover?: boolean;
56
+ showPinSlot?: boolean;
57
+ }) {
58
+ return (
59
+ <Button
60
+ data-part={`app-${entry.slug}`}
61
+ variant="ghost"
62
+ size="sm"
63
+ className={
64
+ /* The row's 13px is EXACTLY --type-title under .scale-chrome, but it
65
+ cannot be spelled `text-title` here: this className goes through
66
+ shadcn's `cn()`, and tailwind-merge parses an unknown `text-<name>`
67
+ as a text-COLOR utility, so `text-title` is dropped outright by the
68
+ `text-muted-foreground` / `text-accent-foreground` that follows it,
69
+ leaving Button's own `text-sm` (14px) to win. Measured. The scale
70
+ utilities are only safe on elements that do NOT go through `cn()`
71
+ until tailwind-merge is taught the register's font-size keys. */
72
+ "scale-chrome relative w-full justify-start gap-2.5 text-[13px] font-normal " +
73
+ (entry.on ? "bg-accent text-accent-foreground" : "text-muted-foreground")
74
+ }
75
+ >
76
+ {/* `group/mark` scopes the hover to the MARK: a row-wide hover would fire
77
+ the root name every time you were merely on your way to clicking the
78
+ app. */}
79
+ <span className="group/mark relative shrink-0 flex items-center">
80
+ <RootMark entry={entry} />
81
+ <span
82
+ data-part={`root-${entry.slug}`}
83
+ className={
84
+ /* BELOW the mark, not beside it. Beside MEASURED wrong: at 240px the
85
+ only space to a left-edge mark's right is the app name, so the chip
86
+ covered the first characters of the row you were pointing at and
87
+ read as a rendering fault rather than a tooltip. Below-left is also
88
+ simply what Tooltip does by default, which is the tell that the
89
+ beside-placement was fighting the pattern rather than adapting it. */
90
+ "pointer-events-none absolute left-0 top-[calc(100%+6px)] z-10 " +
91
+ "whitespace-nowrap rounded-[6px] border px-2 py-1 text-caption leading-none " +
92
+ "bg-popover text-popover-foreground shadow-lg transition-opacity " +
93
+ (forceHover ? "opacity-100" : "opacity-0 group-hover/mark:opacity-100")
94
+ }
95
+ >
96
+ {entry.root}
97
+ </span>
98
+ </span>
99
+ {/* Pinned is NEUTRAL: a glyph, not a colour and not a fill. It marks
100
+ how the row GOT here, you put it there, which is a different axis
101
+ from what-you-owe-vs-what-owes-you, and both can be true. */}
102
+ {/* A RESERVED SLOT, present on every row, occupied only when pinned.
103
+ MEASURED without it: a pinned row's name started at x 78 and an
104
+ unpinned one at x 52, 26px of ragged left edge down the list, which is
105
+ the same defect an earlier kind token was built to avoid and this
106
+ reintroduced one component over. The cost is honest and worth naming:
107
+ ~14px of name width spent on EVERY row to keep the column straight, on
108
+ a rail whose longest real name already needs 232 of 240.
109
+ The alternative, a pin badge on the project mark, costs nothing
110
+ horizontally and is rejected on different grounds: at 18px that square
111
+ already carries a gradient and (collapsed) a liveness dot, and a later
112
+ round measured that small marks stop reading once they carry three
113
+ signals. */}
114
+ {showPinSlot && (
115
+ <span className="shrink-0 w-[11px] flex items-center justify-center">
116
+ {entry.pinned && (
117
+ <span aria-label="pinned" className="size-[5px] rounded-full bg-current opacity-70" />
118
+ )}
119
+ </span>
120
+ )}
121
+ <span className="truncate">{entry.name}</span>
122
+ {/* One slot, one geometry, two weights: brand = something waits on you,
123
+ dim = merely attached. */}
124
+ {entry.notes ? (
125
+ <span
126
+ aria-label="waiting"
127
+ className="ml-auto shrink-0 size-[5px] rounded-full bg-[var(--brand)]"
128
+ />
129
+ ) : entry.live ? (
130
+ <span
131
+ aria-label="connected"
132
+ className="ml-auto shrink-0 size-[5px] rounded-full bg-current opacity-35"
133
+ />
134
+ ) : null}
135
+ </Button>
136
+ );
137
+ }
@@ -0,0 +1,83 @@
1
+ import * as React from "react";
2
+ import { Badge } from "../badge";
3
+
4
+ /* THE FIELD FAMILY. The kernel bar's centre slot: one field shell so its two
5
+ tenancies (address / search) can never drift on geometry, the queue plate
6
+ (count + ⌘⏎ chord, sans on purpose, mono ⌘ floats off-baseline), and the
7
+ ⌘K hint it evicts. Promoted from the shell 2026-08-18. */
8
+
9
+ const FIELD = {
10
+ background: "color-mix(in oklab, var(--paper) 10%, transparent)",
11
+ };
12
+
13
+ export function Field({
14
+ children,
15
+ width = 420,
16
+ }: {
17
+ children: React.ReactNode;
18
+ width?: number | string;
19
+ }) {
20
+ return (
21
+ <div
22
+ data-part="address-field"
23
+ className="scale-chrome h-[30px] rounded-[8px] flex items-center px-2.5 gap-2.5"
24
+ style={{ ...FIELD, width }}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ /* v13's StateSlot at the new height. Same rule, unchanged: a queue evicts the
32
+ hint, and the two-digit column is reserved so the plate never changes width
33
+ between 1 and 12. */
34
+ export function StateSlot({ count, onSend }: { count: number; onSend?: () => void }) {
35
+ if (count > 0) {
36
+ return (
37
+ <Badge
38
+ data-part="queue-slot"
39
+ asChild
40
+ variant="default"
41
+ className="scale-chrome ml-auto shrink-0 h-[17px] px-1.5 py-0"
42
+ >
43
+ <button
44
+ type="button"
45
+ onClick={onSend}
46
+ aria-label={`Send ${count} to session, Command Return`}
47
+ title="Send to session, ⌘⏎"
48
+ className="flex items-center gap-1"
49
+ >
50
+ <span
51
+ data-part="queue-count"
52
+ className="text-meta font-semibold leading-none tabular-nums text-center"
53
+ style={{ minWidth: 13 }}
54
+ >
55
+ {count}
56
+ </span>
57
+ {/* NOT font-mono, that is the "better key shortcut stuff": the
58
+ ⌘K hint already measured that every mono face on this box ships a
59
+ small, baseline-floating ⌘ (JetBrains 6.25px, descent −1.05) while
60
+ the sans stack renders it at cap height sitting on the baseline.
61
+ An earlier pass set this chord in mono and would have reintroduced
62
+ the exact defect a later fix addressed. SEMIBOLD is that fix's
63
+ finding and it holds: weight, not size, is what keeps the ⏎ from
64
+ collapsing to a tick. */}
65
+ <span data-part="queue-chord" className="text-meta font-semibold leading-none opacity-65">
66
+ ⌘⏎
67
+ </span>
68
+ </button>
69
+ </Badge>
70
+ );
71
+ }
72
+ return <KbdHint />;
73
+ }
74
+ export function KbdHint() {
75
+ return (
76
+ <span
77
+ data-part="kbd-hint"
78
+ className="scale-chrome ml-auto shrink-0 text-caption leading-none text-muted-foreground tracking-[0.06em]"
79
+ >
80
+ ⌘K
81
+ </span>
82
+ );
83
+ }
@@ -0,0 +1,19 @@
1
+ /* THE SECTION HEAD. A header is a word, so it has to earn its place. It does,
2
+ because a grouped list creates exactly two questions the layout
3
+ cannot answer on its own: which group am I in, and how much is in here. So the
4
+ label and the count, and nothing else. No rule, no chevron, no "N apps": the
5
+ grid below already draws the section's own left edge and the tabs already say
6
+ what they are.
7
+
8
+ Set in the tab's own name type (12.5px medium) rather than a smaller uppercase
9
+ micro-label, because a section head and an app name are peers in the hierarchy
10
+ here. One names a place, one names a thing in it, and neither outranks the
11
+ other. The count drops to the caption size to stay subordinate to its label. */
12
+ export function GroupHead({ label, count }: { label: string; count: number }) {
13
+ return (
14
+ <div className="scale-chrome flex items-baseline gap-2 pb-1.5 pt-2">
15
+ <span className="text-body font-medium leading-none text-foreground">{label}</span>
16
+ <span className="text-caption leading-none tabular-nums text-muted-foreground">{count}</span>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,32 @@
1
+ /* THE SCOPE CHIP. The kernel's filter chip: lit = ink inversion (a two-state
2
+ control read in peripheral vision; a tint would sit inside the resting band).
3
+ Tap the lit chip again to clear, a reset needs to exist, not a word.
4
+ Promoted from the shell 2026-08-18. */
5
+
6
+ export function ScopeChip({
7
+ label,
8
+ on,
9
+ onClick,
10
+ tag,
11
+ }: {
12
+ label: string;
13
+ on: boolean;
14
+ onClick: () => void;
15
+ tag?: string;
16
+ }) {
17
+ return (
18
+ <button
19
+ data-part={tag}
20
+ type="button"
21
+ onClick={onClick}
22
+ className={
23
+ "flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11.5px] leading-none transition-colors " +
24
+ (on
25
+ ? "bg-foreground text-background"
26
+ : "bg-muted text-foreground hover:bg-[color-mix(in_oklab,var(--foreground)_5%,var(--muted))]")
27
+ }
28
+ >
29
+ <span className="font-medium">{label}</span>
30
+ </button>
31
+ );
32
+ }